Why I Even Tried Go
I've spent most of my career writing PHP — and later JavaScript/TypeScript. jQuery, Vue, and now React + Next.js. The ecosystem is enormous, the feedback loop is instant, and console.log has gotten me out of more jams than I'd like to admit.
So why Go?
Honestly, a new company project pushed me there. The requirement was a high-throughput API that needed to handle concurrent connections without melting a modest server. I'd heard the Go community talk about goroutines like they were magic, and I was curious enough to give it a proper shot.
I set aside a weekend, cloned the official docs, and wrote my first main.go. I was immediately humbled.
The First Ten Minutes
The syntax is clean. Suspiciously clean, actually, like someone looked at every other language and asked, "what can we remove?" No classes. No exceptions (sort of). No implicit anything.
package main
import "fmt"
func main() {
name := "Elva"
fmt.Println("Hello,", name)
}
Coming from TypeScript, the := walrus operator felt weird for about ten minutes, then completely natural. The compiler is fast — I ran go run main.go and it executed so quickly I thought it hadn't done anything.
Then I wrote a function:
func add(a int, b int) int {
return a + b
}
The Go compiler yelled at me for importing a package I wasn't using. Then it yelled at me for declaring a variable I wasn't using. I felt judged. But then I realised this is the compiler acting like a senior developer who catches your lazy habits before they ship to production. I started to respect it.
Building My First HTTP Server
I wanted to build something real, so I looked up how to write an HTTP server. The standard library can do it:
package main
import (
"fmt"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello from Go!")
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
It worked. And it was fast. Genuinely fast in a way that felt different from Node.js. No V8 warm-up, no event loop startup cost. Just instant responses.
But the routing API felt low-level for what I needed. I started looking for something closer to Express.js, and that's when I found GoFiber.
Enter GoFiber
GoFiber bills itself as an Express-inspired framework for Go. The moment I saw the README, I felt at home:
package main
import "github.com/gofiber/fiber/v2"
func main() {
app := fiber.New()
app.Get("/", func(c *fiber.Ctx) error {
return c.SendString("Hello from GoFiber!")
})
app.Listen(":3000")
}
Route groups, middleware, JSON parsing, request context — it was all there, with an API that felt familiar. If you know Express, you can be productive with GoFiber in an afternoon.
app.Get("/users/:id", func(c *fiber.Ctx) error {
id := c.Params("id")
user := findUser(id) // your DB call
if user == nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"error": "user not found",
})
}
return c.JSON(user)
})
GoFiber has been my Go framework of choice ever since. It gets out of your way and lets you focus on the business logic.
The Week I Spent Confused For No Reason
Here's the part of the story I think a lot of developers don't talk about: the silent failure that isn't actually a failure.
In scripting land like PHP or JavaScript/Node.js, especially with Next.js or nodemon, you save a file and the change is there. Immediately. The dev server watches your files and restarts automatically. It's been this way for so long that it's become invisible to me — I just assumed that's how development servers work.
Go is a compiled language. There is no magic file watcher (unless you add one). When you run your server with go run main.go, it compiles your code once, starts the binary, and that's it. From that point on, your running server has no idea what's happening in your text editor.
So for roughly a week, this was my loop:
- Write a new route or fix a bug
- Test it in the browser / Postman
- "Hm, the change isn't there."
- Check my code — it looks right.
- Check the terminal — no errors.
- Stare at the screen for five minutes.
- Google something unrelated.
- Suddenly remember to restart the server (again).
- "Yep, it works now. LOL."
I genuinely thought I was making some obscure Go scoping mistake. I was convinced the language had weird behaviour around package reloading. I filed this away as "a Go quirk I don't understand yet" and kept moving.
It took a teammate (who had used Go before) looking over my shoulder and saying, completely casually, "oh, you need to restart the server after changes", for the penny to finally drop.
I restarted the server. Everything worked exactly as I'd written it.
I had been debugging phantom bugs for hour. The code was always fine. I just kept testing stale binaries.
The Fix: Air for Live Reloading
Once I knew what the problem was, the solution was obvious: use a tool that watches for file changes and restarts the server automatically. The community's standard answer is Air.
Installation is straightforward:
go install github.com/cosmtrek/air@latest
Initialise a config in your project root:
air init
Then just run air instead of go run main.go, and you get the hot-reload experience you're used to from the Node.js world:
__ _ ___
/ /\ | | | |_)
/_/--\ |_| |_| \_ v1.40.4
watching .
watching routes
watching handlers
building...
running...
Edit a file. Save it. Air recompiles and restarts. Done. My development loop was instantly back to normal and I finally stopped gaslighting myself into thinking my code was wrong.
What I Actually Enjoyed About Go
Despite (or maybe because of) the learning friction, Go genuinely grew on me. A few things stand out:
Goroutines are as good as advertised. Spawning thousands of concurrent goroutines for parallel work feels trivial. The channel-based communication model takes getting used to, but once it clicks it's elegant.
func fetchAll(urls []string) []string {
results := make([]string, len(urls))
var wg sync.WaitGroup
for i, url := range urls {
wg.Add(1)
go func(i int, url string) {
defer wg.Done()
results[i] = fetch(url) // runs concurrently
}(i, url)
}
wg.Wait()
return results
}
Error handling is explicit. Go doesn't have exceptions — functions return errors as values. At first this felt verbose. Then I realised I was actually reading every error path instead of assuming it wouldn't happen.
user, err := db.GetUser(id)
if err != nil {
return nil, fmt.Errorf("fetching user %s: %w", id, err)
}
The build output is a single binary. No runtime to install, no node_modules to ship. go build produces one file that runs anywhere. For deploying services this is genuinely lovely.
Would I Use Go Again?
Yes — and I already have, multiple times since. GoFiber's ergonomics make it a great fit for REST APIs where you need speed and straightforward routing without ceremony. The ecosystem isn't as rich as Node.js, but the standard library covers a surprising amount of ground.
If you're a JavaScript developer thinking about trying Go, my advice is simple: just be aware that the server doesn't restart itself. Learn that on day one, save yourself a week of confusion, and you'll have a much better time.
The rest of the language is actually great. The Gopher mascot is fantastic. And the compiler will make you a better programmer whether you want it to or not.
