
Golang Http Frameworks
- 321 installs
- 63 repo stars
- Updated July 18, 2026
- bobmatnyc/claude-mpm-skills
golang-http-frameworks is a Claude MPM skill that guides choosing and implementing Go HTTP servers with idiomatic routing, middleware, and error handling across Gin, Echo, Chi, Fiber, or stdlib net/http.
About
golang-http-frameworks is a Claude MPM web skill from a 7-skill Golang toolchain that compares and implements Go HTTP server patterns across stdlib net/http (Go 1.22+ routing), Chi, Gin, Echo, and Fiber. The skill covers idiomatic routing, middleware chains, error handling, and production-ready patterns for API backends. It is one of 171 production-ready skills in the Claude MPM collection with progressive disclosure and toolchain detection. Developers reach for golang-http-frameworks when starting a new Go API service and deciding between lightweight stdlib routing and framework-specific middleware ecosystems, or when refactoring an existing server toward idiomatic production patterns.
- Compare Go HTTP framework tradeoffs
- Middleware and routing patterns
- Idiomatic error and context handling
- Production-ready server structure
- Framework-specific best practices
Golang Http Frameworks by the numbers
- 321 all-time installs (skills.sh)
- Ranked #30 of 98 Go skills by installs in the Skillselion catalog
- Data as of Aug 1, 2026 (Skillselion catalog sync)
npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill golang-http-frameworksAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 321 |
|---|---|
| repo stars | ★ 63 |
| Last updated | July 18, 2026 |
| Repository | bobmatnyc/claude-mpm-skills ↗ |
How do you choose a Go HTTP framework for production?
Choosing and implementing Go HTTP servers with idiomatic routing, middleware, error handling, and production patterns across Gin, Echo, Chi, Fiber, or stdlib net/http.
Who is it for?
Go backend developers starting or refactoring an HTTP API who need framework comparison and idiomatic routing, middleware, and error-handling patterns.
Skip if: Teams needing Go database patterns, OpenTelemetry observability, or CLI tooling—other Golang skills in the same repository cover those domains.
When should I use this skill?
User asks to choose a Go HTTP framework, set up Gin or Chi routing, implement Go middleware, or build a production net/http server.
What you get
Go HTTP server with routing, middleware chain, error handlers, and framework-specific production patterns.
- HTTP server with routing
- Middleware chain
- Error handling patterns
By the numbers
- Covers 5 Go HTTP stacks: net/http, Chi, Gin, Echo, and Fiber
- Part of a 7-skill Golang toolchain in a 171-skill collection
Files
Go HTTP Frameworks & REST APIs
Overview
Go provides exceptional HTTP capabilities starting with the standard library's net/http package. Go 1.22+ introduced enhanced pattern routing in ServeMux, making stdlib viable for many applications. For more complex needs, frameworks like Chi, Gin, Echo, and Fiber offer additional features while maintaining Go's simplicity and performance.
Key Features:
- 🌐 net/http: Production-ready standard library with Go 1.22+ routing
- 🎯 Chi: Lightweight, stdlib-compatible router with middleware chains
- ⚡ Gin: High-performance framework with binding and validation
- 🛡️ Echo: Type-safe, enterprise framework with OpenAPI support
- 🚀 Fiber: Express.js-inspired framework with WebSocket support
- 🔧 Middleware: Composable request/response processing
- ✅ Validation: Struct tag-based request validation
- 🧪 Testing: httptest.Server for comprehensive integration tests
When to Use This Skill
Activate this skill when:
- Building RESTful APIs or web services
- Choosing appropriate HTTP framework for project requirements
- Implementing authentication or authorization middleware
- Designing REST endpoint patterns and validation
- Testing HTTP handlers and middleware chains
- Optimizing API performance and response times
- Migrating between HTTP frameworks
Framework Selection Guide
net/http (Standard Library) - Go 1.22+
Use When:
- Building simple to moderate complexity APIs
- Avoiding external dependencies is priority
- Need maximum compatibility and long-term stability
- Team prefers explicit over implicit patterns
Strengths:
- Zero dependencies, part of Go standard library
- Go 1.22+ pattern routing with path parameters
- Excellent performance and stability
- Extensive ecosystem compatibility
- No framework lock-in
Limitations:
- More verbose middleware composition
- Manual request validation
- No built-in binding or rendering
Example:
package main
import (
"encoding/json"
"net/http"
"log"
)
// Go 1.22+ pattern routing
func main() {
mux := http.NewServeMux()
// Path parameters with {param} syntax
mux.HandleFunc("GET /users/{id}", getUserHandler)
mux.HandleFunc("POST /users", createUserHandler)
mux.HandleFunc("GET /users", listUsersHandler)
// Middleware wrapping
handler := loggingMiddleware(mux)
log.Fatal(http.ListenAndServe(":8080", handler))
}
func getUserHandler(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id") // Go 1.22+ path parameter extraction
user := User{ID: id, Name: "John Doe"}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
})
}
type User struct {
ID string `json:"id"`
Name string `json:"name"`
}Chi - Lightweight Router
Use When:
- Want stdlib-compatible router with better ergonomics
- Need clean middleware composition
- Prefer explicit over magic patterns
- Building moderate to complex routing structures
Strengths:
- 100% compatible with
net/http - Excellent middleware ecosystem
- Route grouping and nesting
- Context-based parameter passing
- Minimal performance overhead
Installation:
go get -u github.com/go-chi/chi/v5Example:
package main
import (
"encoding/json"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func main() {
r := chi.NewRouter()
// Built-in middleware
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.RequestID)
// Route grouping
r.Route("/api/v1", func(r chi.Router) {
r.Route("/users", func(r chi.Router) {
r.Get("/", listUsers)
r.Post("/", createUser)
r.Route("/{userID}", func(r chi.Router) {
r.Use(UserContext) // Middleware for nested routes
r.Get("/", getUser)
r.Put("/", updateUser)
r.Delete("/", deleteUser)
})
})
})
http.ListenAndServe(":8080", r)
}
func UserContext(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID := chi.URLParam(r, "userID")
// Load user from database, set in context
ctx := context.WithValue(r.Context(), "user", userID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func getUser(w http.ResponseWriter, r *http.Request) {
userID := r.Context().Value("user").(string)
// Return user data
json.NewEncoder(w).Encode(map[string]string{"id": userID})
}Gin - High Performance Framework
Use When:
- Need maximum performance (8x faster than most frameworks)
- Want batteries-included experience
- Require built-in validation and binding
- Building JSON APIs with minimal boilerplate
Strengths:
- Extremely fast (fastest Go framework in benchmarks)
- Built-in JSON binding and validation
- Middleware ecosystem
- Group-based routing
- Custom error handling
Installation:
go get -u github.com/gin-gonic/ginExample:
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
type CreateUserRequest struct {
Name string `json:"name" binding:"required,min=3"`
Email string `json:"email" binding:"required,email"`
Age int `json:"age" binding:"required,gte=18"`
}
func main() {
r := gin.Default() // Logger + Recovery middleware
api := r.Group("/api/v1")
{
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 createUser(c *gin.Context) {
var req CreateUserRequest
// Automatic validation based on struct tags
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Process user creation
user := User{
ID: generateID(),
Name: req.Name,
Email: req.Email,
Age: req.Age,
}
c.JSON(http.StatusCreated, user)
}
func getUser(c *gin.Context) {
id := c.Param("id")
// Return user
c.JSON(http.StatusOK, gin.H{
"id": id,
"name": "John Doe",
})
}Echo - Enterprise Framework
Use When:
- Building enterprise applications
- Need OpenAPI/Swagger integration
- Want comprehensive middleware library
- Require type-safe routing and binding
Strengths:
- Type-safe routing with automatic parameter binding
- Built-in middleware for common patterns
- OpenAPI/Swagger generation support
- Excellent error handling middleware
- WebSocket support
Installation:
go get -u github.com/labstack/echo/v4Example:
package main
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func main() {
e := echo.New()
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.CORS())
// Routes
e.GET("/users/:id", getUser)
e.POST("/users", createUser)
e.PUT("/users/:id", updateUser)
e.DELETE("/users/:id", deleteUser)
e.Logger.Fatal(e.Start(":8080"))
}
func getUser(c echo.Context) error {
id := c.Param("id")
user := User{ID: id, Name: "John Doe"}
return c.JSON(http.StatusOK, user)
}
func createUser(c echo.Context) error {
var user User
if err := c.Bind(&user); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
if err := c.Validate(&user); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
return c.JSON(http.StatusCreated, user)
}
// Custom error handler
func customErrorHandler(err error, c echo.Context) {
code := http.StatusInternalServerError
message := "Internal Server Error"
if he, ok := err.(*echo.HTTPError); ok {
code = he.Code
message = he.Message.(string)
}
c.JSON(code, map[string]string{"error": message})
}Fiber - Express.js Style
Use When:
- Team familiar with Express.js patterns
- Need WebSocket support out of the box
- Building real-time applications
- Want fastest route matching performance
Strengths:
- Express.js-inspired API (easy for Node.js developers)
- Fastest route matching (uses fasthttp)
- Built-in WebSocket support
- Template engine support
- File upload handling
Limitations:
- Uses fasthttp instead of net/http (less ecosystem compatibility)
- Not compatible with standard http.Handler interface
- Slightly less mature ecosystem
Installation:
go get -u github.com/gofiber/fiber/v2Example:
package main
import (
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/fiber/v2/middleware/cors"
)
func main() {
app := fiber.New()
// Middleware
app.Use(logger.New())
app.Use(cors.New())
// Routes
api := app.Group("/api/v1")
users := api.Group("/users")
users.Get("/", listUsers)
users.Post("/", createUser)
users.Get("/:id", getUser)
users.Put("/:id", updateUser)
users.Delete("/:id", deleteUser)
app.Listen(":8080")
}
func getUser(c *fiber.Ctx) error {
id := c.Params("id")
return c.JSON(fiber.Map{
"id": id,
"name": "John Doe",
})
}
func createUser(c *fiber.Ctx) error {
var user User
if err := c.BodyParser(&user); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
return c.Status(201).JSON(user)
}Common HTTP Patterns
Request Validation
Struct Tag Validation:
import "github.com/go-playground/validator/v10"
type CreateUserRequest struct {
Name string `json:"name" validate:"required,min=3,max=50"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"required,gte=18,lte=120"`
Password string `json:"password" validate:"required,min=8"`
}
var validate = validator.New()
func validateRequest(req interface{}) error {
return validate.Struct(req)
}
// Usage
func createUser(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
if err := validateRequest(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Process valid request
}Custom Validators:
import "github.com/go-playground/validator/v10"
var validate = validator.New()
func init() {
validate.RegisterValidation("username", validateUsername)
}
func validateUsername(fl validator.FieldLevel) bool {
username := fl.Field().String()
// Custom validation logic
return len(username) >= 3 && isAlphanumeric(username)
}
type SignupRequest struct {
Username string `validate:"required,username"`
Email string `validate:"required,email"`
}Middleware Patterns
Authentication Middleware:
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
userID, err := validateToken(token)
if err != nil {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
// Add user to context
ctx := context.WithValue(r.Context(), "userID", userID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}Rate Limiting Middleware:
import (
"golang.org/x/time/rate"
"sync"
)
type RateLimiter struct {
limiters map[string]*rate.Limiter
mu sync.RWMutex
rate rate.Limit
burst int
}
func NewRateLimiter(r rate.Limit, b int) *RateLimiter {
return &RateLimiter{
limiters: make(map[string]*rate.Limiter),
rate: r,
burst: b,
}
}
func (rl *RateLimiter) getLimiter(ip string) *rate.Limiter {
rl.mu.Lock()
defer rl.mu.Unlock()
limiter, exists := rl.limiters[ip]
if !exists {
limiter = rate.NewLimiter(rl.rate, rl.burst)
rl.limiters[ip] = limiter
}
return limiter
}
func (rl *RateLimiter) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip := r.RemoteAddr
limiter := rl.getLimiter(ip)
if !limiter.Allow() {
http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}CORS Middleware:
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 == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}Error Handling Strategies
Custom Error Types:
type APIError struct {
Code int `json:"code"`
Message string `json:"message"`
Details string `json:"details,omitempty"`
}
func (e *APIError) Error() string {
return e.Message
}
// Error constructors
func NewBadRequestError(msg string) *APIError {
return &APIError{Code: http.StatusBadRequest, Message: msg}
}
func NewNotFoundError(msg string) *APIError {
return &APIError{Code: http.StatusNotFound, Message: msg}
}
func NewInternalError(msg string) *APIError {
return &APIError{Code: http.StatusInternalServerError, Message: msg}
}Error Response Middleware:
type APIHandler func(w http.ResponseWriter, r *http.Request) error
func ErrorHandlerMiddleware(h APIHandler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err := h(w, r)
if err == nil {
return
}
// Handle different error types
var apiErr *APIError
if errors.As(err, &apiErr) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(apiErr.Code)
json.NewEncoder(w).Encode(apiErr)
return
}
// Unknown error - log and return generic message
log.Printf("Internal error: %v", err)
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(APIError{
Code: http.StatusInternalServerError,
Message: "Internal server error",
})
})
}
// Usage
func getUserHandler(w http.ResponseWriter, r *http.Request) error {
id := r.PathValue("id")
user, err := db.GetUser(id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return NewNotFoundError("User not found")
}
return fmt.Errorf("database error: %w", err)
}
w.Header().Set("Content-Type", "application/json")
return json.NewEncoder(w).Encode(user)
}
// Register with middleware
mux.Handle("GET /users/{id}", ErrorHandlerMiddleware(getUserHandler))REST API Design Patterns
Resource Naming Conventions:
// Good: Plural nouns for collections
GET /api/v1/users // List users
POST /api/v1/users // Create user
GET /api/v1/users/{id} // Get user
PUT /api/v1/users/{id} // Update user (full)
PATCH /api/v1/users/{id} // Update user (partial)
DELETE /api/v1/users/{id} // Delete user
// Nested resources
GET /api/v1/users/{id}/posts // User's posts
POST /api/v1/users/{id}/posts // Create post for user
GET /api/v1/users/{id}/posts/{pid} // Specific post
// Avoid: Verbs in URLs (use HTTP methods instead)
// Bad: POST /api/v1/users/create
// Bad: GET /api/v1/users/get/{id}Pagination Pattern:
type PaginationParams struct {
Page int `json:"page" validate:"gte=1"`
PageSize int `json:"page_size" validate:"gte=1,lte=100"`
}
type PaginatedResponse struct {
Data interface{} `json:"data"`
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalCount int `json:"total_count"`
TotalPages int `json:"total_pages"`
}
func listUsers(w http.ResponseWriter, r *http.Request) {
// Parse pagination params
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
if page < 1 {
page = 1
}
pageSize, _ := strconv.Atoi(r.URL.Query().Get("page_size"))
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
// Fetch data
users, totalCount := db.GetUsers(page, pageSize)
totalPages := (totalCount + pageSize - 1) / pageSize
response := PaginatedResponse{
Data: users,
Page: page,
PageSize: pageSize,
TotalCount: totalCount,
TotalPages: totalPages,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}Query Parameter Filtering:
type UserFilter struct {
Status string `json:"status"`
Role string `json:"role"`
CreatedAt string `json:"created_at"`
Search string `json:"search"`
}
func parseFilters(r *http.Request) UserFilter {
return UserFilter{
Status: r.URL.Query().Get("status"),
Role: r.URL.Query().Get("role"),
CreatedAt: r.URL.Query().Get("created_at"),
Search: r.URL.Query().Get("search"),
}
}
func listUsers(w http.ResponseWriter, r *http.Request) {
filters := parseFilters(r)
users := db.GetUsersWithFilters(filters)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(users)
}
// Example request: GET /api/v1/users?status=active&role=admin&search=johnHTTP Client Patterns
Production-Ready HTTP Client:
import (
"context"
"net/http"
"time"
)
func NewHTTPClient() *http.Client {
return &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
DisableKeepAlives: false,
},
}
}
// Making requests with context
func fetchUser(ctx context.Context, userID string) (*User, error) {
client := NewHTTPClient()
req, err := http.NewRequestWithContext(
ctx,
"GET",
fmt.Sprintf("https://api.example.com/users/%s", userID),
nil,
)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", "Bearer "+getToken())
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("execute request: %w", 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, fmt.Errorf("decode response: %w", err)
}
return &user, nil
}Retry Logic with Exponential Backoff:
import (
"context"
"math"
"time"
)
type RetryConfig struct {
MaxRetries int
BaseDelay time.Duration
MaxDelay time.Duration
}
func DoWithRetry(ctx context.Context, cfg RetryConfig, fn func() error) error {
var err error
for attempt := 0; attempt <= cfg.MaxRetries; attempt++ {
err = fn()
if err == nil {
return nil
}
if attempt < cfg.MaxRetries {
// Exponential backoff
delay := time.Duration(math.Pow(2, float64(attempt))) * cfg.BaseDelay
if delay > cfg.MaxDelay {
delay = cfg.MaxDelay
}
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(delay):
// Continue to next attempt
}
}
}
return fmt.Errorf("max retries exceeded: %w", err)
}
// Usage
err := DoWithRetry(ctx, RetryConfig{
MaxRetries: 3,
BaseDelay: 100 * time.Millisecond,
MaxDelay: 2 * time.Second,
}, func() error {
return makeAPIRequest()
})Testing HTTP Handlers
Using httptest.Server:
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestGetUser(t *testing.T) {
// Create test server
ts := httptest.NewServer(http.HandlerFunc(getUserHandler))
defer ts.Close()
// Make request
resp, err := http.Get(ts.URL + "/users/123")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
// Assertions
if resp.StatusCode != http.StatusOK {
t.Errorf("expected status 200, got %d", resp.StatusCode)
}
var user User
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
t.Fatal(err)
}
if user.ID != "123" {
t.Errorf("expected user ID 123, got %s", user.ID)
}
}Testing Middleware:
func TestAuthMiddleware(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID := r.Context().Value("userID")
if userID == nil {
t.Error("userID not found in context")
}
w.WriteHeader(http.StatusOK)
})
wrapped := AuthMiddleware(handler)
tests := []struct {
name string
token string
wantStatus int
}{
{"Valid token", "valid-token-123", http.StatusOK},
{"Missing token", "", http.StatusUnauthorized},
{"Invalid token", "invalid", http.StatusUnauthorized},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest("GET", "/test", nil)
if tt.token != "" {
req.Header.Set("Authorization", tt.token)
}
rr := httptest.NewRecorder()
wrapped.ServeHTTP(rr, req)
if rr.Code != tt.wantStatus {
t.Errorf("expected status %d, got %d", tt.wantStatus, rr.Code)
}
})
}
}Performance Optimization
Response Compression:
import (
"compress/gzip"
"io"
"net/http"
"strings"
)
type gzipResponseWriter struct {
io.Writer
http.ResponseWriter
}
func (w gzipResponseWriter) Write(b []byte) (int, error) {
return w.Writer.Write(b)
}
func GzipMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
next.ServeHTTP(w, r)
return
}
w.Header().Set("Content-Encoding", "gzip")
gz := gzip.NewWriter(w)
defer gz.Close()
gzw := gzipResponseWriter{Writer: gz, ResponseWriter: w}
next.ServeHTTP(gzw, r)
})
}Connection Pooling Configuration:
import (
"net"
"net/http"
"time"
)
func NewProductionHTTPClient() *http.Client {
return &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
},
}
}Decision Tree
Building HTTP API in Go?
│
├─ Simple API, few dependencies? → net/http (stdlib)
│ ├─ Go 1.22+? → Use new ServeMux pattern routing
│ └─ Go < 1.22? → Consider Chi for better routing
│
├─ Need stdlib compatibility + better ergonomics? → Chi
│ └─ Great for: Middleware chains, route grouping
│
├─ Maximum performance priority? → Gin
│ └─ Great for: JSON APIs, high throughput services
│
├─ Enterprise app with OpenAPI? → Echo
│ └─ Great for: Type safety, comprehensive middleware
│
└─ Team knows Express.js + need WebSockets? → Fiber
└─ Note: Uses fasthttp, not stdlib-compatibleCommon Pitfalls
Pitfall 1: Not Closing Response Bodies
// Bad: Memory leak
resp, _ := http.Get(url)
body, _ := io.ReadAll(resp.Body) // Never closed!
// Good: Always defer close
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)Pitfall 2: Not Using Context for Timeouts
// Bad: No timeout control
resp, _ := http.Get(url)
// Good: Use context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := client.Do(req)Pitfall 3: Ignoring HTTP Status Codes
// Bad: Not checking status
resp, _ := http.Get(url)
defer resp.Body.Close()
json.NewDecoder(resp.Body).Decode(&result)
// Good: Always check status
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status: %d", resp.StatusCode)
}Pitfall 4: Not Reusing HTTP Clients
// Bad: Creates new client per request (no connection pooling)
func makeRequest() {
client := &http.Client{}
resp, _ := client.Get(url)
}
// Good: Reuse client for connection pooling
var httpClient = &http.Client{
Timeout: 30 * time.Second,
}
func makeRequest() {
resp, _ := httpClient.Get(url)
}Go Idiomatic Quality Rules
Small, high-frequency Go idioms worth enforcing in handler/service code:
- Use `:=` for locals with an initial value — reserve
varfor zero values and
package-level declarations.
- Don't name an unused method receiver —
func (*Server) Foo()makes it clear the
receiver is irrelevant.
- Initialize struct pointers with `&T{}`, not `new(T)` — consistent with composite
literals and lets you set fields inline.
- Handle (don't discard) returned errors — a dropped error from
Decode/Write/a
repository call turns a failure into a silent success.
See [go-idioms-quality.md](references/go-idioms-quality.md) for non-idiomatic vs idiomatic examples and how to test/lint each. Most are enforced by gofmt -s, staticcheck, errcheck, and revive under golangci-lint.
Derived from CAST Highlight Go-tagged code-quality indicators
(https://doc.casthighlight.com/), which reference Effective Go and the Go Code Review
Comments wiki as primary sources.
Related Skills
- golang-testing-strategies: Testing HTTP handlers and middleware
- golang-database-patterns: Integrating databases with HTTP APIs
- toolchains-typescript-frameworks-nodejs-backend: Comparison with Node.js patterns
References
{
"name": "golang-http-frameworks",
"version": "1.1.0",
"category": "toolchain",
"toolchain": "golang",
"framework": "web",
"tags": [
"http",
"golang",
"chi",
"gin",
"echo",
"fiber",
"rest-api",
"middleware",
"code-quality",
"idioms",
"anti-patterns"
],
"entry_point_tokens": 160,
"full_tokens": 7400,
"related_skills": [
"golang-testing-strategies",
"golang-database-patterns"
],
"author": "Claude MPM",
"license": "MIT",
"updated": "2026-06-15"
}
Go Idiomatic Quality Rules
Small, high-frequency Go idioms that keep handler and service code readable and idiomatic. These are the kind of thing a reviewer flags repeatedly; encode them in gofmt/go vet/staticcheck/golangci-lint so review effort goes to design instead.
Source note: These idioms are derived from CAST Highlight's language-agnostic
(_multi, Go-tagged) code-quality indicators (https://doc.casthighlight.com/), whichreference the canonical Go style sources. Those sources — Effective Go and the
Go Code Review Comments wiki — are authoritative; cite them, not CAST, for the
underlying rules. Examples are original.
---
1. Use short variable declarations (:=) for locals with an initial value
Why: For a local variable that is set to a value immediately, := is the idiomatic, least-noisy form. var x = v for a local reads as ceremony. Reserve var for zero-value declarations (var buf bytes.Buffer) and package-level variables (where := is not allowed).
Non-idiomatic:
func handler(w http.ResponseWriter, r *http.Request) {
var name = r.URL.Query().Get("name") // use :=
var count = 0 // zero value: just `var count int`
_ = name
_ = count
}Idiomatic:
func handler(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name") // explicit value -> :=
var count int // zero value -> var, no redundant `= 0`
_ = name
_ = count
}How to test: Lint-enforced, not unit-tested. gofmt -s and golangci-lint (gosimple S1021/related) catch the redundant forms; gate them in CI.
---
2. Don't name an unused method receiver
Why: If a method never uses its receiver, leaving it unnamed (func (*Server) Foo()) makes it immediately clear the receiver is irrelevant. A named-but-unused receiver invites the reader to look for a use that isn't there.
Non-idiomatic:
func (s *Server) DefaultPort() int { // s is never used
return 8080
}Idiomatic:
func (*Server) DefaultPort() int { // receiver unnamed: clearly unused
return 8080
}How to test: Lint concern (revive/staticcheck unused-receiver style). Keep it in CI rather than unit tests.
---
3. Initialize struct pointers with &T{}, not new(T)
Why: &T{...} is consistent with composite-literal struct initialization, lets you set fields inline, and reads the same whether or not fields are provided. new(T) returns a zeroed *T you must then mutate field-by-field, and mixing both styles in a codebase is needless inconsistency.
Non-idiomatic:
srv := new(Server)
srv.Addr = ":8080"
srv.Handler = muxIdiomatic:
srv := &Server{
Addr: ":8080",
Handler: mux,
}How to test: Behavior is identical, so this is a readability/lint rule (staticcheck flags new(T) where a literal is clearer). Gate via golangci-lint.
---
4. Handle (don't discard) returned errors
Why: Although not in the original four CAST idioms, the most consequential Go quality signal in handler code is a silently dropped error. An ignored error from w.Write, json.NewDecoder(...).Decode, or a repository call turns a failure into a silent 200. Check every returned error or explicitly document why it's ignored.
Non-idiomatic:
func create(w http.ResponseWriter, r *http.Request) {
var in CreateReq
json.NewDecoder(r.Body).Decode(&in) // error dropped: malformed body -> garbage
repo.Save(in) // error dropped: failure looks like success
w.WriteHeader(http.StatusCreated)
}Idiomatic:
func create(w http.ResponseWriter, r *http.Request) {
var in CreateReq
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
http.Error(w, "invalid body", http.StatusBadRequest)
return
}
if err := repo.Save(r.Context(), in); err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
}How to test: errcheck (via golangci-lint) flags unchecked errors statically. Add a handler test asserting that a malformed body yields 400 and a repo failure yields 500 — proving the errors are actually handled, not dropped.
---
Where this fits
These are review/lint checks complementary to the framework patterns in the main skill. Most are enforced by gofmt -s, go vet, staticcheck, errcheck, and revive under golangci-lint — wire them into CI. For concurrency-specific quality (race-free goroutines, context cancellation), see the golang-concurrency-patterns skill.
Related skills
How it compares
Pick golang-http-frameworks over golang-database-patterns when the task is HTTP routing and middleware, not database access layer design.
FAQ
Which Go HTTP frameworks does golang-http-frameworks cover?
golang-http-frameworks covers stdlib net/http with Go 1.22+ routing plus Chi, Gin, Echo, and Fiber. The skill compares idiomatic routing, middleware, and error-handling patterns across all five options.
When should I use stdlib net/http over Gin or Fiber?
golang-http-frameworks helps evaluate stdlib net/http for minimal dependencies and Go 1.22+ native routing versus Gin or Fiber for richer middleware ecosystems. The choice depends on dependency tolerance and middleware needs.
What other Go skills complement golang-http-frameworks?
The Claude MPM Golang toolchain includes 7 skills covering testing (testify, httptest), concurrency, database patterns, CLI (Cobra/Viper), and OpenTelemetry observability alongside golang-http-frameworks for web APIs.