HTTP & Web Development in Go
Go’snet/http package is powerful enough for production use without external dependencies. This sets Go apart from most languages where you need a framework (Express, Flask, Spring) for anything beyond a toy server. Companies like Cloudflare, Dropbox, and Netflix run production services using just the standard library’s HTTP server, sometimes with a lightweight router on top. This chapter covers building robust web services from the ground up.
The net/http Package
Basic HTTP Server
http.Handler Interface
ServeHTTP can handle HTTP requests:
JSON APIs
Encoding JSON Responses
Decoding JSON Requests
Routing
Standard Library Router (Go 1.22+)
Go 1.22 introduced enhanced routing patterns:Custom Router
Middleware
Middleware are functions that wrap handlers to add functionality. Think of middleware as layers in an onion: each request passes through the outer layers (logging, auth, CORS) before reaching the core handler, and the response passes back out through the same layers. This is the Go version of the decorator pattern, and it is the standard way to add cross-cutting concerns like authentication, logging, rate limiting, and panic recovery.Middleware Pattern
Logging Middleware
Recovery Middleware
CORS Middleware
Authentication Middleware
Rate Limiting Middleware
Using Middleware
HTTP Server Configuration
Production-Ready Server
TLS Configuration
HTTP Client
Basic Client
Production HTTP Client
A production HTTP client is like a well-tuned car engine: it needs proper connection pooling (how many connections to keep warm), timeouts (when to give up), and transport settings (how to manage the underlying TCP connections). Thehttp.Transport is the engine; the http.Client is the driver interface.
Retry with Exponential Backoff
Popular Frameworks
Chi Router
Gin Framework
Echo Framework
Interview Questions
What's the difference between http.Handle and http.HandleFunc?
What's the difference between http.Handle and http.HandleFunc?
http.Handletakes anhttp.Handlerinterfacehttp.HandleFunctakes a function with signaturefunc(w http.ResponseWriter, r *http.Request)
HandleFunc is a convenience wrapper that converts the function to a HandlerFunc type which implements Handler.How would you implement request timeouts?
How would you implement request timeouts?
- Server-level: Set
ReadTimeout,WriteTimeoutonhttp.Server - Request-level: Use
context.WithTimeoutwithhttp.NewRequestWithContext - Handler-level: Use
http.TimeoutHandlerwrapper - Middleware: Create custom timeout middleware
What's the purpose of defer resp.Body.Close()?
What's the purpose of defer resp.Body.Close()?
- Release the connection back to the pool for reuse
- Prevent resource leaks (file descriptors, memory)
- Allow the transport to reuse connections (keep-alive)
How do you prevent slow clients from holding connections?
How do you prevent slow clients from holding connections?
- Set appropriate server timeouts (
ReadTimeout,WriteTimeout,IdleTimeout) - Use
http.MaxBytesReaderto limit request body size - Implement request deadlines with context
- Use rate limiting middleware
Summary
Interview Deep-Dive
Go's net/http package is considered production-ready without frameworks. What specifically makes it sufficient, and when would you reach for Chi, Gin, or Echo instead?
Go's net/http package is considered production-ready without frameworks. What specifically makes it sufficient, and when would you reach for Chi, Gin, or Echo instead?
- The net/http standard library provides: a production-grade HTTP server with configurable timeouts, keep-alive, TLS, and graceful shutdown; the
http.Handlerinterface (one method:ServeHTTP) which is the foundation of Go’s composable middleware ecosystem; built-in connection pooling in the HTTP client; and since Go 1.22, method-based routing with path parameters (GET /users/{id}). - What it lacks: Go 1.22 routing covers most cases, but before that, the default mux only did prefix matching without method dispatch, which was insufficient for REST APIs. Other missing pieces: route groups with shared middleware, built-in request validation/binding, and automatic OPTIONS/CORS handling.
- When to use a framework: Chi is my default choice when I need route groups, middleware chaining, and URL parameters on Go versions before 1.22. Chi is just a router that wraps the standard
http.Handlerinterface, so your handlers are portable. Gin is appropriate when you want performance-optimized routing (radix tree), built-in JSON binding/validation, and your team is comfortable with Gin’s non-standard*gin.Contextinstead ofhttp.ResponseWriter + *http.Request. Echo is similar to Gin in philosophy. - My recommendation for new projects: start with the standard library plus Go 1.22 routing. Add Chi if you need route groups or are on an older Go version. Only reach for Gin/Echo if your team already knows them or you need their specific middleware ecosystem.
func(http.Handler) http.Handler enable composable middleware?The type func(http.Handler) http.Handler is a function that takes a handler and returns a new handler that wraps the original. This is the decorator pattern applied to HTTP handling. Each middleware adds behavior (logging, auth, CORS, rate limiting) before or after calling the inner handler. Because every middleware has the same signature, they compose naturally. A Chain helper reverses this nesting for readability. The beauty is that any middleware works with any handler from any library, because they all speak the same http.Handler interface. This is why Go does not need a monolithic framework — the interface IS the framework.You are building a JSON API in Go. Walk me through the security considerations for handling request bodies, and show me the production-grade pattern.
You are building a JSON API in Go. Walk me through the security considerations for handling request bodies, and show me the production-grade pattern.
- First, limit the request body size. An attacker can send a multi-gigabyte body to exhaust memory. Use
http.MaxBytesReader(w, r.Body, 1<<20)to cap at 1MB (or whatever is appropriate). This replacesr.Bodywith a reader that returns an error when the limit is exceeded. - Second, use
json.NewDecoder(r.Body).Decode(&req)instead ofio.ReadAll+json.Unmarshal. The decoder streams the JSON instead of loading the entire body into memory, and withMaxBytesReader, it fails early on oversized payloads. - Third, call
decoder.DisallowUnknownFields()to reject JSON with fields not in your struct. This catches typos and prevents unexpected data from silently being accepted. - Fourth, validate the decoded struct. Use a validation library like
go-playground/validatorwith struct tags (validate:"required,email"), or write explicit validation logic. Never trust client input. - Fifth, prevent JSON injection: always set
Content-Type: application/jsonon responses, never interpolate user input into JSON strings manually, and sanitize any data that might be rendered in HTML contexts. - Sixth, handle the error from
json.NewEncoder(w).Encode(response). If the client disconnects mid-response, this write can fail, and you want to log it rather than crash.
ReadTimeout limits how long the server waits to read the full request. WriteTimeout limits how long the server allows for writing the response. IdleTimeout controls how long keep-alive connections stay open between requests. ReadHeaderTimeout (often overlooked) limits reading just the headers, protecting against slowloris attacks. In production, I set ReadTimeout: 5s, WriteTimeout: 10s, IdleTimeout: 120s, ReadHeaderTimeout: 2s. Without these, a single slow or malicious client can consume a goroutine and connection slot forever.The default HTTP client in Go has no timeout. Explain why this is dangerous and show me the production-grade HTTP client configuration.
The default HTTP client in Go has no timeout. Explain why this is dangerous and show me the production-grade HTTP client configuration.
http.DefaultClienthasTimeout: 0, which means requests can block indefinitely. If the target server is slow or unresponsive, your goroutine hangs forever. In a service handling concurrent requests, this quickly leads to goroutine exhaustion.- The production configuration includes:
Timeout: 30 * time.Secondon the client (overall request timeout), and detailed transport settings:MaxIdleConns: 100,MaxIdleConnsPerHost: 10,IdleConnTimeout: 90 * time.Second. - Additionally, always use
http.NewRequestWithContext(ctx, method, url, body)to create requests with context-based cancellation. This ties the request to the caller’s context, so if the caller times out or cancels, the HTTP request is aborted immediately. - Critical detail: always read and close the response body, even on error responses. If you do not drain the body, the underlying TCP connection cannot be reused. The pattern is:
defer resp.Body.Close()and for error cases:io.Copy(io.Discard, resp.Body).
baseDelay * 2^attempt + jitter, where jitter prevents thundering herd. Always check ctx.Done() between retries so cancellation is respected. And always close the response body of failed attempts before retrying, or you leak connections.