go-web-expert

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Go Web Expert System

Go Web 专家规范

Five non-negotiable rules for production-quality Go web applications. Every handler, every service, every line of code must satisfy all five.
构建生产级Go Web应用的五条不可妥协的规则。每个处理器、每个服务、每一行代码都必须满足所有五条规则。

Quick Reference

快速参考

TopicReference
Validation tags, custom validators, nested structs, error formattingreferences/validation.md
httptest patterns, middleware testing, integration tests, fixturesreferences/testing-handlers.md
主题参考文档
验证标签、自定义验证器、嵌套结构体、错误格式化references/validation.md
httptest 模式、中间件测试、集成测试、测试夹具references/testing-handlers.md

Rules of Engagement

核心规则

#RuleOne-Liner
1Zero Global StateAll handlers are methods on a struct; no package-level
var
for mutable state
2Explicit Error HandlingEvery error is checked, wrapped with
fmt.Errorf("doing X: %w", err)
3Validation FirstAll incoming JSON validated with
go-playground/validator
at the boundary
4TestabilityEvery handler has a
_test.go
using
httptest
with table-driven tests
5DocumentationEvery exported symbol has a Go doc comment starting with its name

序号规则一句话说明
1零全局状态所有处理器都是结构体的方法;不使用包级
var
存储可变状态
2显式错误处理每个错误都必须被检查,使用
fmt.Errorf("doing X: %w", err)
进行包装
3优先验证所有传入的JSON必须在边界层使用
go-playground/validator
进行验证
4可测试性每个处理器都有对应的
_test.go
文件,使用
httptest
实现表格驱动测试
5文档规范每个导出符号都有以其名称开头的Go文档注释

Rule 1: Zero Global State

规则1:零全局状态

All handlers must be methods on a server struct. No package-level
var
for databases, loggers, clients, or any mutable state.
go
// FORBIDDEN
var db *sql.DB
var logger *slog.Logger

func handleGetUser(w http.ResponseWriter, r *http.Request) {
    user, err := db.QueryRow(...)  // global state -- untestable, unsafe
}

// REQUIRED
type Server struct {
    db     *sql.DB
    logger *slog.Logger
    router *http.ServeMux
}

func (s *Server) handleGetUser(w http.ResponseWriter, r *http.Request) {
    user, err := s.db.QueryRow(...)  // explicit dependency
}
所有处理器必须是服务器结构体的方法。不得使用包级
var
存储数据库、日志器、客户端或任何可变状态。
go
// 禁止写法
var db *sql.DB
var logger *slog.Logger

func handleGetUser(w http.ResponseWriter, r *http.Request) {
    user, err := db.QueryRow(...)  // 全局状态 -- 不可测试、不安全
}

// 要求写法
type Server struct {
    db     *sql.DB
    logger *slog.Logger
    router *http.ServeMux
}

func (s *Server) handleGetUser(w http.ResponseWriter, r *http.Request) {
    user, err := s.db.QueryRow(...)  // 显式依赖
}

What Is Allowed at Package Level

包级允许的内容

  • Constants --
    const maxPageSize = 100
  • Pure functions -- functions with no side effects that depend only on their arguments
  • Sentinel errors --
    var ErrNotFound = errors.New("not found")
  • Validator instance --
    var validate = validator.New()
    (stateless after init)
  • 常量 --
    const maxPageSize = 100
  • 纯函数 -- 无副作用、仅依赖参数的函数
  • 哨兵错误 --
    var ErrNotFound = errors.New("not found")
  • Validator实例 --
    var validate = validator.New()
    (初始化后无状态)

What Is Forbidden at Package Level

包级禁止的内容

  • Database connections (
    *sql.DB
    ,
    *pgxpool.Pool
    )
  • Loggers (
    *slog.Logger
    )
  • HTTP clients configured with timeouts or transport
  • Configuration structs read from environment
  • Caches, rate limiters, or any mutable shared resource
  • 数据库连接(
    *sql.DB
    ,
    *pgxpool.Pool
  • 日志器(
    *slog.Logger
  • 配置了超时或传输的HTTP客户端
  • 从环境变量读取的配置结构体
  • 缓存、限流器或任何可变共享资源

Constructor Pattern

构造函数模式

go
func NewServer(db *sql.DB, logger *slog.Logger) *Server {
    s := &Server{
        db:     db,
        logger: logger,
        router: http.NewServeMux(),
    }
    s.routes()
    return s
}

func (s *Server) routes() {
    s.router.HandleFunc("GET /api/users/{id}", s.handleGetUser)
    s.router.HandleFunc("POST /api/users", s.handleCreateUser)
}

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    s.router.ServeHTTP(w, r)
}

go
func NewServer(db *sql.DB, logger *slog.Logger) *Server {
    s := &Server{
        db:     db,
        logger: logger,
        router: http.NewServeMux(),
    }
    s.routes()
    return s
}

func (s *Server) routes() {
    s.router.HandleFunc("GET /api/users/{id}", s.handleGetUser)
    s.router.HandleFunc("POST /api/users", s.handleCreateUser)
}

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    s.router.ServeHTTP(w, r)
}

Rule 2: Explicit Error Handling

规则2:显式错误处理

Never ignore errors. Every error must be wrapped with context describing what was being attempted when the error occurred.
go
// FORBIDDEN
result, _ := doSomething()
json.NewEncoder(w).Encode(data)  // error ignored

// REQUIRED
result, err := doSomething()
if err != nil {
    return fmt.Errorf("doing something for user %s: %w", userID, err)
}

if err := json.NewEncoder(w).Encode(data); err != nil {
    s.logger.Error("encoding response", "err", err, "request_id", reqID)
}
永远不要忽略错误。每个错误都必须包装上下文信息,描述发生错误时正在执行的操作。
go
// 禁止写法
result, _ := doSomething()
json.NewEncoder(w).Encode(data)  // 错误被忽略

// 要求写法
result, err := doSomething()
if err != nil {
    return fmt.Errorf("doing something for user %s: %w", userID, err)
}

if err := json.NewEncoder(w).Encode(data); err != nil {
    s.logger.Error("encoding response", "err", err, "request_id", reqID)
}

Error Wrapping Convention

错误包装约定

Format:
"<verb>ing <noun>: %w"
-- lowercase, no period, provides call-chain context.
go
// Good wrapping -- each layer adds context
return fmt.Errorf("creating user: %w", err)
return fmt.Errorf("inserting user into database: %w", err)
return fmt.Errorf("hashing password for user %s: %w", email, err)

// Bad wrapping
return fmt.Errorf("error: %w", err)           // no context
return fmt.Errorf("Failed to create user: %w", err) // uppercase, verbose
return err                                      // no wrapping at all
格式:
"<verb>ing <noun>: %w"
-- 小写、无句号,提供调用链上下文。
go
// 良好的包装 -- 每层都添加上下文
return fmt.Errorf("creating user: %w", err)
return fmt.Errorf("inserting user into database: %w", err)
return fmt.Errorf("hashing password for user %s: %w", email, err)

// 糟糕的包装
return fmt.Errorf("error: %w", err)           // 无上下文
return fmt.Errorf("Failed to create user: %w", err) // 大写、冗余
return err                                      // 完全不包装

Structured Error Type for HTTP APIs

HTTP API的结构化错误类型

go
type AppError struct {
    Code    int    `json:"-"`
    Message string `json:"error"`
    Detail  string `json:"detail,omitempty"`
}

func (e *AppError) Error() string {
    return fmt.Sprintf("%d: %s", e.Code, e.Message)
}

// Map domain errors to HTTP errors in one place
func handleError(w http.ResponseWriter, r *http.Request, err error) {
    var appErr *AppError
    if errors.As(err, &appErr) {
        writeJSON(w, appErr.Code, appErr)
        return
    }

    slog.Error("unhandled error",
        "err", err,
        "path", r.URL.Path,
    )
    writeJSON(w, 500, map[string]string{"error": "internal server error"})
}
go
type AppError struct {
    Code    int    `json:"-"`
    Message string `json:"error"`
    Detail  string `json:"detail,omitempty"`
}

func (e *AppError) Error() string {
    return fmt.Sprintf("%d: %s", e.Code, e.Message)
}

// 在一处将领域错误映射为HTTP错误
func handleError(w http.ResponseWriter, r *http.Request, err error) {
    var appErr *AppError
    if errors.As(err, &appErr) {
        writeJSON(w, appErr.Code, appErr)
        return
    }

    slog.Error("unhandled error",
        "err", err,
        "path", r.URL.Path,
    )
    writeJSON(w, 500, map[string]string{"error": "internal server error"})
}

Common Mistakes

常见错误

go
// MISTAKE: not checking Close errors on writers
defer f.Close()  // at minimum, log Close errors for writable resources

// BETTER for writable resources:
defer func() {
    if err := f.Close(); err != nil {
        s.logger.Error("closing file", "err", err)
    }
}()

// OK for read-only resources where Close rarely fails:
defer resp.Body.Close()

go
// 错误:不检查写入器的Close错误
defer f.Close()  // 对于可写资源,至少要记录Close错误

// 可写资源的更好写法:
defer func() {
    if err := f.Close(); err != nil {
        s.logger.Error("closing file", "err", err)
    }
}()

// 只读资源(Close很少失败)的写法没问题:
defer resp.Body.Close()

Rule 3: Validation First

规则3:优先验证

Use
go-playground/validator
for all incoming JSON. Validate at the boundary, trust internal data.
go
import "github.com/go-playground/validator/v10"

var validate = validator.New()

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:"omitempty,gte=0,lte=150"`
}

func (s *Server) handleCreateUser(w http.ResponseWriter, r *http.Request) error {
    var req CreateUserRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        return &AppError{Code: 400, Message: "invalid JSON", Detail: err.Error()}
    }

    if err := validate.Struct(req); err != nil {
        return &AppError{Code: 422, Message: "validation failed", Detail: formatValidationErrors(err)}
    }

    // From here, req is trusted
    user, err := s.userService.Create(r.Context(), req.Name, req.Email)
    if err != nil {
        return fmt.Errorf("creating user: %w", err)
    }

    writeJSON(w, http.StatusCreated, user)
    return nil
}
对所有传入的JSON使用
go-playground/validator
进行验证。在边界层完成验证,信任内部数据。
go
import "github.com/go-playground/validator/v10"

var validate = validator.New()

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:"omitempty,gte=0,lte=150"`
}

func (s *Server) handleCreateUser(w http.ResponseWriter, r *http.Request) error {
    var req CreateUserRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        return &AppError{Code: 400, Message: "invalid JSON", Detail: err.Error()}
    }

    if err := validate.Struct(req); err != nil {
        return &AppError{Code: 422, Message: "validation failed", Detail: formatValidationErrors(err)}
    }

    // 从此处开始,req是可信的
    user, err := s.userService.Create(r.Context(), req.Name, req.Email)
    if err != nil {
        return fmt.Errorf("creating user: %w", err)
    }

    writeJSON(w, http.StatusCreated, user)
    return nil
}

Validation Error Formatting

验证错误格式化

go
func formatValidationErrors(err error) string {
    var msgs []string
    for _, e := range err.(validator.ValidationErrors) {
        msgs = append(msgs, fmt.Sprintf("field '%s' failed on '%s'", e.Field(), e.Tag()))
    }
    return strings.Join(msgs, "; ")
}
go
func formatValidationErrors(err error) string {
    var msgs []string
    for _, e := range err.(validator.ValidationErrors) {
        msgs = append(msgs, fmt.Sprintf("field '%s' failed on '%s'", e.Field(), e.Tag()))
    }
    return strings.Join(msgs, "; ")
}

Validation Boundary Rule

验证边界规则

  • Validate at the edge -- HTTP handlers, message consumers, CLI input
  • Trust internal data -- service layer receives already-validated types
  • Never validate twice -- if the handler validated, the service does not re-validate the same fields
See references/validation.md for custom validators, nested struct validation, slice validation, and cross-field validation.

  • 在边缘层验证 -- HTTP处理器、消息消费者、CLI输入
  • 信任内部数据 -- 服务层接收已验证的类型
  • 不要重复验证 -- 如果处理器已验证,服务层无需重新验证相同字段
请查看references/validation.md了解自定义验证器、嵌套结构体验证、切片验证和跨字段验证的内容。

Rule 4: Testability

规则4:可测试性

Every handler must have a corresponding
_test.go
file using
httptest
. Test through the HTTP layer, not by calling handler methods directly.
go
func TestServer_handleGetUser(t *testing.T) {
    mockStore := &MockUserStore{
        GetUserFunc: func(ctx context.Context, id string) (*User, error) {
            if id == "123" {
                return &User{ID: "123", Name: "Alice"}, nil
            }
            return nil, ErrNotFound
        },
    }
    srv := NewServer(mockStore, slog.Default())

    tests := []struct {
        name       string
        path       string
        wantStatus int
        wantBody   string
    }{
        {
            name:       "existing user",
            path:       "/api/users/123",
            wantStatus: http.StatusOK,
            wantBody:   `"name":"Alice"`,
        },
        {
            name:       "not found",
            path:       "/api/users/999",
            wantStatus: http.StatusNotFound,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            req := httptest.NewRequest("GET", tt.path, nil)
            w := httptest.NewRecorder()

            srv.ServeHTTP(w, req)

            if w.Code != tt.wantStatus {
                t.Errorf("status = %d, want %d", w.Code, tt.wantStatus)
            }
            if tt.wantBody != "" && !strings.Contains(w.Body.String(), tt.wantBody) {
                t.Errorf("body = %q, want to contain %q", w.Body.String(), tt.wantBody)
            }
        })
    }
}
每个处理器必须有对应的
_test.go
文件,使用
httptest
实现。通过HTTP层进行测试,不要直接调用处理器方法。
go
func TestServer_handleGetUser(t *testing.T) {
    mockStore := &MockUserStore{
        GetUserFunc: func(ctx context.Context, id string) (*User, error) {
            if id == "123" {
                return &User{ID: "123", Name: "Alice"}, nil
            }
            return nil, ErrNotFound
        },
    }
    srv := NewServer(mockStore, slog.Default())

    tests := []struct {
        name       string
        path       string
        wantStatus int
        wantBody   string
    }{
        {
            name:       "existing user",
            path:       "/api/users/123",
            wantStatus: http.StatusOK,
            wantBody:   `"name":"Alice"`,
        },
        {
            name:       "not found",
            path:       "/api/users/999",
            wantStatus: http.StatusNotFound,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            req := httptest.NewRequest("GET", tt.path, nil)
            w := httptest.NewRecorder()

            srv.ServeHTTP(w, req)

            if w.Code != tt.wantStatus {
                t.Errorf("status = %d, want %d", w.Code, tt.wantStatus)
            }
            if tt.wantBody != "" && !strings.Contains(w.Body.String(), tt.wantBody) {
                t.Errorf("body = %q, want to contain %q", w.Body.String(), tt.wantBody)
            }
        })
    }
}

Key Testing Principles

核心测试原则

  • Test through HTTP -- use
    httptest.NewRequest
    and
    httptest.NewRecorder
    , call
    srv.ServeHTTP
  • Interface-based mocks -- define narrow interfaces at the consumer, create mock implementations for tests
  • Table-driven tests -- one
    []struct
    with test cases, one
    t.Run
    loop
  • Error paths matter -- test 400s, 404s, 422s, and 500s, not just 200s
  • No global test state -- each test creates its own server with its own mocks
See references/testing-handlers.md for middleware testing, integration tests with real databases, file upload testing, and streaming response testing.

  • 通过HTTP测试 -- 使用
    httptest.NewRequest
    httptest.NewRecorder
    ,调用
    srv.ServeHTTP
  • 基于接口的Mock -- 在消费端定义窄接口,为测试创建Mock实现
  • 表格驱动测试 -- 使用一个
    []struct
    存储测试用例,通过一个
    t.Run
    循环执行
  • 错误路径同样重要 -- 测试400、404、422和500错误,不只是200成功响应
  • 无全局测试状态 -- 每个测试创建自己的服务器和Mock
请查看references/testing-handlers.md了解中间件测试、使用真实数据库的集成测试、文件上传测试和流式响应测试的内容。

Rule 5: Documentation

规则5:文档规范

Every exported function, type, method, and constant must have a Go doc comment following standard conventions.
go
// CreateUser creates a new user with the given name and email.
// It returns ErrDuplicateEmail if a user with the same email already exists.
func (s *UserService) CreateUser(ctx context.Context, name, email string) (*User, error) {
    // ...
}

// Server handles HTTP requests for the user API.
type Server struct {
    // ...
}

// NewServer creates a Server with the given dependencies.
// The logger must not be nil.
func NewServer(store UserStore, logger *slog.Logger) *Server {
    // ...
}

// ErrNotFound is returned when a requested resource does not exist.
var ErrNotFound = errors.New("not found")
每个导出的函数、类型、方法和常量都必须有符合标准约定的Go文档注释。
go
// CreateUser creates a new user with the given name and email.
// It returns ErrDuplicateEmail if a user with the same email already exists.
func (s *UserService) CreateUser(ctx context.Context, name, email string) (*User, error) {
    // ...
}

// Server handles HTTP requests for the user API.
type Server struct {
    // ...
}

// NewServer creates a Server with the given dependencies.
// The logger must not be nil.
func NewServer(store UserStore, logger *slog.Logger) *Server {
    // ...
}

// ErrNotFound is returned when a requested resource does not exist.
var ErrNotFound = errors.New("not found")

Doc Comment Conventions

文档注释约定

  • Start with the name --
    // CreateUser creates...
    not
    // This function creates...
  • First sentence is the summary -- shown in
    go doc
    listings and IDE tooltips
  • Mention important error returns -- callers need to know which errors to check
  • Don't document the obvious --
    // SetName sets the name
    adds no value
  • Document why, not what -- when behavior is non-obvious, explain the reasoning
  • 以名称开头 --
    // CreateUser creates...
    而不是
    // This function creates...
  • 第一句是摘要 -- 会显示在
    go doc
    列表和IDE提示中
  • 提及重要的错误返回 -- 调用者需要知道要检查哪些错误
  • 不要记录显而易见的内容 --
    // SetName sets the name
    没有任何价值
  • 记录原因,而非操作 -- 当行为不明显时,解释背后的逻辑

Package Documentation

包文档

go
// Package user provides user management for the application.
// It handles creation, retrieval, and deletion of user accounts,
// with email uniqueness enforced at the database level.
package user

go
// Package user provides user management for the application.
// It handles creation, retrieval, and deletion of user accounts,
// with email uniqueness enforced at the database level.
package user

Cross-Cutting Concerns

跨领域关注点

The five rules reinforce each other. Here is how they interact.
五条规则相互强化。以下是它们的交互方式。

Zero Global State Enables Testability

零全局状态提升可测试性

Because all dependencies are on the struct, tests can inject mocks:
go
// Production
srv := NewServer(realDB, prodLogger)

// Test
srv := NewServer(mockStore, slog.Default())
If
db
were a global
var
, tests would need to mutate package state, causing race conditions in parallel tests.
因为所有依赖都存储在结构体中,测试可以注入Mock:
go
// 生产环境
srv := NewServer(realDB, prodLogger)

// 测试环境
srv := NewServer(mockStore, slog.Default())
如果
db
是全局
var
,测试需要修改包状态,会导致并行测试中的竞态条件。

Validation First Simplifies Error Handling

优先验证简化错误处理

When handlers validate at the boundary, the service layer can assume valid input. This means service-layer errors are always unexpected (database failures, network issues), and error handling becomes simpler:
go
func (s *UserService) Create(ctx context.Context, name, email string) (*User, error) {
    // No need to check if name is empty -- handler already validated
    user := &User{Name: name, Email: email}
    if err := s.store.Insert(ctx, user); err != nil {
        return nil, fmt.Errorf("inserting user: %w", err)
    }
    return user, nil
}
当处理器在边界层完成验证后,服务层可以假设输入是有效的。这意味着服务层的错误总是意外情况(数据库故障、网络问题),错误处理变得更简单:
go
func (s *UserService) Create(ctx context.Context, name, email string) (*User, error) {
    // 无需检查名称是否为空 -- 处理器已验证
    user := &User{Name: name, Email: email}
    if err := s.store.Insert(ctx, user); err != nil {
        return nil, fmt.Errorf("inserting user: %w", err)
    }
    return user, nil
}

Documentation Makes Error Handling Discoverable

文档让错误处理更易发现

Doc comments that mention error returns tell callers what to handle:
go
// Delete removes a user by ID.
// It returns ErrNotFound if the user does not exist.
// It returns ErrHasActiveOrders if the user has unfinished orders.
func (s *UserService) Delete(ctx context.Context, id string) error {

提及错误返回的文档注释告诉调用者需要处理哪些错误:
go
// Delete removes a user by ID.
// It returns ErrNotFound if the user does not exist.
// It returns ErrHasActiveOrders if the user has unfinished orders.
func (s *UserService) Delete(ctx context.Context, id string) error {

Self-Review Checklist

自我检查清单

Before considering any handler or service complete, verify all five rules:
在认为任何处理器或服务完成之前,验证所有五条规则:

Zero Global State

零全局状态

  • No package-level
    var
    for mutable state (db, logger, clients)
  • All handlers are methods on a struct
  • Dependencies injected through constructor
  • 没有使用包级
    var
    存储可变状态(数据库、日志器、客户端)
  • 所有处理器都是结构体的方法
  • 通过构造函数注入依赖

Explicit Error Handling

显式错误处理

  • No
    _
    ignoring returned errors
  • All errors wrapped with
    fmt.Errorf("doing X: %w", err)
  • json.NewEncoder(w).Encode(...)
    error checked or logged
  • Structured
    AppError
    used for HTTP error responses
  • 没有使用
    _
    忽略返回的错误
  • 所有错误都使用
    fmt.Errorf("doing X: %w", err)
    进行包装
  • json.NewEncoder(w).Encode(...)
    的错误被检查或记录
  • 使用结构化
    AppError
    处理HTTP错误响应

Validation First

优先验证

  • All request structs have
    validate
    tags
  • validate.Struct(req)
    called before any business logic
  • Validation errors return 422 with field-level detail
  • Service layer does not re-validate handler-validated data
  • 所有请求结构体都有
    validate
    标签
  • 在执行业务逻辑前调用
    validate.Struct(req)
  • 验证错误返回422状态码和字段级详情
  • 服务层不重新验证处理器已验证的数据

Testability

可测试性

  • _test.go
    file exists for every handler file
  • Tests use
    httptest.NewRequest
    and
    httptest.NewRecorder
  • Table-driven tests cover happy path and error paths
  • Mocks implement narrow interfaces, not concrete types
  • 每个处理器文件都有对应的
    _test.go
    文件
  • 测试使用
    httptest.NewRequest
    httptest.NewRecorder
  • 表格驱动测试覆盖正常路径和错误路径
  • Mock实现窄接口,而非具体类型

Documentation

文档规范

  • Every exported function has a doc comment starting with its name
  • Error return values are documented
  • Package has a doc comment
  • 每个导出函数都有以其名称开头的文档注释
  • 错误返回值已被记录
  • 包有文档注释

When to Load References

何时查看参考文档

Load validation.md when:
  • Adding new request types with validation tags
  • Creating custom validators
  • Validating nested structs, slices, or maps
  • Formatting validation errors for API responses
Load testing-handlers.md when:
  • Writing handler tests for the first time in a project
  • Testing middleware chains or authentication
  • Setting up integration tests with a real database
  • Testing file uploads or streaming responses
当以下情况时查看validation.md
  • 添加带有验证标签的新请求类型
  • 创建自定义验证器
  • 验证嵌套结构体、切片或映射
  • 为API响应格式化验证错误
当以下情况时查看testing-handlers.md
  • 在项目中首次编写处理器测试
  • 测试中间件链或认证逻辑
  • 设置使用真实数据库的集成测试
  • 测试文件上传或流式响应