go-architect

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Lead Go Architect

资深Go架构师实践指南

Quick Reference

快速参考

TopicReference
Flat vs modular project layout, migration signalsreferences/project-structure.md
Graceful shutdown with signal handlingreferences/graceful-shutdown.md
Dependency injection patterns, testing seamsreferences/dependency-injection.md
主题参考文档
扁平与模块化项目布局、迁移信号references/project-structure.md
带信号处理的优雅关闭references/graceful-shutdown.md
依赖注入模式、测试接缝references/dependency-injection.md

Core Principles

核心原则

  1. Standard library first -- Use
    net/http
    and the Go 1.22+ enhanced
    ServeMux
    for routing. Only reach for a framework (chi, echo, gin) when you have a concrete need the stdlib cannot satisfy (e.g., complex middleware chains, regex routes).
  2. Dependency injection over globals -- Pass databases, loggers, and services through struct fields and constructors, never package-level
    var
    .
  3. Explicit over magic -- No
    init()
    side effects, no framework auto-wiring.
    main.go
    is the composition root where everything is assembled visibly.
  4. Small interfaces, big structs -- Define interfaces at the consumer, keep them narrow (1-3 methods). Concrete types carry the implementation.
  1. 优先使用标准库 —— 使用
    net/http
    及Go 1.22+增强版
    ServeMux
    实现路由。只有当标准库无法满足具体需求时(例如复杂中间件链、正则路由),才考虑使用第三方框架(如chi、echo、gin)。
  2. 依赖注入替代全局变量 —— 通过结构体字段和构造函数传递数据库、日志器和服务实例,绝不要使用包级全局
    var
    变量。
  3. 显式胜于魔法 —— 避免
    init()
    函数的副作用,不使用框架自动注入。
    main.go
    作为组合根,所有组件的装配过程清晰可见。
  4. 小接口,大结构体 —— 在消费端定义接口,保持接口精简(1-3个方法)。具体类型承载实现逻辑。

Go 1.22+ Enhanced Routing

Go 1.22+增强型路由

Go 1.22 upgraded
http.ServeMux
with method-based routing and path parameters, eliminating the most common reason for third-party routers.
Go 1.22版本升级了
http.ServeMux
,支持基于请求方法的路由和路径参数,消除了使用第三方路由库的最常见理由。

Method-Based Routing and Path Parameters

基于方法的路由与路径参数

go
mux := http.NewServeMux()
mux.HandleFunc("GET /api/users", s.handleListUsers)
mux.HandleFunc("GET /api/users/{id}", s.handleGetUser)
mux.HandleFunc("POST /api/users", s.handleCreateUser)
mux.HandleFunc("DELETE /api/users/{id}", s.handleDeleteUser)
go
mux := http.NewServeMux()
mux.HandleFunc("GET /api/users", s.handleListUsers)
mux.HandleFunc("GET /api/users/{id}", s.handleGetUser)
mux.HandleFunc("POST /api/users", s.handleCreateUser)
mux.HandleFunc("DELETE /api/users/{id}", s.handleDeleteUser)

Extracting Path Parameters

提取路径参数

go
func (s *Server) handleGetUser(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    if id == "" {
        http.Error(w, "missing id", http.StatusBadRequest)
        return
    }

    user, err := s.users.GetUser(r.Context(), id)
    if err != nil {
        s.logger.Error("getting user", "err", err, "id", id)
        http.Error(w, "internal error", http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(user)
}
go
func (s *Server) handleGetUser(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    if id == "" {
        http.Error(w, "missing id", http.StatusBadRequest)
        return
    }

    user, err := s.users.GetUser(r.Context(), id)
    if err != nil {
        s.logger.Error("getting user", "err", err, "id", id)
        http.Error(w, "internal error", http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(user)
}

Wildcard and Exact Match

通配符与精确匹配

go
// Exact match on trailing slash -- serves /api/files/ only
mux.HandleFunc("GET /api/files/", s.handleListFiles)

// Wildcard to end of path -- /api/files/path/to/doc.txt
mux.HandleFunc("GET /api/files/{path...}", s.handleGetFile)
go
// 精确匹配尾部斜杠 -- 仅匹配/api/files/
mux.HandleFunc("GET /api/files/", s.handleListFiles)

// 通配符匹配路径剩余部分 -- 匹配/api/files/path/to/doc.txt
mux.HandleFunc("GET /api/files/{path...}", s.handleGetFile)

Routing Precedence

路由优先级

The new
ServeMux
uses most-specific-wins precedence:
  • GET /api/users/{id}
    is more specific than
    GET /api/users/
  • GET /api/users/me
    is more specific than
    GET /api/users/{id}
  • Method routes take precedence over method-less routes
新版
ServeMux
采用"最具体优先"的规则:
  • GET /api/users/{id}
    GET /api/users/
    更具体
  • GET /api/users/me
    GET /api/users/{id}
    更具体
  • 带请求方法的路由优先级高于不带方法的路由

Server Struct Pattern

Server结构体模式

The Server struct is the central dependency container for your application. It holds all shared dependencies and implements
http.Handler
.
go
type Server struct {
    db     *sql.DB
    logger *slog.Logger
    router *http.ServeMux
}

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)
    s.router.HandleFunc("GET /healthz", s.handleHealth)
}

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    s.router.ServeHTTP(w, r)
}
Server结构体是应用的核心依赖容器,持有所有共享依赖并实现
http.Handler
接口。
go
type Server struct {
    db     *sql.DB
    logger *slog.Logger
    router *http.ServeMux
}

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)
    s.router.HandleFunc("GET /healthz", s.handleHealth)
}

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

Middleware Wrapping

中间件包装

Apply middleware at the
http.Server
level or per-route:
go
// Wrap entire server
httpServer := &http.Server{
    Addr:    ":8080",
    Handler: requestLogger(s),
}

// Or per-route
s.router.Handle("GET /api/admin/", adminOnly(http.HandlerFunc(s.handleAdmin)))
可以在
http.Server
级别或单个路由级别应用中间件:
go
// 包装整个服务器
httpServer := &http.Server{
    Addr:    ":8080",
    Handler: requestLogger(s),
}

// 或者针对单个路由
s.router.Handle("GET /api/admin/", adminOnly(http.HandlerFunc(s.handleAdmin)))

Middleware Signature

中间件签名

go
func requestLogger(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        slog.Info("request", "method", r.Method, "path", r.URL.Path, "dur", time.Since(start))
    })
}
go
func requestLogger(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        slog.Info("request", "method", r.Method, "path", r.URL.Path, "dur", time.Since(start))
    })
}

Project Structure

项目结构

Choose based on project size:
  • Flat structure -- single package, all files in root. Best for CLIs, small services, < ~10 handlers. See references/project-structure.md.
  • Modular/domain-driven --
    cmd/
    ,
    internal/
    with domain packages. For larger apps with multiple bounded contexts. See references/project-structure.md.
Start flat. Migrate when you see the signs described in the reference.
根据项目规模选择合适的结构:
  • 扁平结构 -- 单包,所有文件放在根目录。最适合CLI工具、小型服务、少于约10个处理器的项目。详见references/project-structure.md
  • 模块化/领域驱动结构 -- 包含
    cmd/
    internal/
    目录及领域包。适用于具有多个限界上下文的大型应用。详见references/project-structure.md
建议从扁平结构开始,当项目出现参考文档中描述的迁移信号时,再进行结构升级。

Graceful Shutdown

优雅关闭

Every production Go server needs graceful shutdown. The pattern uses
signal.NotifyContext
to listen for OS signals and
http.Server.Shutdown
to drain connections.
go
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
defer cancel()

// ... start server in goroutine ...

<-ctx.Done()

shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer shutdownCancel()
httpServer.Shutdown(shutdownCtx)
Full pattern with cleanup ordering in references/graceful-shutdown.md.
所有生产环境的Go服务器都需要实现优雅关闭。该模式使用
signal.NotifyContext
监听操作系统信号,并通过
http.Server.Shutdown
处理连接排空。
go
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
defer cancel()

// ... 在goroutine中启动服务器 ...

<-ctx.Done()

shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer shutdownCancel()
httpServer.Shutdown(shutdownCtx)
完整的包含清理顺序的实现模式详见references/graceful-shutdown.md

When to Load References

何时加载参考文档

Load project-structure.md when:
  • Scaffolding a new Go project
  • Discussing package layout or directory organization
  • The project is growing and needs restructuring
Load graceful-shutdown.md when:
  • Setting up a production HTTP server
  • Implementing signal handling or clean shutdown
  • Discussing deployment or container readiness
Load dependency-injection.md when:
  • Designing how services, stores, and handlers connect
  • Making code testable with interfaces
  • Reviewing constructor functions or wiring logic
当以下场景时加载project-structure.md
  • 搭建新的Go项目
  • 讨论包布局或目录组织
  • 项目规模增长需要重构
当以下场景时加载graceful-shutdown.md
  • 搭建生产环境HTTP服务器
  • 实现信号处理或优雅关闭
  • 讨论部署或容器就绪性
当以下场景时加载dependency-injection.md
  • 设计服务、存储和处理器之间的连接方式
  • 通过接口提升代码可测试性
  • 审查构造函数或组件装配逻辑

Anti-Patterns

反模式

Global database variables

全局数据库变量

go
// BAD -- untestable, hidden dependency
var db *sql.DB

func handleGetUser(w http.ResponseWriter, r *http.Request) {
    db.QueryRow(...)
}
Pass
db
through a Server or Service struct instead.
go
// 错误示例 -- 不可测试,依赖隐藏
var db *sql.DB

func handleGetUser(w http.ResponseWriter, r *http.Request) {
    db.QueryRow(...)
}
应通过Server或Service结构体传递
db
实例。

Framework-first thinking

优先使用框架的思维

Do not start with
gin.Default()
or
echo.New()
. Start with
http.NewServeMux()
. Only introduce a framework if you hit a real limitation of the stdlib that justifies the dependency.
不要从
gin.Default()
echo.New()
开始。先使用
http.NewServeMux()
。只有当标准库的限制确实影响到功能实现时,再引入框架依赖。

God packages

上帝包

A single
handlers
package with 50 files is not organization. Group by domain (
user
,
order
,
billing
), not by technical layer.
单个
handlers
包包含50个文件不属于合理的组织方式。应按领域(
user
order
billing
)分组,而非按技术层分组。

Using init() for setup

使用init()进行初始化

go
// BAD -- invisible side effects, untestable
func init() {
    db, _ = sql.Open("postgres", os.Getenv("DATABASE_URL"))
}
All initialization belongs in
main()
or a
run()
function so it can be tested and errors can be handled.
go
// 错误示例 -- 不可见的副作用,不可测试
func init() {
    db, _ = sql.Open("postgres", os.Getenv("DATABASE_URL"))
}
所有初始化逻辑应放在
main()
run()
函数中,以便进行测试和错误处理。

Reading config in business logic

在业务逻辑中读取配置

go
// BAD -- couples handler to environment
func (s *Server) handleSendEmail(w http.ResponseWriter, r *http.Request) {
    apiKey := os.Getenv("SENDGRID_API_KEY") // don't do this
}
Inject configuration values or clients through constructors.
go
// 错误示例 -- 处理器与环境强耦合
func (s *Server) handleSendEmail(w http.ResponseWriter, r *http.Request) {
    apiKey := os.Getenv("SENDGRID_API_KEY") // 不要这样做
}
应通过构造函数注入配置值或客户端实例。