HTTP & Web Development in Go
Go’snet/http package is powerful enough for production use without external dependencies. This chapter covers building robust web services from the ground up.
The net/http Package
Basic HTTP Server
Copy
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", homeHandler)
http.HandleFunc("/api/users", usersHandler)
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
func homeHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Welcome to the API!")
}
func usersHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
getUsers(w, r)
case http.MethodPost:
createUser(w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
http.Handler Interface
Copy
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
ServeHTTP can handle HTTP requests:
Copy
type APIHandler struct {
db *sql.DB
}
func (h *APIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Handle request with access to h.db
}
func main() {
handler := &APIHandler{db: connectDB()}
http.Handle("/api/", handler)
http.ListenAndServe(":8080", nil)
}
JSON APIs
Encoding JSON Responses
Copy
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
CreatedAt time.Time `json:"created_at"`
}
type Response struct {
Data interface{} `json:"data,omitempty"`
Error string `json:"error,omitempty"`
Message string `json:"message,omitempty"`
}
func respondJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(data); err != nil {
log.Printf("Error encoding response: %v", err)
}
}
func respondError(w http.ResponseWriter, status int, message string) {
respondJSON(w, status, Response{Error: message})
}
func getUserHandler(w http.ResponseWriter, r *http.Request) {
user := User{
ID: 1,
Name: "John Doe",
Email: "[email protected]",
CreatedAt: time.Now(),
}
respondJSON(w, http.StatusOK, Response{Data: user})
}
Decoding JSON Requests
Copy
type CreateUserRequest struct {
Name string `json:"name" validate:"required,min=2,max=100"`
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=8"`
}
func createUserHandler(w http.ResponseWriter, r *http.Request) {
// Limit request body size
r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1MB
var req CreateUserRequest
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields() // Strict parsing
if err := decoder.Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "Invalid JSON: "+err.Error())
return
}
// Validate (using validator package)
if err := validate.Struct(req); err != nil {
respondError(w, http.StatusBadRequest, "Validation failed: "+err.Error())
return
}
// Create user...
user := createUser(req)
respondJSON(w, http.StatusCreated, Response{Data: user})
}
Routing
Standard Library Router (Go 1.22+)
Go 1.22 introduced enhanced routing patterns:Copy
func main() {
mux := http.NewServeMux()
// Method-specific routing (Go 1.22+)
mux.HandleFunc("GET /api/users", listUsers)
mux.HandleFunc("POST /api/users", createUser)
mux.HandleFunc("GET /api/users/{id}", getUser)
mux.HandleFunc("PUT /api/users/{id}", updateUser)
mux.HandleFunc("DELETE /api/users/{id}", deleteUser)
// Wildcard patterns
mux.HandleFunc("GET /files/{path...}", serveFiles)
http.ListenAndServe(":8080", mux)
}
func getUser(w http.ResponseWriter, r *http.Request) {
// Extract path parameter (Go 1.22+)
id := r.PathValue("id")
// Or manually for older Go versions
// id := strings.TrimPrefix(r.URL.Path, "/api/users/")
user, err := findUser(id)
if err != nil {
respondError(w, http.StatusNotFound, "User not found")
return
}
respondJSON(w, http.StatusOK, Response{Data: user})
}
Custom Router
Copy
type Router struct {
routes map[string]map[string]http.HandlerFunc
}
func NewRouter() *Router {
return &Router{
routes: make(map[string]map[string]http.HandlerFunc),
}
}
func (router *Router) Handle(method, path string, handler http.HandlerFunc) {
if router.routes[path] == nil {
router.routes[path] = make(map[string]http.HandlerFunc)
}
router.routes[path][method] = handler
}
func (router *Router) GET(path string, handler http.HandlerFunc) {
router.Handle(http.MethodGet, path, handler)
}
func (router *Router) POST(path string, handler http.HandlerFunc) {
router.Handle(http.MethodPost, path, handler)
}
func (router *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if methods, ok := router.routes[r.URL.Path]; ok {
if handler, ok := methods[r.Method]; ok {
handler(w, r)
return
}
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
http.NotFound(w, r)
}
Middleware
Middleware are functions that wrap handlers to add functionality.Middleware Pattern
Copy
type Middleware func(http.Handler) http.Handler
func Chain(h http.Handler, middlewares ...Middleware) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
h = middlewares[i](h)
}
return h
}
Logging Middleware
Copy
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Wrap ResponseWriter to capture status code
wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
next.ServeHTTP(wrapped, r)
log.Printf(
"%s %s %d %s",
r.Method,
r.URL.Path,
wrapped.statusCode,
time.Since(start),
)
})
}
type responseWriter struct {
http.ResponseWriter
statusCode int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
Recovery Middleware
Copy
func RecoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("Panic recovered: %v\n%s", err, debug.Stack())
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
CORS Middleware
Copy
func CORSMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
Authentication Middleware
Copy
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Validate token
claims, err := validateToken(strings.TrimPrefix(token, "Bearer "))
if err != nil {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
// Add claims to context
ctx := context.WithValue(r.Context(), userClaimsKey{}, claims)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Rate Limiting Middleware
Copy
func RateLimitMiddleware(rps int) Middleware {
limiter := rate.NewLimiter(rate.Limit(rps), rps)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !limiter.Allow() {
http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
}
Using Middleware
Copy
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/api/users", usersHandler)
mux.HandleFunc("/api/orders", ordersHandler)
// Apply middleware chain
handler := Chain(
mux,
RecoveryMiddleware,
LoggingMiddleware,
CORSMiddleware,
RateLimitMiddleware(100),
)
http.ListenAndServe(":8080", handler)
}
HTTP Server Configuration
Production-Ready Server
Copy
func main() {
mux := setupRoutes()
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
MaxHeaderBytes: 1 << 20, // 1MB
}
// Graceful shutdown
go func() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
log.Println("Shutting down server...")
if err := srv.Shutdown(ctx); err != nil {
log.Printf("Server shutdown error: %v", err)
}
}()
log.Printf("Server starting on %s", srv.Addr)
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
log.Println("Server stopped")
}
TLS Configuration
Copy
func main() {
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
},
}
srv := &http.Server{
Addr: ":443",
Handler: mux,
TLSConfig: tlsConfig,
}
log.Fatal(srv.ListenAndServeTLS("cert.pem", "key.pem"))
}
HTTP Client
Basic Client
Copy
func fetchUser(id string) (*User, error) {
resp, err := http.Get("https://api.example.com/users/" + id)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode)
}
var user User
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
return nil, err
}
return &user, nil
}
Production HTTP Client
Copy
type APIClient struct {
baseURL string
httpClient *http.Client
apiKey string
}
func NewAPIClient(baseURL, apiKey string) *APIClient {
return &APIClient{
baseURL: baseURL,
apiKey: apiKey,
httpClient: &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
},
},
}
}
func (c *APIClient) do(ctx context.Context, method, path string, body interface{}) (*http.Response, error) {
var bodyReader io.Reader
if body != nil {
jsonBody, err := json.Marshal(body)
if err != nil {
return nil, err
}
bodyReader = bytes.NewReader(jsonBody)
}
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, bodyReader)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+c.apiKey)
req.Header.Set("User-Agent", "MyApp/1.0")
return c.httpClient.Do(req)
}
func (c *APIClient) GetUser(ctx context.Context, id string) (*User, error) {
resp, err := c.do(ctx, http.MethodGet, "/users/"+id, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return nil, ErrNotFound
}
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("API error: %d - %s", resp.StatusCode, string(body))
}
var user User
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
return nil, err
}
return &user, nil
}
func (c *APIClient) CreateUser(ctx context.Context, user *CreateUserRequest) (*User, error) {
resp, err := c.do(ctx, http.MethodPost, "/users", user)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
return nil, parseAPIError(resp)
}
var created User
if err := json.NewDecoder(resp.Body).Decode(&created); err != nil {
return nil, err
}
return &created, nil
}
Retry with Exponential Backoff
Copy
func (c *APIClient) doWithRetry(ctx context.Context, method, path string, body interface{}) (*http.Response, error) {
maxRetries := 3
baseDelay := 100 * time.Millisecond
var lastErr error
for attempt := 0; attempt < maxRetries; attempt++ {
if attempt > 0 {
delay := baseDelay * time.Duration(1<<uint(attempt-1))
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(delay):
}
}
resp, err := c.do(ctx, method, path, body)
if err != nil {
lastErr = err
continue
}
// Don't retry on client errors
if resp.StatusCode >= 400 && resp.StatusCode < 500 {
return resp, nil
}
// Retry on server errors
if resp.StatusCode >= 500 {
resp.Body.Close()
lastErr = fmt.Errorf("server error: %d", resp.StatusCode)
continue
}
return resp, nil
}
return nil, fmt.Errorf("max retries exceeded: %w", lastErr)
}
Popular Frameworks
Chi Router
Copy
import "github.com/go-chi/chi/v5"
func main() {
r := chi.NewRouter()
// Built-in middleware
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.Timeout(60 * time.Second))
// Routes
r.Get("/", homeHandler)
// Route groups
r.Route("/api/v1", func(r chi.Router) {
r.Use(AuthMiddleware)
r.Route("/users", func(r chi.Router) {
r.Get("/", listUsers)
r.Post("/", createUser)
r.Get("/{id}", getUser)
r.Put("/{id}", updateUser)
r.Delete("/{id}", deleteUser)
})
})
http.ListenAndServe(":8080", r)
}
func getUser(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
// ...
}
Gin Framework
Copy
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default() // Includes logger and recovery
// Middleware
r.Use(CORSMiddleware())
// Routes
api := r.Group("/api/v1")
{
api.Use(AuthMiddleware())
users := api.Group("/users")
{
users.GET("", listUsers)
users.POST("", createUser)
users.GET("/:id", getUser)
users.PUT("/:id", updateUser)
users.DELETE("/:id", deleteUser)
}
}
r.Run(":8080")
}
func getUser(c *gin.Context) {
id := c.Param("id")
user, err := findUser(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
}
c.JSON(http.StatusOK, user)
}
func createUser(c *gin.Context) {
var req CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
user := createUserFromRequest(req)
c.JSON(http.StatusCreated, user)
}
Echo Framework
Copy
import "github.com/labstack/echo/v4"
func main() {
e := echo.New()
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// Routes
api := e.Group("/api/v1")
api.Use(AuthMiddleware)
api.GET("/users", listUsers)
api.POST("/users", createUser)
api.GET("/users/:id", getUser)
e.Logger.Fatal(e.Start(":8080"))
}
func getUser(c echo.Context) error {
id := c.Param("id")
user, err := findUser(id)
if err != nil {
return c.JSON(http.StatusNotFound, map[string]string{"error": "not found"})
}
return c.JSON(http.StatusOK, user)
}
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?
Multiple approaches:
- 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()?
HTTP responses must have their body closed to:
- 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
| Topic | Key Points |
|---|---|
| net/http | Built-in, production-ready, Handler interface |
| Routing | Go 1.22+ patterns, or use Chi/Gin/Echo |
| Middleware | Chain pattern, logging, auth, recovery |
| JSON | Encoder/Decoder, validation, proper error handling |
| Client | Timeouts, connection pooling, retry logic |
| Security | TLS, rate limiting, input validation |