正在加载,请稍候…

Go Web 开发:使用 net/http、Chi 和最佳实践构建 REST API

Go 语言 Web API 完整指南:net/http 基础、Chi 路由、中间件、JSON 处理、数据库集成、错误处理模式及项目结构

Go Web 开发:使用 net/http、Chi 和最佳实践构建 REST API

为什么选择 Go 构建 Web API?

Go 已成为后端 API 和微服务的主流选择,与 Node.js 和 Python 直接竞争。原因很实际:编译为单个二进制文件、无需调优即可获得出色性能、内置并发支持,以及标准库无需第三方框架即可满足约 80% 的 HTTP 需求。

本指南将从零开始构建一个完整的 REST API,涵盖你在生产环境中会使用的模式。

Go Web 开发:使用 net/http、Chi 和最佳实践构建 REST API 插图

从 net/http 开始

Go 的标准库 net/http 已可用于生产环境:

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+ 路径参数
    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)
}

// 辅助函数
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})
}

使用 Chi 处理复杂路由

对于更复杂的 API,chi 提供了简洁的路由,无需重型框架:

// 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()
    
    // 内置中间件
    r.Use(middleware.Logger)
    r.Use(middleware.Recoverer)
    r.Use(middleware.RequestID)
    r.Use(middleware.RealIP)
    
    // 自定义中间件
    r.Use(corsMiddleware)
    r.Use(authMiddleware)
    
    // 路由分组
    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) // 将用户加载到上下文中
                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)
}

Go Web 开发:使用 net/http、Chi 和最佳实践构建 REST API 插图

中间件模式

// 中间件是一个包装 http.Handler 的函数
func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        
        // 包装 ResponseWriter 以捕获状态码
        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)
}

// 认证中间件
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
        }
        
        // 将用户存储到请求上下文中
        ctx := context.WithValue(r.Context(), userKey, claims.UserID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// 从上下文中获取当前用户
func currentUser(r *http.Request) int {
    return r.Context().Value(userKey).(int)
}

JSON 请求处理

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
    
    // 解码 JSON 请求体
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeError(w, http.StatusBadRequest, "Invalid JSON: "+err.Error())
        return
    }
    
    // 验证(使用 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)
}

Go Web 开发:使用 net/http、Chi 和最佳实践构建 REST API 插图

使用 pgx 集成数据库

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

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

// 连接池
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)
}

// 仓库模式

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()
}

// 事务
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) // 如果已提交则无操作
    
    _, 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)
}

错误处理模式

// 哨兵错误——用于比较的特定错误值
var (
    ErrNotFound    = errors.New("not found")
    ErrEmailTaken  = errors.New("email already taken")
    ErrUnauthorized = errors.New("unauthorized")
)

// 包装错误——在保留原始错误的同时添加上下文
func getUser(id int) (*User, error) {
    user, err := db.QueryUser(id)
    if err != nil {
        return nil, fmt.Errorf("getUser(%d): %w", id, err) // %w 包装错误
    }
    return user, nil
}

// 使用 errors.Is 检查错误类型(可穿透包装链)
err := getUser(123)
if errors.Is(err, sql.ErrNoRows) {
    // 处理未找到
}

// 自定义错误类型以提供丰富的错误信息
type ValidationError struct {
    Field   string
    Message string
}

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

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

项目结构

myapi/
├── cmd/
│   └── api/
│       └── main.go          # 入口点
├── internal/
│   ├── handler/             # HTTP 处理器
│   │   ├── users.go
│   │   └── posts.go
│   ├── service/             # 业务逻辑
│   │   ├── user_service.go
│   │   └── post_service.go
│   ├── repository/          # 数据访问
│   │   ├── user_repo.go
│   │   └── post_repo.go
│   ├── middleware/          # HTTP 中间件
│   │   ├── auth.go
│   │   └── logging.go
│   └── model/               # 领域类型
│       ├── user.go
│       └── post.go
├── config/
│   └── config.go            # 配置加载
├── go.mod
└── go.sum

Go 的 Web 开发方法令人耳目一新地务实——极简的标准库、显式的错误处理和直接的并发支持。缺乏“魔法”(默认没有 ORM、没有 DI 容器)使得 Go 代码易于阅读和调试,即使在大型代码库中也是如此。

→ 使用 URL 编码/解码工具 对 URL 参数进行编码和解码。