Go Web Development: Frameworks, Middleware, WebSockets & Routing
To framework or not to framework? We evaluate the ecosystem (Gin, Chi, Fiber) against the standard library, diving deep into routing algorithms, middleware chaining, and building scalable real-time apps.
Web Frameworks
Standard library net/http
The net/http package is Go's built-in HTTP server and client implementation, providing everything needed to build production-ready web servers without external dependencies. It's surprisingly powerful and many frameworks are just wrappers around it.
package main import ( "fmt" "net/http" ) func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello, World!") }) http.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"users": ["alice", "bob"]}`)) }) http.ListenAndServe(":8080", nil) }
Gin
Gin is the most popular Go web framework, offering a martini-like API with up to 40x better performance, featuring a radix tree-based router, middleware support, JSON validation, and excellent error management.
package main import "github.com/gin-gonic/gin" func main() { r := gin.Default() // Includes Logger and Recovery middleware r.GET("/ping", func(c *gin.Context) { c.JSON(200, gin.H{"message": "pong"}) }) r.GET("/user/:id", func(c *gin.Context) { id := c.Param("id") c.JSON(200, gin.H{"user_id": id}) }) r.Run(":8080") }
Echo
Echo is a high-performance, minimalist framework with automatic TLS, HTTP/2 support, and a robust middleware ecosystem, known for its elegant API and comprehensive documentation.
package main import ( "net/http" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" ) func main() { e := echo.New() e.Use(middleware.Logger()) e.Use(middleware.Recover()) e.GET("/", func(c echo.Context) error { return c.String(http.StatusOK, "Hello, Echo!") }) e.GET("/users/:id", func(c echo.Context) error { return c.JSON(http.StatusOK, map[string]string{ "id": c.Param("id"), }) }) e.Logger.Fatal(e.Start(":8080")) }
Fiber
Fiber is an Express.js-inspired framework built on top of Fasthttp (not net/http), making it one of the fastest Go frameworks, ideal for developers coming from Node.js background.
package main import "github.com/gofiber/fiber/v2" func main() { app := fiber.New() app.Get("/", func(c *fiber.Ctx) error { return c.SendString("Hello, Fiber!") }) app.Get("/api/:param", func(c *fiber.Ctx) error { return c.JSON(fiber.Map{ "param": c.Params("param"), "query": c.Query("filter"), }) }) app.Listen(":3000") }
Chi
Chi is a lightweight, idiomatic router built on net/http that composes well with the standard library, featuring a powerful middleware stack and zero external dependencies.
package main import ( "net/http" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" ) func main() { r := chi.NewRouter() r.Use(middleware.Logger) r.Use(middleware.Recoverer) r.Get("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hello Chi!")) }) r.Route("/api/users", func(r chi.Router) { r.Get("/", listUsers) r.Post("/", createUser) r.Get("/{userID}", getUser) }) http.ListenAndServe(":8080", r) }
Gorilla Mux
Gorilla Mux is a powerful URL router and dispatcher that extends net/http with advanced pattern matching, though note the Gorilla toolkit is now in maintenance mode (archived Dec 2022).
package main import ( "fmt" "net/http" "github.com/gorilla/mux" ) func main() { r := mux.NewRouter() r.HandleFunc("/", HomeHandler) r.HandleFunc("/products", ProductsHandler).Methods("GET") r.HandleFunc("/products/{key}", ProductHandler).Methods("GET") r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler) // Host-based routing r.Host("api.example.com").Subrouter() http.ListenAndServe(":8080", r) }
Buffalo
Buffalo is a full-stack web framework providing an ecosystem with code generators, hot reload, asset pipeline, and database migrations - the "Rails of Go" for rapid application development.
// actions/app.go package actions import "github.com/gobuffalo/buffalo" func App() *buffalo.App { app := buffalo.New(buffalo.Options{ Env: ENV, }) app.GET("/", HomeHandler) api := app.Group("/api/v1") api.Resource("/users", UsersResource{}) return app } func HomeHandler(c buffalo.Context) error { return c.Render(200, r.HTML("home/index.html")) } // CLI: buffalo new myapp && buffalo dev
Beego
Beego is a full-featured MVC framework with built-in ORM, session management, caching, and auto-generated API documentation, popular in enterprise environments especially in China.
package main import ( "github.com/beego/beego/v2/server/web" ) type MainController struct { web.Controller } func (c *MainController) Get() { c.Data["Website"] = "beego.me" c.Data["Email"] = "astaxie@gmail.com" c.TplName = "index.tpl" } func main() { web.Router("/", &MainController{}) web.Router("/api/user/:id", &UserController{}, "get:GetUser") web.Run() } // CLI: bee new myproject && bee run
┌─────────────────────────────────────────────────────────────────┐ │ GO WEB FRAMEWORKS COMPARISON │ ├─────────────────┬───────────┬────────────┬──────────┬───────────┤ │ Framework │Performance│ Features │ Learning │ Use Case │ ├─────────────────┼───────────┼────────────┼──────────┼───────────┤ │ net/http │ ████████░ │ ███░░░░░░░ │ ████░░░░ │ Simple API│ │ Gin │ █████████ │ ███████░░░ │ ███████░ │ REST APIs │ │ Echo │ █████████ │ ███████░░░ │ ███████░ │ REST APIs │ │ Fiber │ ██████████│ ██████░░░░ │ ████████ │ High perf │ │ Chi │ █████████ │ █████░░░░░ │ ████████ │ Idiomatic │ │ Gorilla Mux │ ███████░░ │ ██████░░░░ │ ███████░ │ Legacy │ │ Buffalo │ ██████░░░ │ ██████████ │ ██████░░ │ Full-stack│ │ Beego │ ██████░░░ │ ██████████ │ █████░░░ │ Enterprise│ └─────────────────┴───────────┴────────────┴──────────┴───────────┘
Routing
URL Routing Patterns
URL routing matches incoming HTTP requests to handler functions based on the URL path and HTTP method, using either exact matching, prefix matching, or pattern matching with wildcards and regular expressions.
// Standard library - prefix matching http.Handle("/api/", apiHandler) http.HandleFunc("/", rootHandler) // Gin - exact and pattern matching r.GET("/users", listUsers) // Exact: /users r.GET("/users/:id", getUser) // Pattern: /users/123 r.GET("/files/*filepath", serveFile) // Wildcard: /files/css/style.css // Chi - with regex constraints r.Get("/articles/{id:[0-9]+}", getArticle) r.Get("/users/{name:[a-z]+}", getUserByName) /* Request Flow: ┌──────────────────┐ ┌─────────────┐ ┌─────────────┐ │ GET /users/123 │ ──► │ Router │ ──► │ Handler │ └──────────────────┘ │ (Pattern │ │ Function │ │ Matching) │ │ │ └─────────────┘ └─────────────┘ */
Path Parameters
Path parameters (also called URL parameters or route parameters) extract dynamic values from URL segments, enabling RESTful resource identification like /users/42 where 42 is the user ID.
// Gin r.GET("/users/:id/posts/:postID", func(c *gin.Context) { userID := c.Param("id") // "42" postID := c.Param("postID") // "5" c.JSON(200, gin.H{"user": userID, "post": postID}) }) // Echo e.GET("/users/:id", func(c echo.Context) error { id := c.Param("id") return c.String(200, "User ID: "+id) }) // Chi r.Get("/users/{userID}/orders/{orderID}", func(w http.ResponseWriter, r *http.Request) { userID := chi.URLParam(r, "userID") orderID := chi.URLParam(r, "orderID") fmt.Fprintf(w, "User: %s, Order: %s", userID, orderID) }) // Fiber app.Get("/users/:id<int>", func(c *fiber.Ctx) error { id, _ := c.ParamsInt("id") // Type-safe! return c.JSON(fiber.Map{"id": id}) })
Query Parameters
Query parameters are key-value pairs appended to URLs after ?, used for filtering, pagination, searching, and optional request modifiers like /users?page=1&limit=10&sort=name.
// Standard library func handler(w http.ResponseWriter, r *http.Request) { values := r.URL.Query() page := values.Get("page") // Single value tags := values["tags"] // Multiple values: ?tags=go&tags=web // With default values limit := values.Get("limit") if limit == "" { limit = "10" } } // Gin - with binding type QueryParams struct { Page int `form:"page" binding:"required,min=1"` Limit int `form:"limit" binding:"max=100"` Sort string `form:"sort"` Search string `form:"q"` } r.GET("/users", func(c *gin.Context) { var params QueryParams if err := c.ShouldBindQuery(¶ms); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } // params.Page, params.Limit are typed and validated }) // Echo e.GET("/search", func(c echo.Context) error { query := c.QueryParam("q") page, _ := strconv.Atoi(c.QueryParam("page")) return c.JSON(200, map[string]interface{}{ "query": query, "page": page, }) })
Route Groups
Route groups organize related routes under a common prefix and share middleware, enabling modular API design like grouping all /api/v1/ routes or applying authentication to /admin/ paths.
// Gin func main() { r := gin.Default() // Public routes public := r.Group("/") { public.GET("/health", healthCheck) public.POST("/login", login) } // API v1 routes - with auth middleware v1 := r.Group("/api/v1") v1.Use(AuthMiddleware()) { v1.GET("/users", getUsers) v1.GET("/users/:id", getUser) // Nested group admin := v1.Group("/admin") admin.Use(AdminMiddleware()) { admin.DELETE("/users/:id", deleteUser) } } r.Run(":8080") } /* Route Tree: ├── / │ ├── GET /health │ └── POST /login └── /api/v1 [AuthMiddleware] ├── GET /users ├── GET /users/:id └── /admin [AdminMiddleware] └── DELETE /users/:id */
Route Naming
Route naming assigns identifiers to routes for programmatic reference, enabling URL generation without hardcoding paths, useful for redirects, link building, and maintaining URL consistency.
// Echo e := echo.New() e.GET("/users/:id", getUser).Name = "get-user" e.GET("/users/:id/posts", getUserPosts).Name = "user-posts" // Generate URL by name func someHandler(c echo.Context) error { url := c.Echo().Reverse("get-user", "42") // Returns "/users/42" return c.Redirect(302, url) } // Gin (custom implementation) type NamedRouter struct { *gin.Engine routes map[string]string } func (r *NamedRouter) NamedGET(name, path string, handler gin.HandlerFunc) { r.routes[name] = path r.GET(path, handler) } func (r *NamedRouter) URL(name string, params ...string) string { path := r.routes[name] for i := 0; i < len(params); i += 2 { path = strings.Replace(path, ":"+params[i], params[i+1], 1) } return path }
Reverse Routing
Reverse routing generates URLs from route names and parameters, decoupling your code from URL structure so changing /user/:id to /users/:id only requires updating the route definition, not every reference.
// Echo example e := echo.New() // Define routes with names e.GET("/users/:id", getUser).Name = "user.show" e.GET("/users/:id/posts/:postID", getUserPost).Name = "user.post" func homeHandler(c echo.Context) error { // Generate URLs dynamically userURL := c.Echo().Reverse("user.show", "42") // Result: "/users/42" postURL := c.Echo().Reverse("user.post", "42", "5") // Result: "/users/42/posts/5" return c.JSON(200, map[string]string{ "user_link": userURL, "post_link": postURL, }) } // Beego - built-in support beego.Router("/user/:id", &UserController{}, "get:Get").Name("user_show") // In template: {{urlfor "user_show" ":id" .User.Id}} // Custom implementation type Route struct { Name string Pattern string Method string } func reverseRoute(routes []Route, name string, params map[string]string) string { for _, r := range routes { if r.Name == name { result := r.Pattern for k, v := range params { result = strings.Replace(result, ":"+k, v, 1) } return result } } return "" }
Trailing Slashes
Trailing slashes handling determines whether /users and /users/ are the same route; most frameworks provide options to redirect, strip, or treat them as distinct routes to avoid duplicate content and 404 errors.
// Gin - RedirectTrailingSlash is enabled by default r := gin.New() r.RedirectTrailingSlash = true // /users/ redirects to /users (or vice versa) r.RedirectFixedPath = true // Case-insensitive + clean path // Echo e := echo.New() e.Pre(middleware.RemoveTrailingSlash()) // Strip trailing slash // OR e.Pre(middleware.AddTrailingSlash()) // Add trailing slash // Chi r := chi.NewRouter() r.Use(middleware.StripSlashes) // Removes trailing slashes // Fiber app := fiber.New(fiber.Config{ StrictRouting: false, // /users and /users/ are same (default) // StrictRouting: true // /users and /users/ are different }) // Custom middleware for standard library func TrailingSlashMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" && strings.HasSuffix(r.URL.Path, "/") { newPath := strings.TrimSuffix(r.URL.Path, "/") http.Redirect(w, r, newPath, http.StatusMovedPermanently) return } next.ServeHTTP(w, r) }) }
Middleware
Middleware Pattern in Go
Middleware wraps HTTP handlers to execute code before/after request processing, following the decorator pattern where each middleware receives a handler and returns a new handler, enabling cross-cutting concerns like logging and auth.
/* Middleware Flow: ┌─────────────────────────────────────────────────────────────────┐ │ Request ──► MW1 ──► MW2 ──► MW3 ──► Handler │ │ │ │ Response ◄── MW1 ◄── MW2 ◄── MW3 ◄── (response) │ └─────────────────────────────────────────────────────────────────┘ */ // Standard library pattern type Middleware func(http.Handler) http.Handler func LoggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() // Before log.Printf("Started %s %s", r.Method, r.URL.Path) // Call next handler next.ServeHTTP(w, r) // After log.Printf("Completed in %v", time.Since(start)) }) } func main() { finalHandler := http.HandlerFunc(myHandler) http.Handle("/", LoggingMiddleware(AuthMiddleware(finalHandler))) }
Middleware Chaining
Middleware chaining composes multiple middleware functions into a single handler, executing them in order, typically implemented using a chain builder or variadic function to avoid deeply nested calls.
// Manual chaining (ugly) handler := Logger(Auth(RateLimit(Recovery(finalHandler)))) // Chain builder pattern type Chain struct { middlewares []func(http.Handler) http.Handler } func NewChain(middlewares ...func(http.Handler) http.Handler) Chain { return Chain{middlewares: middlewares} } func (c Chain) Then(h http.Handler) http.Handler { for i := len(c.middlewares) - 1; i >= 0; i-- { h = c.middlewares[i](h) } return h } // Usage chain := NewChain(Logger, Auth, RateLimit, Recovery) http.Handle("/api/", chain.Then(apiHandler)) // Alice library (popular choice) import "github.com/justinas/alice" chain := alice.New(Logger, Auth, Recovery) http.Handle("/", chain.Then(indexHandler)) // Chi - built-in r := chi.NewRouter() r.Use(Logger) r.Use(Auth) r.Use(Recovery) r.Get("/", handler)
Authentication Middleware
Authentication middleware validates user identity via tokens (JWT/session/API keys) before allowing access to protected routes, typically extracting credentials from headers, validating them, and populating context with user info.
func AuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { // Extract token from header authHeader := c.GetHeader("Authorization") if authHeader == "" { c.AbortWithStatusJSON(401, gin.H{"error": "missing authorization header"}) return } // Parse Bearer token parts := strings.Split(authHeader, " ") if len(parts) != 2 || parts[0] != "Bearer" { c.AbortWithStatusJSON(401, gin.H{"error": "invalid token format"}) return } // Validate JWT token, err := jwt.Parse(parts[1], func(t *jwt.Token) (interface{}, error) { return []byte(secretKey), nil }) if err != nil || !token.Valid { c.AbortWithStatusJSON(401, gin.H{"error": "invalid token"}) return } // Extract claims and set in context claims := token.Claims.(jwt.MapClaims) c.Set("userID", claims["sub"]) c.Set("role", claims["role"]) c.Next() } } // Usage r.GET("/protected", AuthMiddleware(), protectedHandler)
Logging Middleware
Logging middleware records request/response details including method, path, status code, latency, and client IP for debugging, monitoring, and audit trails, often outputting structured logs for aggregation systems.
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, status: 200} next.ServeHTTP(wrapped, r) log.Printf( "%s %s %s %d %v %s", r.RemoteAddr, r.Method, r.URL.Path, wrapped.status, time.Since(start), r.UserAgent(), ) }) } type responseWriter struct { http.ResponseWriter status int } func (rw *responseWriter) WriteHeader(code int) { rw.status = code rw.ResponseWriter.WriteHeader(code) } // Structured logging with zerolog func StructuredLogger(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() wrapped := &responseWriter{ResponseWriter: w, status: 200} next.ServeHTTP(wrapped, r) log.Info(). Str("method", r.Method). Str("path", r.URL.Path). Int("status", wrapped.status). Dur("latency", time.Since(start)). Str("ip", r.RemoteAddr). Msg("request completed") }) }
Recovery Middleware
Recovery middleware catches panics that occur during request handling, preventing server crashes, logging the stack trace for debugging, and returning a 500 Internal Server Error to clients.
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 the panic with stack trace log.Printf("PANIC: %v\n%s", err, debug.Stack()) // Return 500 error w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(map[string]string{ "error": "Internal Server Error", }) } }() next.ServeHTTP(w, r) }) } // Gin's built-in recovery with custom handler r.Use(gin.CustomRecovery(func(c *gin.Context, recovered interface{}) { if err, ok := recovered.(string); ok { c.JSON(500, gin.H{"error": err}) } c.AbortWithStatus(500) })) // Echo e.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{ StackSize: 4 << 10, // 4 KB LogErrorFunc: func(c echo.Context, err error, stack []byte) error { return fmt.Errorf("[PANIC RECOVER] %v %s", err, stack) }, }))
CORS Middleware
CORS (Cross-Origin Resource Sharing) middleware handles preflight OPTIONS requests and sets appropriate headers to allow web browsers to make cross-origin requests to your API from different domains.
func CORSMiddleware() gin.HandlerFunc { return func(c *gin.Context) { c.Header("Access-Control-Allow-Origin", "*") c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Authorization") c.Header("Access-Control-Max-Age", "86400") c.Header("Access-Control-Allow-Credentials", "true") // Handle preflight request if c.Request.Method == "OPTIONS" { c.AbortWithStatus(204) return } c.Next() } } // Using rs/cors library import "github.com/rs/cors" c := cors.New(cors.Options{ AllowedOrigins: []string{"https://example.com", "https://app.example.com"}, AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"}, AllowedHeaders: []string{"Authorization", "Content-Type"}, ExposedHeaders: []string{"X-Total-Count"}, AllowCredentials: true, MaxAge: 86400, }) handler := c.Handler(mux) // Echo e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ AllowOrigins: []string{"https://example.com"}, AllowMethods: []string{echo.GET, echo.POST, echo.PUT, echo.DELETE}, }))
Rate Limiting Middleware
Rate limiting middleware restricts the number of requests a client can make within a time window, protecting your API from abuse, DDoS attacks, and ensuring fair usage using algorithms like token bucket or sliding window.
import "golang.org/x/time/rate" func RateLimitMiddleware(rps float64, burst int) func(http.Handler) http.Handler { // Per-IP limiters limiters := make(map[string]*rate.Limiter) mu := sync.Mutex{} getLimiter := func(ip string) *rate.Limiter { mu.Lock() defer mu.Unlock() if limiter, exists := limiters[ip]; exists { return limiter } limiter := rate.NewLimiter(rate.Limit(rps), burst) limiters[ip] = limiter return limiter } return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ip := r.RemoteAddr limiter := getLimiter(ip) if !limiter.Allow() { w.Header().Set("Retry-After", "1") http.Error(w, "Too Many Requests", http.StatusTooManyRequests) return } next.ServeHTTP(w, r) }) } } // Usage: 10 requests per second, burst of 20 r.Use(RateLimitMiddleware(10, 20)) /* Token Bucket Algorithm: ┌─────────────────────────────────────────┐ │ Bucket Capacity: 20 tokens │ │ Refill Rate: 10 tokens/second │ │ │ │ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ (tokens) │ │ ↓ │ │ Request consumes 1 token │ │ ↓ │ │ Empty bucket = 429 Too Many Requests │ └─────────────────────────────────────────┘ */
Request ID Middleware
Request ID middleware generates or extracts a unique identifier for each request, propagating it through the system for distributed tracing, log correlation, and debugging across microservices.
func RequestIDMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Check for existing request ID (from upstream service) requestID := r.Header.Get("X-Request-ID") if requestID == "" { requestID = uuid.New().String() } // Add to response header w.Header().Set("X-Request-ID", requestID) // Add to request context ctx := context.WithValue(r.Context(), "requestID", requestID) next.ServeHTTP(w, r.WithContext(ctx)) }) } // Usage in handlers func handler(w http.ResponseWriter, r *http.Request) { requestID := r.Context().Value("requestID").(string) log.Printf("[%s] Processing request", requestID) } // Echo e.Use(middleware.RequestID()) // Chi r.Use(middleware.RequestID) /* Distributed Tracing Flow: ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ Client │────►│ API GW │────►│ Service │────►│ DB │ │ │ │ │ │ A │ │ │ │ │ │X-Req-ID │ │X-Req-ID │ │X-Req-ID │ │ │ │abc-123 │ │abc-123 │ │abc-123 │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ All logs tagged with abc-123 */
Timeout Middleware
Timeout middleware enforces maximum request duration, canceling long-running requests to prevent resource exhaustion, returning 503 Service Unavailable when handlers exceed the time limit.
func TimeoutMiddleware(timeout time.Duration) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), timeout) defer cancel() r = r.WithContext(ctx) done := make(chan struct{}) go func() { next.ServeHTTP(w, r) close(done) }() select { case <-done: return case <-ctx.Done(): w.WriteHeader(http.StatusServiceUnavailable) w.Write([]byte("Request timeout")) } }) } } // Using http.TimeoutHandler (built-in) handler := http.TimeoutHandler(myHandler, 5*time.Second, "Request timeout") // Gin r.Use(timeout.New( timeout.WithTimeout(5*time.Second), timeout.WithHandler(func(c *gin.Context) { c.Next() }), timeout.WithResponse(func(c *gin.Context) { c.JSON(503, gin.H{"error": "timeout"}) }), )) // Echo e.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{ Timeout: 5 * time.Second, }))
Request/Response Handling
Request Parsing
Request parsing extracts data from incoming HTTP requests including URL path, query string, headers, body, and form data, with Go providing methods on *http.Request to access each component.
func handler(w http.ResponseWriter, r *http.Request) { // Method and URL method := r.Method // "GET", "POST", etc. path := r.URL.Path // "/api/users" rawQuery := r.URL.RawQuery // "page=1&limit=10" // Headers contentType := r.Header.Get("Content-Type") authHeader := r.Header.Get("Authorization") // Query parameters page := r.URL.Query().Get("page") // Body (must read before accessing form) body, err := io.ReadAll(r.Body) defer r.Body.Close() // Form data (application/x-www-form-urlencoded) r.ParseForm() username := r.FormValue("username") // Context values (set by middleware) userID := r.Context().Value("userID") // Remote address clientIP := r.RemoteAddr } /* HTTP Request Structure: ┌────────────────────────────────────────────────┐ │ POST /api/users?role=admin HTTP/1.1 │ ← Request Line ├────────────────────────────────────────────────┤ │ Host: example.com │ │ Content-Type: application/json │ ← Headers │ Authorization: Bearer token123 │ ├────────────────────────────────────────────────┤ │ │ │ {"name": "John", "email": "john@example.com"} │ ← Body │ │ └────────────────────────────────────────────────┘ */
JSON Request/Response
JSON is the most common format for REST APIs in Go, with encoding/json package providing Marshal/Unmarshal for conversion between Go structs and JSON, while frameworks add convenient binding methods.
// Request struct with validation tags type CreateUserRequest struct { Name string `json:"name" binding:"required,min=2"` Email string `json:"email" binding:"required,email"` Age int `json:"age" binding:"gte=0,lte=150"` Role string `json:"role,omitempty"` } // Response struct type UserResponse struct { ID int `json:"id"` Name string `json:"name"` Email string `json:"email"` CreatedAt time.Time `json:"created_at"` } // Standard library func createUser(w http.ResponseWriter, r *http.Request) { var req CreateUserRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } // Process... response := UserResponse{ID: 1, Name: req.Name} w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(response) } // Gin (with binding/validation) func createUserGin(c *gin.Context) { var req CreateUserRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } c.JSON(201, UserResponse{ID: 1, Name: req.Name}) }
XML Request/Response
XML handling in Go uses encoding/xml package with struct tags defining element/attribute names, useful for SOAP APIs, RSS feeds, or legacy system integration.
type User struct { XMLName xml.Name `xml:"user"` ID int `xml:"id,attr"` Name string `xml:"name"` Email string `xml:"email"` Address Address `xml:"address"` } type Address struct { Street string `xml:"street"` City string `xml:"city"` } func handler(w http.ResponseWriter, r *http.Request) { // Parse XML request var user User if err := xml.NewDecoder(r.Body).Decode(&user); err != nil { http.Error(w, err.Error(), 400) return } // XML response response := User{ID: 1, Name: "John", Email: "john@example.com"} w.Header().Set("Content-Type", "application/xml") w.WriteHeader(200) xml.NewEncoder(w).Encode(response) } // Output: // <user id="1"> // <name>John</name> // <email>john@example.com</email> // <address> // <street></street> // <city></city> // </address> // </user> // Gin c.XML(200, gin.H{"message": "hey", "status": 200})
Form Data
Form data (application/x-www-form-urlencoded) is the traditional way HTML forms submit data, parsed in Go using ParseForm() with values accessed via FormValue() or PostFormValue() methods.
func formHandler(w http.ResponseWriter, r *http.Request) { // Parse form data if err := r.ParseForm(); err != nil { http.Error(w, "Error parsing form", 400) return } // Get form values username := r.FormValue("username") // From URL query OR body password := r.PostFormValue("password") // Only from body // Multiple values (checkboxes) interests := r.Form["interests"] // []string{"sports", "music"} fmt.Fprintf(w, "Username: %s", username) } // Gin with struct binding type LoginForm struct { Username string `form:"username" binding:"required"` Password string `form:"password" binding:"required,min=8"` Remember bool `form:"remember"` } func loginHandler(c *gin.Context) { var form LoginForm if err := c.ShouldBind(&form); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } // form.Username, form.Password are populated } // Echo type User struct { Name string `form:"name"` Email string `form:"email"` } e.POST("/users", func(c echo.Context) error { u := new(User) if err := c.Bind(u); err != nil { return err } return c.JSON(200, u) })
Multipart Form Data
Multipart form data (multipart/form-data) handles forms with file uploads, encoding fields and files as separate parts with boundaries, requiring special parsing with size limits to prevent memory exhaustion.
func uploadHandler(w http.ResponseWriter, r *http.Request) { // Limit upload size (32 MB) r.Body = http.MaxBytesReader(w, r.Body, 32<<20) // Parse multipart form if err := r.ParseMultipartForm(32 << 20); err != nil { http.Error(w, "File too large", 400) return } // Get text fields title := r.FormValue("title") // Get uploaded file file, header, err := r.FormFile("document") if err != nil { http.Error(w, "Error retrieving file", 400) return } defer file.Close() // File info filename := header.Filename size := header.Size contentType := header.Header.Get("Content-Type") // Save file dst, err := os.Create(filepath.Join("./uploads", filename)) if err != nil { http.Error(w, "Error saving file", 500) return } defer dst.Close() io.Copy(dst, file) fmt.Fprintf(w, "Uploaded: %s (%d bytes)", filename, size) } /* Multipart Form Structure: ------boundary123 Content-Disposition: form-data; name="title" My Document ------boundary123 Content-Disposition: form-data; name="document"; filename="file.pdf" Content-Type: application/pdf [binary content] ------boundary123-- */
File Uploads
File uploads require handling multipart forms, validating file types and sizes, storing files safely with sanitized names, and optionally streaming to cloud storage for scalability.
// Gin example with multiple files func uploadHandler(c *gin.Context) { form, err := c.MultipartForm() if err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } files := form.File["files"] // Multiple files var uploaded []string for _, file := range files { // Validate file type if !isAllowedType(file.Header.Get("Content-Type")) { continue } // Generate safe filename ext := filepath.Ext(file.Filename) newName := fmt.Sprintf("%s%s", uuid.New().String(), ext) // Save file dst := filepath.Join("./uploads", newName) if err := c.SaveUploadedFile(file, dst); err != nil { c.JSON(500, gin.H{"error": "failed to save file"}) return } uploaded = append(uploaded, newName) } c.JSON(200, gin.H{"uploaded": uploaded}) } func isAllowedType(contentType string) bool { allowed := map[string]bool{ "image/jpeg": true, "image/png": true, "image/gif": true, "application/pdf": true, } return allowed[contentType] } // Stream upload to S3 func streamToS3(file multipart.File, key string) error { uploader := s3manager.NewUploader(sess) _, err := uploader.Upload(&s3manager.UploadInput{ Bucket: aws.String("my-bucket"), Key: aws.String(key), Body: file, }) return err }
Streaming Responses
Streaming responses send data incrementally without buffering the entire response in memory, essential for large files, real-time data feeds, Server-Sent Events (SSE), and chunked transfer encoding.
// Chunked streaming func streamHandler(w http.ResponseWriter, r *http.Request) { flusher, ok := w.(http.Flusher) if !ok { http.Error(w, "Streaming unsupported", 500) return } w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") for i := 0; i < 10; i++ { fmt.Fprintf(w, "data: Message %d\n\n", i) flusher.Flush() time.Sleep(time.Second) } } // Server-Sent Events (SSE) func sseHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/event-stream") events := make(chan string) go generateEvents(events) for { select { case event := <-events: fmt.Fprintf(w, "event: update\ndata: %s\n\n", event) w.(http.Flusher).Flush() case <-r.Context().Done(): return } } } // Stream large file func streamFile(w http.ResponseWriter, r *http.Request) { file, err := os.Open("large-file.zip") if err != nil { http.Error(w, "File not found", 404) return } defer file.Close() w.Header().Set("Content-Disposition", "attachment; filename=large-file.zip") io.Copy(w, file) // Streams without loading entire file }
Content Negotiation
Content negotiation selects the response format based on client's Accept header, allowing the same endpoint to return JSON, XML, HTML, or other formats depending on what the client requests.
func handler(w http.ResponseWriter, r *http.Request) { data := User{ID: 1, Name: "John"} accept := r.Header.Get("Accept") switch { case strings.Contains(accept, "application/xml"): w.Header().Set("Content-Type", "application/xml") xml.NewEncoder(w).Encode(data) case strings.Contains(accept, "text/html"): w.Header().Set("Content-Type", "text/html") tmpl.Execute(w, data) default: // Default to JSON w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(data) } } // Gin automatic negotiation func handler(c *gin.Context) { data := gin.H{"id": 1, "name": "John"} c.Negotiate(200, gin.Negotiate{ Offered: []string{gin.MIMEJSON, gin.MIMEXML, gin.MIMEHTML}, Data: data, HTMLName: "user.html", }) } /* Content Negotiation Flow: ┌──────────────────────────────────────────────────┐ │ Client: Accept: application/json, text/xml;q=0.9│ │ │ │ Server: Checks preferences, returns JSON │ │ Content-Type: application/json │ └──────────────────────────────────────────────────┘ */
Custom Binders/Validators
Custom binders extend the default request parsing to handle special formats or sources, while custom validators add domain-specific validation rules beyond basic type checking and built-in constraints.
// Custom validator with go-playground/validator import "github.com/go-playground/validator/v10" var validate *validator.Validate func init() { validate = validator.New() // Register custom validation validate.RegisterValidation("username", validateUsername) validate.RegisterValidation("strongpass", validateStrongPassword) } func validateUsername(fl validator.FieldLevel) bool { username := fl.Field().String() // Only alphanumeric, 3-20 chars matched, _ := regexp.MatchString(`^[a-zA-Z0-9]{3,20}$`, username) return matched } func validateStrongPassword(fl validator.FieldLevel) bool { pass := fl.Field().String() hasUpper := regexp.MustCompile(`[A-Z]`).MatchString(pass) hasLower := regexp.MustCompile(`[a-z]`).MatchString(pass) hasNumber := regexp.MustCompile(`[0-9]`).MatchString(pass) return len(pass) >= 8 && hasUpper && hasLower && hasNumber } type RegisterRequest struct { Username string `json:"username" validate:"required,username"` Password string `json:"password" validate:"required,strongpass"` Email string `json:"email" validate:"required,email"` } // Custom binder (Gin) type QueryArrayBinder struct{} func (QueryArrayBinder) Name() string { return "query-array" } func (QueryArrayBinder) Bind(req *http.Request, obj interface{}) error { values := req.URL.Query() // Custom parsing logic for array query params // ?ids=1,2,3 -> []int{1, 2, 3} return nil }
Template Engines
html/template
The html/template package is Go's built-in template engine with automatic HTML escaping to prevent XSS attacks, supporting variables, conditionals, loops, functions, and template composition.
package main import ( "html/template" "os" ) const tmpl = ` <!DOCTYPE html> <html> <head><title>{{.Title}}</title></head> <body> <h1>{{.Title}}</h1> {{if .User}} <p>Welcome, {{.User.Name}}!</p> {{else}} <p>Please log in</p> {{end}} <ul> {{range .Items}} <li>{{.}}</li> {{end}} </ul> <!-- Auto-escaped for safety --> <p>{{.UnsafeHTML}}</p> <!-- Raw HTML (use carefully) --> {{.SafeHTML}} </body> </html> ` type PageData struct { Title string User *User Items []string UnsafeHTML string SafeHTML template.HTML } func main() { t := template.Must(template.New("page").Parse(tmpl)) data := PageData{ Title: "My Page", User: &User{Name: "John"}, Items: []string{"Go", "is", "awesome"}, UnsafeHTML: "<script>alert('xss')</script>", // Escaped SafeHTML: template.HTML("<strong>Bold</strong>"), // Raw } t.Execute(os.Stdout, data) }
Pongo2
Pongo2 is a Django-syntax template engine for Go, offering familiar syntax for Python developers with built-in filters, tags, template inheritance, and macro support.
import "github.com/flosch/pongo2/v6" // template.html /* {% extends "base.html" %} {% block content %} <h1>{{ title }}</h1> <ul> {% for item in items %} <li>{{ item.name|title }} - ${{ item.price|floatformat:2 }}</li> {% endfor %} </ul> {% if user.is_admin %} <a href="/admin">Admin Panel</a> {% endif %} {% endblock %} */ func handler(w http.ResponseWriter, r *http.Request) { tpl := pongo2.Must(pongo2.FromFile("template.html")) err := tpl.ExecuteWriter(pongo2.Context{ "title": "Product List", "items": []map[string]interface{}{ {"name": "laptop", "price": 999.99}, {"name": "mouse", "price": 29.99}, }, "user": map[string]bool{"is_admin": true}, }, w) if err != nil { http.Error(w, err.Error(), 500) } }
Jet
Jet is a fast, flexible template engine with simple syntax, template inheritance, custom functions, and automatic HTML escaping, designed as a drop-in replacement for html/template with more features.
import "github.com/CloudyKit/jet/v6" var views = jet.NewSet( jet.NewOSFileSystemLoader("./views"), jet.InDevelopmentMode(), // Hot reload ) // views/layout.jet /* <!DOCTYPE html> <html> <head><title>{{ .Title }}</title></head> <body> {{ yield content }} </body> </html> */ // views/home.jet /* {{ extends "layout.jet" }} {{ block content }} <h1>Welcome {{ .User.Name }}</h1> {{ range .Posts }} <article>{{ .Title }}</article> {{ end }} {{ end }} */ func handler(w http.ResponseWriter, r *http.Request) { t, err := views.GetTemplate("home.jet") if err != nil { http.Error(w, err.Error(), 500) return } vars := make(jet.VarMap) vars.Set("User", User{Name: "John"}) vars.Set("Posts", posts) t.Execute(w, vars, nil) }
Amber
Amber is a Go port of Pug/Jade template engine, using indentation-based syntax for cleaner HTML templates without closing tags, compiling to native Go templates for performance.
import "github.com/eknkc/amber" // template.amber /* doctype html html head title= Title body h1= Title if User p Welcome, #{User.Name}! else p Please log in ul each item in Items li= item form(action="/submit" method="post") input(type="text" name="email" placeholder="Email") button(type="submit") Submit */ func main() { compiler := amber.New() err := compiler.ParseFile("template.amber") if err != nil { panic(err) } tpl, err := compiler.Compile() if err != nil { panic(err) } data := map[string]interface{}{ "Title": "My Page", "User": User{Name: "John"}, "Items": []string{"One", "Two", "Three"}, } tpl.Execute(os.Stdout, data) }
Template Inheritance
Template inheritance allows defining a base layout with blocks that child templates can override, enabling consistent layouts across pages while customizing specific sections like headers, content, and footers.
// templates/base.html const baseTemplate = ` {{define "base"}} <!DOCTYPE html> <html> <head> <title>{{block "title" .}}Default Title{{end}}</title> {{block "head" .}}{{end}} </head> <body> <nav>{{template "nav" .}}</nav> <main>{{block "content" .}}{{end}}</main> <footer>{{block "footer" .}}© 2024{{end}}</footer> </body> </html> {{end}} {{define "nav"}} <ul><li><a href="/">Home</a></li></ul> {{end}} ` // templates/home.html const homeTemplate = ` {{define "title"}}Home - My Site{{end}} {{define "content"}} <h1>Welcome Home</h1> <p>{{.Message}}</p> {{end}} ` func main() { templates := template.Must(template.New("").Parse(baseTemplate)) template.Must(templates.Parse(homeTemplate)) templates.ExecuteTemplate(os.Stdout, "base", map[string]string{ "Message": "Hello World", }) } /* Template Hierarchy: ┌─────────────────────────────────────┐ │ base.html │ │ ┌─────────────────────────────┐ │ │ │ {{block "title"}} │ │ │ │ {{block "content"}} │ │ │ │ {{block "footer"}} │ │ │ └─────────────────────────────┘ │ │ ↑ ↑ ↑ │ │ │ │ │ │ │ home.html about.html │ │ (overrides content block) │ └─────────────────────────────────────┘ */
Template Caching
Template caching parses templates once at startup and reuses them for each request, avoiding the overhead of parsing on every request, critical for production performance.
// Global template cache var templates *template.Template func init() { // Parse all templates at startup templates = template.Must(template.New("").Funcs(funcMap).ParseGlob("templates/*.html")) } func handler(w http.ResponseWriter, r *http.Request) { // Use cached template templates.ExecuteTemplate(w, "home.html", data) } // More sophisticated caching type TemplateCache struct { templates map[string]*template.Template mu sync.RWMutex devMode bool } func (tc *TemplateCache) Get(name string) (*template.Template, error) { if tc.devMode { // Reload in development return template.ParseFiles("templates/"+name, "templates/base.html") } tc.mu.RLock() tmpl, ok := tc.templates[name] tc.mu.RUnlock() if ok { return tmpl, nil } // Parse and cache tc.mu.Lock() defer tc.mu.Unlock() tmpl, err := template.ParseFiles("templates/"+name, "templates/base.html") if err != nil { return nil, err } tc.templates[name] = tmpl return tmpl, nil }
Hot Reload in Development
Hot reload automatically recompiles templates when files change during development, improving developer experience without requiring server restarts, typically disabled in production for performance.
import "github.com/fsnotify/fsnotify" type DevTemplateEngine struct { templates *template.Template dir string mu sync.RWMutex } func NewDevTemplateEngine(dir string) *DevTemplateEngine { engine := &DevTemplateEngine{dir: dir} engine.reload() go engine.watch() return engine } func (e *DevTemplateEngine) reload() { e.mu.Lock() defer e.mu.Unlock() e.templates = template.Must(template.ParseGlob(e.dir + "/*.html")) log.Println("Templates reloaded") } func (e *DevTemplateEngine) watch() { watcher, err := fsnotify.NewWatcher() if err != nil { log.Fatal(err) } watcher.Add(e.dir) for { select { case event := <-watcher.Events: if event.Op&fsnotify.Write == fsnotify.Write { e.reload() } case err := <-watcher.Errors: log.Println("Watcher error:", err) } } } // Gin with gin-contrib/multitemplate r := gin.Default() r.HTMLRender = createRenderer() func createRenderer() multitemplate.Renderer { r := multitemplate.NewRenderer() r.AddFromFiles("home", "templates/base.html", "templates/home.html") return r }
WebSockets
gorilla/websocket
Gorilla WebSocket is the most popular WebSocket library for Go, providing a complete and well-tested implementation of the WebSocket protocol with support for text and binary messages, ping/pong, and close handshakes.
import "github.com/gorilla/websocket" var upgrader = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, CheckOrigin: func(r *http.Request) bool { return true // Allow all origins (configure properly in production) }, } func wsHandler(w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) if err != nil { log.Println("Upgrade error:", err) return } defer conn.Close() for { messageType, message, err := conn.ReadMessage() if err != nil { log.Println("Read error:", err) break } log.Printf("Received: %s", message) // Echo back if err := conn.WriteMessage(messageType, message); err != nil { log.Println("Write error:", err) break } } } func main() { http.HandleFunc("/ws", wsHandler) http.ListenAndServe(":8080", nil) }
WebSocket Upgrading
WebSocket upgrading converts an HTTP connection to a WebSocket connection through a handshake, where the client sends an Upgrade request and the server responds with 101 Switching Protocols.
/* WebSocket Handshake: Client Request: ┌────────────────────────────────────────────┐ │ GET /ws HTTP/1.1 │ │ Host: localhost:8080 │ │ Upgrade: websocket │ │ Connection: Upgrade │ │ Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==│ │ Sec-WebSocket-Version: 13 │ └────────────────────────────────────────────┘ Server Response: ┌────────────────────────────────────────────┐ │ HTTP/1.1 101 Switching Protocols │ │ Upgrade: websocket │ │ Connection: Upgrade │ │ Sec-WebSocket-Accept: s3pPLMBiTxaQ9k... │ └────────────────────────────────────────────┘ */ var upgrader = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, // Subprotocol selection Subprotocols: []string{"graphql-ws", "json"}, // Compression EnableCompression: true, // Origin check CheckOrigin: func(r *http.Request) bool { origin := r.Header.Get("Origin") return origin == "https://myapp.com" }, // Custom error handler Error: func(w http.ResponseWriter, r *http.Request, status int, reason error) { log.Printf("Upgrade failed: %v", reason) http.Error(w, reason.Error(), status) }, } func handler(w http.ResponseWriter, r *http.Request) { // Check for auth before upgrading token := r.URL.Query().Get("token") if !validateToken(token) { http.Error(w, "Unauthorized", 401) return } // Upgrade with response headers respHeader := http.Header{} respHeader.Add("X-Custom-Header", "value") conn, err := upgrader.Upgrade(w, r, respHeader) if err != nil { return } defer conn.Close() }
WebSocket Read/Write
WebSocket messages are read and written through the connection using message types (text or binary), with proper error handling to detect disconnections and clean up resources.
func handleConnection(conn *websocket.Conn) { // Set read deadline conn.SetReadDeadline(time.Now().Add(60 * time.Second)) // Set max message size conn.SetReadLimit(512 * 1024) // 512 KB for { // Read message messageType, message, err := conn.ReadMessage() if err != nil { if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { log.Printf("Unexpected close: %v", err) } break } // Process based on message type switch messageType { case websocket.TextMessage: var data map[string]interface{} json.Unmarshal(message, &data) handleTextMessage(conn, data) case websocket.BinaryMessage: handleBinaryMessage(conn, message) } } } // Write operations func sendMessage(conn *websocket.Conn, data interface{}) error { conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) return conn.WriteJSON(data) // Convenience method for JSON } // For concurrent writes, use a write pump func writePump(conn *websocket.Conn, send <-chan []byte) { for message := range send { conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) if err := conn.WriteMessage(websocket.TextMessage, message); err != nil { return } } }
Broadcast Patterns
Broadcasting sends messages to multiple connected clients, typically implemented with a hub/room pattern that manages client connections and distributes messages efficiently.
type Hub struct { clients map[*Client]bool broadcast chan []byte register chan *Client unregister chan *Client mu sync.RWMutex } type Client struct { hub *Hub conn *websocket.Conn send chan []byte } func NewHub() *Hub { return &Hub{ clients: make(map[*Client]bool), broadcast: make(chan []byte, 256), register: make(chan *Client), unregister: make(chan *Client), } } func (h *Hub) Run() { for { select { case client := <-h.register: h.mu.Lock() h.clients[client] = true h.mu.Unlock() case client := <-h.unregister: h.mu.Lock() if _, ok := h.clients[client]; ok { delete(h.clients, client) close(client.send) } h.mu.Unlock() case message := <-h.broadcast: h.mu.RLock() for client := range h.clients { select { case client.send <- message: default: close(client.send) delete(h.clients, client) } } h.mu.RUnlock() } } } /* Broadcast Pattern: ┌─────────┐ │ Hub │ └────┬────┘ │ broadcast ┌─────────┼─────────┐ ▼ ▼ ▼ ┌───────┐ ┌───────┐ ┌───────┐ │Client1│ │Client2│ │Client3│ └───────┘ └───────┘ └───────┘ */
Connection Management
Connection management handles client lifecycle including registration, heartbeat monitoring, graceful disconnection, and reconnection, ensuring resources are properly cleaned up and clients can recover from network issues.
type ConnectionManager struct { connections map[string]*websocket.Conn mu sync.RWMutex } func (cm *ConnectionManager) Add(id string, conn *websocket.Conn) { cm.mu.Lock() defer cm.mu.Unlock() cm.connections[id] = conn } func (cm *ConnectionManager) Remove(id string) { cm.mu.Lock() defer cm.mu.Unlock() if conn, ok := cm.connections[id]; ok { conn.Close() delete(cm.connections, id) } } func (cm *ConnectionManager) Get(id string) *websocket.Conn { cm.mu.RLock() defer cm.mu.RUnlock() return cm.connections[id] } func (cm *ConnectionManager) SendTo(id string, msg []byte) error { conn := cm.Get(id) if conn == nil { return errors.New("client not connected") } return conn.WriteMessage(websocket.TextMessage, msg) } // Heartbeat monitoring func monitorConnection(conn *websocket.Conn, done chan struct{}) { ticker := time.NewTicker(30 * time.Second) defer ticker.Stop() for { select { case <-ticker.C: if err := conn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(10*time.Second)); err != nil { return } case <-done: return } } }
Ping/Pong Frames
Ping/pong frames are WebSocket control messages used for keepalive and connection health checks; the server sends ping and expects pong response, detecting dead connections and cleaning up stale resources.
func handleConnection(conn *websocket.Conn) { // Pong handler - reset read deadline when pong received conn.SetPongHandler(func(appData string) error { log.Println("Pong received") conn.SetReadDeadline(time.Now().Add(60 * time.Second)) return nil }) // Ping handler - respond to client pings conn.SetPingHandler(func(appData string) error { log.Println("Ping received, sending pong") return conn.WriteControl(websocket.PongMessage, []byte(appData), time.Now().Add(10*time.Second)) }) // Start ping ticker in separate goroutine go func() { ticker := time.NewTicker(30 * time.Second) defer ticker.Stop() for range ticker.C { if err := conn.WriteControl(websocket.PingMessage, []byte("ping"), time.Now().Add(10*time.Second)); err != nil { log.Println("Ping failed:", err) return } } }() // Read loop for { conn.SetReadDeadline(time.Now().Add(60 * time.Second)) _, message, err := conn.ReadMessage() if err != nil { break } // Process message } } /* Ping/Pong Timeline: Client Server │ │ │ ◄── Ping ─── │ │ ─── Pong ──► │ │ │ │ [60 second wait] │ │ │ │ ◄── Ping ─── │ │ ─── Pong ──► │ │ │ */
Close Handshake
The WebSocket close handshake ensures both endpoints gracefully terminate the connection, sending close frames with status codes and optional reasons, allowing both sides to clean up resources properly.
func closeConnection(conn *websocket.Conn) { // Send close frame with code and reason closeMsg := websocket.FormatCloseMessage( websocket.CloseNormalClosure, "goodbye", ) conn.WriteControl(websocket.CloseMessage, closeMsg, time.Now().Add(time.Second)) // Give time for close frame to be sent time.Sleep(time.Second) conn.Close() } // Handle incoming close func handleConnection(conn *websocket.Conn) { conn.SetCloseHandler(func(code int, text string) error { log.Printf("Close received: %d - %s", code, text) // Send close response message := websocket.FormatCloseMessage(code, "") conn.WriteControl(websocket.CloseMessage, message, time.Now().Add(time.Second)) return nil }) for { _, _, err := conn.ReadMessage() if err != nil { if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) { log.Println("Normal close") } else { log.Printf("Error: %v", err) } break } } } /* Close Status Codes: ┌───────┬──────────────────────────────┐ │ Code │ Meaning │ ├───────┼──────────────────────────────┤ │ 1000 │ Normal closure │ │ 1001 │ Going away │ │ 1002 │ Protocol error │ │ 1003 │ Unsupported data │ │ 1006 │ Abnormal closure (no frame) │ │ 1008 │ Policy violation │ │ 1011 │ Internal server error │ └───────┴──────────────────────────────┘ */