DEV Community

Orbit Websites
Orbit Websites

Posted on

Crossing Paths: A Gopher's Encounter with a Crab in the World of Go and Web Development

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"))
    // ...
}
Enter fullscreen mode Exit fullscreen mode

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"))
    // ...
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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!")
}()
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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,
    },
}
Enter fullscreen mode Exit fullscreen mode

And reuse this client globally. Don’t create one


Appreciative

Top comments (0)