Crossing Paths: A Gopher's Encounter with a Crab in the World of Go and Web Development
As a long-time Gopher—someone who lives and breathes Go—I’ve built microservices, CLI tools, and backend APIs with the kind of confidence that only comes from years of wrestling with sync.Mutex and context.WithTimeout. But recently, I crossed paths with a Crab—a Rustacean who wandered into our Go-dominated ecosystem, asking questions that made me pause. Not because they were wrong, but because they exposed blind spots we Gophers often ignore.
This article isn’t about Go vs. Rust. It’s about the non-obvious pitfalls in Go web development that even experienced developers overlook—mistakes that become glaring when viewed through a different lens. Let’s unpack them.
1. Treating context.Context as Optional
We all know context is important. But in practice, it’s often tacked on as an afterthought.
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
user, err := h.store.GetUser(r.URL.Query().Get("id"))
// ...
}
Where’s the context? Nowhere. This function can’t be canceled, timed out, or traced properly.
The insight: Every handler, every database call, every goroutine should accept a context.Context. Not because it’s “best practice,” but because you’re building a distributed system, even if it’s just one service calling PostgreSQL.
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() // Use the request's context
user, err := h.store.GetUser(ctx, r.URL.Query().Get("id"))
// ...
}
And ensure your GetUser method actually respects cancellation.
2. Ignoring Structured Logging
Gophers love fmt.Println. It’s simple. It works. Until you’re debugging a production issue across 12 pods.
log.Printf("User %s logged in", userID)
This is unstructured noise. You can’t filter, aggregate, or correlate it efficiently.
The insight: Use structured logging. Tools like Zerolog or Slog (Go 1.21+) are not optional.
logger.Info("user logged in", "user_id", userID, "ip", r.RemoteAddr)
Now you can query: “Show me all logins from IP X in the last hour.” Without parsing strings.
And yes, structured logs are faster than fmt.Sprintf-based ones when done right.
3. Misusing Goroutines: The “Fire and Forget” Anti-Pattern
go func() {
sendEmail(user.Email, "Welcome!")
}()
This is dangerous. You’ve spawned a goroutine with no way to:
- Cancel it
- Wait for it
- Handle errors
- Limit concurrency
If this runs under load, you’ll exhaust system resources.
The insight: Use worker pools, errgroup, or at least sync.WaitGroup when launching goroutines. For background tasks, consider a proper queue (e.g., Redis + worker) or use context to ensure cleanup.
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error {
return sendEmail(ctx, user.Email, "Welcome!")
})
if err := g.Wait(); err != nil {
log.Printf("email failed: %v", err)
}
Now you can cancel, wait, and handle errors.
4. Treating json.Unmarshal as Infallible
var req LoginRequest
json.NewDecoder(r.Body).Decode(&req)
// Proceed assuming req is valid
This is a security and reliability time bomb. What if the JSON is malformed? What if a field is missing? What if it’s a string where you expect a number?
The insight: Always check errors. Always validate.
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid JSON", http.StatusBadRequest)
return
}
if req.Email == "" || req.Password == "" {
http.Error(w, "missing fields", http.StatusBadRequest)
return
}
Better yet, use a validation library like go-playground/validator and validate after unmarshaling.
5. Ignoring HTTP/2 and Connection Reuse
Many Go web servers run behind proxies like Nginx or load balancers. But developers often forget that client-side HTTP calls (e.g., to external APIs) need proper http.Client tuning.
resp, err := http.Get("https://api.example.com/data")
This creates a new connection every time. No reuse. No limits. No timeouts.
The insight: Reuse http.Client and configure Transport.
client := &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxConnsPerHost: 10,
IdleConnTimeout: 30 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
},
}
And reuse this client globally. Don’t create one
☕ Appreciative
Top comments (0)