正在加载,请稍候…

Go Web Development: Building REST APIs with net/http, Chi, and Best Practices

Complete guide to building web APIs in Go: net/http basics, routing with Chi, middleware, JSON handling, database integration, error handling patterns, and structuring Go web projects.

Why Go for Web APIs?

Go has become a mainstream choice for backend APIs and microservices, competing directly with Node.js and Python. The reasons are practical: compilation to a single binary, excellent performance without tuning, built-in concurrency, and a standard library that handles ~80% of HTTP needs without third-party frameworks.

This guide builds a complete REST API from scratch, covering patterns you'll use in production.

Starting with net/http

Go's standard library net/http is production-ready:

package main

import (
    "encoding/json"
    "log"
    "net/http"
)

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

func main() {
    mux := http.NewServeMux()
    
    mux.HandleFunc("GET /users", listUsers)
    mux.HandleFunc("GET /users/{id}", getUser) // Go 1.22+ path parameters
    mux.HandleFunc("POST /users", createUser)
    
    server := &http.Server{
        Addr:         ":8080",
        Handler:      mux,
        ReadTimeout:  10 * time.Second,
        WriteTimeout: 10 * time.Second,
        IdleTimeout:  120 * time.Second,
    }
    
    log.Println("Starting server on :8080")
    log.Fatal(server.ListenAndServe())
}

func listUsers(w http.ResponseWriter, r *http.Request) {
    users := []User{
        {ID: 1, Name: "Alice", Email: "alice@example.com"},
        {ID: 2, Name: "Bob", Email: "bob@example.com"},
    }
    
    writeJSON(w, http.StatusOK, users)
}

func getUser(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id") // Go 1.22+
    user, err := findUserByID(id)
    if err != nil {
        writeError(w, http.StatusNotFound, "User not found")
        return
    }
    writeJSON(w, http.StatusOK, user)
}

// Helper functions
func writeJSON(w http.ResponseWriter, status int, v any) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    if err := json.NewEncoder(w).Encode(v); err != nil {
        log.Printf("Error encoding response: %v", err)
    }
}

func writeError(w http.ResponseWriter, status int, message string) {
    writeJSON(w, status, map[string]string{"error": message})
}

Using Chi for Complex Routing

For more complex APIs, chi provides clean routing without a heavy framework:

// go get github.com/go-chi/chi/v5

import (
    "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)
    r.Use(middleware.RealIP)
    
    // Custom middleware
    r.Use(corsMiddleware)
    r.Use(authMiddleware)
    
    // Route groups
    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(userCtx) // Load user into context
                r.Get("/", getUser)
                r.Put("/", updateUser)
                r.Delete("/", deleteUser)
                
                r.Get("/posts", getUserPosts)
            })
        })
        
        r.Route("/posts", func(r chi.Router) {
            r.Get("/", listPosts)
            r.With(requireAuth).Post("/", createPost)
        })
    })
    
    http.ListenAndServe(":8080", r)
}

Middleware Pattern

// Middleware is a function that wraps an http.Handler
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 %d %v %s",
            r.Method, r.URL.Path,
            wrapped.status,
            time.Since(start),
            r.RemoteAddr,
        )
    })
}

type responseWriter struct {
    http.ResponseWriter
    status int
}

func (rw *responseWriter) WriteHeader(status int) {
    rw.status = status
    rw.ResponseWriter.WriteHeader(status)
}

// 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 !strings.HasPrefix(token, "Bearer ") {
            writeError(w, http.StatusUnauthorized, "Missing token")
            return
        }
        
        claims, err := validateJWT(strings.TrimPrefix(token, "Bearer "))
        if err != nil {
            writeError(w, http.StatusUnauthorized, "Invalid token")
            return
        }
        
        // Store user in request context
        ctx := context.WithValue(r.Context(), userKey, claims.UserID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// Retrieve from context
func currentUser(r *http.Request) int {
    return r.Context().Value(userKey).(int)
}

JSON Request Handling

type CreateUserRequest struct {
    Name  string `json:"name"  validate:"required,min=1,max=100"`
    Email string `json:"email" validate:"required,email"`
    Age   int    `json:"age"   validate:"gte=0,lte=150"`
}

func createUser(w http.ResponseWriter, r *http.Request) {
    var req CreateUserRequest
    
    // Decode JSON body
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeError(w, http.StatusBadRequest, "Invalid JSON: "+err.Error())
        return
    }
    
    // Validate (using github.com/go-playground/validator)
    if err := validate.Struct(req); err != nil {
        errors := formatValidationErrors(err)
        writeJSON(w, http.StatusUnprocessableEntity, map[string]any{
            "error":  "Validation failed",
            "fields": errors,
        })
        return
    }
    
    user, err := userService.Create(r.Context(), req)
    if err != nil {
        switch {
        case errors.Is(err, ErrEmailTaken):
            writeError(w, http.StatusConflict, "Email already registered")
        default:
            log.Printf("Error creating user: %v", err)
            writeError(w, http.StatusInternalServerError, "Internal server error")
        }
        return
    }
    
    writeJSON(w, http.StatusCreated, user)
}

Database Integration with pgx

// go get github.com/jackc/pgx/v5

import "github.com/jackc/pgx/v5/pgxpool"

// Connection pool
func newDB(ctx context.Context, connStr string) (*pgxpool.Pool, error) {
    config, err := pgxpool.ParseConfig(connStr)
    if err != nil {
        return nil, err
    }
    
    config.MaxConns = 25
    config.MinConns = 5
    config.MaxConnLifetime = 5 * time.Minute
    config.HealthCheckPeriod = 30 * time.Second
    
    return pgxpool.NewWithConfig(ctx, config)
}

// Repository pattern
type UserRepository struct {
    db *pgxpool.Pool
}

func (r *UserRepository) FindByID(ctx context.Context, id int) (*User, error) {
    var user User
    err := r.db.QueryRow(ctx,
        "SELECT id, name, email, created_at FROM users WHERE id = $1",
        id,
    ).Scan(&user.ID, &user.Name, &user.Email, &user.CreatedAt)
    
    if err != nil {
        if errors.Is(err, pgx.ErrNoRows) {
            return nil, ErrNotFound
        }
        return nil, fmt.Errorf("FindByID: %w", err)
    }
    
    return &user, nil
}

func (r *UserRepository) List(ctx context.Context, limit, offset int) ([]User, error) {
    rows, err := r.db.Query(ctx,
        "SELECT id, name, email FROM users ORDER BY created_at DESC LIMIT $1 OFFSET $2",
        limit, offset,
    )
    if err != nil {
        return nil, err
    }
    defer rows.Close()
    
    users := make([]User, 0)
    for rows.Next() {
        var u User
        if err := rows.Scan(&u.ID, &u.Name, &u.Email); err != nil {
            return nil, err
        }
        users = append(users, u)
    }
    
    return users, rows.Err()
}

// Transaction
func (r *UserRepository) Transfer(ctx context.Context, fromID, toID int, amount float64) error {
    tx, err := r.db.Begin(ctx)
    if err != nil {
        return err
    }
    defer tx.Rollback(ctx) // No-op if committed
    
    _, err = tx.Exec(ctx,
        "UPDATE accounts SET balance = balance - $1 WHERE user_id = $2",
        amount, fromID,
    )
    if err != nil {
        return fmt.Errorf("debit: %w", err)
    }
    
    _, err = tx.Exec(ctx,
        "UPDATE accounts SET balance = balance + $1 WHERE user_id = $2",
        amount, toID,
    )
    if err != nil {
        return fmt.Errorf("credit: %w", err)
    }
    
    return tx.Commit(ctx)
}

Error Handling Patterns

// Sentinel errors — specific error values for comparison
var (
    ErrNotFound    = errors.New("not found")
    ErrEmailTaken  = errors.New("email already taken")
    ErrUnauthorized = errors.New("unauthorized")
)

// Wrapped errors — add context while preserving the original
func getUser(id int) (*User, error) {
    user, err := db.QueryUser(id)
    if err != nil {
        return nil, fmt.Errorf("getUser(%d): %w", id, err) // %w wraps the error
    }
    return user, nil
}

// Check error type with errors.Is (works through wrapping chain)
err := getUser(123)
if errors.Is(err, sql.ErrNoRows) {
    // Handle not found
}

// Custom error types for rich error information
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation error: %s - %s", e.Field, e.Message)
}

// Check with errors.As
var valErr *ValidationError
if errors.As(err, &valErr) {
    fmt.Printf("Field %s: %s", valErr.Field, valErr.Message)
}

Project Structure

myapi/
├── cmd/
│   └── api/
│       └── main.go          # Entry point
├── internal/
│   ├── handler/             # HTTP handlers
│   │   ├── users.go
│   │   └── posts.go
│   ├── service/             # Business logic
│   │   ├── user_service.go
│   │   └── post_service.go
│   ├── repository/          # Data access
│   │   ├── user_repo.go
│   │   └── post_repo.go
│   ├── middleware/          # HTTP middleware
│   │   ├── auth.go
│   │   └── logging.go
│   └── model/               # Domain types
│       ├── user.go
│       └── post.go
├── config/
│   └── config.go            # Configuration loading
├── go.mod
└── go.sum

Go's approach to web development is refreshingly pragmatic — a minimal standard library, explicit error handling, and straightforward concurrency. The lack of "magic" (no ORMs, no DI containers by default) makes Go code easy to read and debug, even in large codebases.

→ Encode and decode URL parameters with the URL Encoder/Decoder tool.