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/v5
Example:
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/gin
Example:
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/v4
Example:
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/v2
Example:
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=john
HTTP 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-compatible
Common 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) }
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
-
Go net/http Documentation
-
Chi Framework
-
Gin Framework
-
Echo Framework
-
Fiber Framework
-
Go HTTP Best Practices