go-architecture-review

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Go Architecture Review

Go 项目架构评审

Good architecture makes the next change easy. Bad architecture makes every change scary.
优秀的架构让下一次变更变得轻松,糟糕的架构则让每一次变更都令人胆战心惊。

1. Standard Project Layout

1. 标准项目布局

myproject/
├── cmd/                    # Main applications (one dir per binary)
│   ├── api-server/
│   │   └── main.go
│   └── worker/
│       └── main.go
├── internal/               # Private packages — cannot be imported externally
│   ├── domain/             # Core business types (entities, value objects)
│   │   ├── user.go
│   │   └── order.go
│   ├── service/            # Business logic (use cases)
│   │   ├── user.go
│   │   └── order.go
│   ├── store/              # Data access (repositories)
│   │   ├── postgres/
│   │   │   └── user.go
│   │   └── redis/
│   │       └── cache.go
│   ├── handler/            # HTTP/gRPC handlers (adapters)
│   │   └── user.go
│   └── config/             # Configuration loading
│       └── config.go
├── pkg/                    # Public packages (use sparingly)
│   └── httputil/
│       └── response.go
├── migrations/             # Database migrations
├── api/                    # API definitions (OpenAPI, proto files)
├── go.mod
├── go.sum
└── Makefile
myproject/
├── cmd/                    # Main applications (one dir per binary)
│   ├── api-server/
│   │   └── main.go
│   └── worker/
│       └── main.go
├── internal/               # Private packages — cannot be imported externally
│   ├── domain/             # Core business types (entities, value objects)
│   │   ├── user.go
│   │   └── order.go
│   ├── service/            # Business logic (use cases)
│   │   ├── user.go
│   │   └── order.go
│   ├── store/              # Data access (repositories)
│   │   ├── postgres/
│   │   │   └── user.go
│   │   └── redis/
│   │       └── cache.go
│   ├── handler/            # HTTP/gRPC handlers (adapters)
│   │   └── user.go
│   └── config/             # Configuration loading
│       └── config.go
├── pkg/                    # Public packages (use sparingly)
│   └── httputil/
│       └── response.go
├── migrations/             # Database migrations
├── api/                    # API definitions (OpenAPI, proto files)
├── go.mod
├── go.sum
└── Makefile

Key Rules:

核心规则:

  • internal/
    enforces encapsulation at the compiler level. Use it aggressively.
  • pkg/
    is for genuinely reusable packages. When in doubt, use
    internal/
    .
  • cmd/
    main packages should be thin — wire dependencies and call
    Run()
    .
  • One
    main.go
    per binary, minimal logic inside.
  • internal/
    在编译器层面强制实现封装,请积极使用。
  • pkg/
    仅用于真正可复用的包,若存疑,请使用
    internal/
  • cmd/
    下的主包应尽量精简——仅负责依赖注入并调用
    Run()
  • 每个二进制文件对应一个
    main.go
    ,内部仅包含最少逻辑。

2. Dependency Direction

2. 依赖方向

Dependencies MUST flow inward. Domain core has zero external dependencies:
handlers → services → domain ← stores
    ↓          ↓                  ↓
  (net/http)  (pure Go)     (database/sql)
Rules:
  • domain/
    imports NOTHING from the project. No
    store
    , no
    handler
    , no
    config
    .
  • service/
    depends on
    domain/
    types and interfaces, NOT on concrete stores.
  • handler/
    depends on
    service/
    interfaces.
  • store/
    implements interfaces defined in
    service/
    or
    domain/
    .
  • Circular dependencies are a 🔴 BLOCKER. The compiler catches them, but design should prevent them.
go
// ✅ Good — service defines the interface it needs
// internal/service/user.go
type UserStore interface {
    GetByID(ctx context.Context, id string) (*domain.User, error)
    Create(ctx context.Context, user *domain.User) error
}

type UserService struct {
    store UserStore // depends on interface, not postgres.Store
}

// internal/store/postgres/user.go
type Store struct { db *sql.DB }

// Implements service.UserStore without importing the service package
func (s *Store) GetByID(ctx context.Context, id string) (*domain.User, error) { ... }
依赖必须向内流动。领域核心层不应有任何外部依赖:
handlers → services → domain ← stores
    ↓          ↓                  ↓
  (net/http)  (pure Go)     (database/sql)
规则:
  • domain/
    不导入项目内的任何其他包,包括
    store
    handler
    config
  • service/
    依赖
    domain/
    的类型和接口,而非具体的存储实现。
  • handler/
    依赖
    service/
    的接口。
  • store/
    实现
    service/
    domain/
    中定义的接口。
  • 循环依赖是🔴 阻塞项,编译器会检测到它们,但设计阶段就应避免。
go
// ✅ Good — service defines the interface it needs
// internal/service/user.go
type UserStore interface {
    GetByID(ctx context.Context, id string) (*domain.User, error)
    Create(ctx context.Context, user *domain.User) error
}

type UserService struct {
    store UserStore // depends on interface, not postgres.Store
}

// internal/store/postgres/user.go
type Store struct { db *sql.DB }

// Implements service.UserStore without importing the service package
func (s *Store) GetByID(ctx context.Context, id string) (*domain.User, error) { ... }

3. Main Package Wiring

3. 主包依赖注入

main.go
is the composition root. Wire everything here:
go
func main() {
    cfg := config.Load()
    logger := zap.Must(zap.NewProduction())

    db, err := sql.Open("postgres", cfg.DatabaseURL)
    if err != nil {
        logger.Fatal("connect db", zap.Error(err))
    }
    defer db.Close()

    // Wire dependencies
    userStore := postgres.NewUserStore(db)
    userService := service.NewUserService(userStore)
    userHandler := handler.NewUserHandler(userService, logger)

    // Setup router
    r := chi.NewRouter()
    r.Mount("/api/v1/users", userHandler.Routes())

    // Run server
    srv := &http.Server{Addr: cfg.Addr, Handler: r}
    // ... graceful shutdown
}
Avoid dependency injection frameworks. Go's explicit wiring is a feature. If wiring gets complex, use Google's
wire
for compile-time DI code generation.
main.go
是组合根,所有依赖的注入都应在这里完成:
go
func main() {
    cfg := config.Load()
    logger := zap.Must(zap.NewProduction())

    db, err := sql.Open("postgres", cfg.DatabaseURL)
    if err != nil {
        logger.Fatal("connect db", zap.Error(err))
    }
    defer db.Close()

    // Wire dependencies
    userStore := postgres.NewUserStore(db)
    userService := service.NewUserService(userStore)
    userHandler := handler.NewUserHandler(userService, logger)

    // Setup router
    r := chi.NewRouter()
    r.Mount("/api/v1/users", userHandler.Routes())

    // Run server
    srv := &http.Server{Addr: cfg.Addr, Handler: r}
    // ... graceful shutdown
}
避免使用依赖注入框架,Go的显式注入是一项特性。 若注入逻辑变得复杂,可使用Google的
wire
进行编译期DI代码生成。

4. Package Design Principles

4. 包设计原则

One package = one purpose

一个包对应一个职责

go
// ✅ Good — clear purpose
package orderservice  // business rules for orders
package postgres      // PostgreSQL data access
package httphandler   // HTTP transport layer

// ❌ Bad — grab-bag packages
package utils    // what ISN'T a util?
package common   // everything and nothing
package models   // types without behavior
go
// ✅ Good — clear purpose
package orderservice  // business rules for orders
package postgres      // PostgreSQL data access
package httphandler   // HTTP transport layer

// ❌ Bad — grab-bag packages
package utils    // what ISN'T a util?
package common   // everything and nothing
package models   // types without behavior

Avoid package stuttering

避免包名重复

go
// ❌ Bad — package name repeated in type
package user
type UserService struct{} // user.UserService

// ✅ Good
package user
type Service struct{} // user.Service
go
// ❌ Bad — package name repeated in type
package user
type UserService struct{} // user.UserService

// ✅ Good
package user
type Service struct{} // user.Service

Package cohesion over size

包的内聚性优先于大小

A package with 20 related files is better than 20 packages with 1 file each. Split packages when they have distinct responsibilities, not when they get big.
一个包含20个相关文件的包,比20个各含1个文件的包更好。 当包的职责明显不同时再拆分,而非因为包变大就拆分。

5. Configuration

5. 配置管理

go
type Config struct {
    Addr        string        `env:"ADDR" envDefault:":8080"`
    DatabaseURL string        `env:"DATABASE_URL,required"`
    LogLevel    string        `env:"LOG_LEVEL" envDefault:"info"`
    Timeout     time.Duration `env:"TIMEOUT" envDefault:"30s"`
}
Rules:
  • All config from environment variables (12-factor).
  • Validate at startup, fail fast with clear messages.
  • No config scattered across packages — centralize in
    internal/config
    .
  • Never hardcode values. Not even "just for now."
go
type Config struct {
    Addr        string        `env:"ADDR" envDefault:":8080"`
    DatabaseURL string        `env:"DATABASE_URL,required"`
    LogLevel    string        `env:"LOG_LEVEL" envDefault:"info"`
    Timeout     time.Duration `env:"TIMEOUT" envDefault:"30s"`
}
规则:
  • 所有配置均来自环境变量(12要素原则)。
  • 在启动时验证配置,若不合法则快速失败并给出清晰提示。
  • 配置不要分散在各个包中——集中管理在
    internal/config
  • 绝对不要硬编码值,哪怕是“暂时的”也不行。

6. Init Functions

6. Init 函数

Avoid
init()
. It runs implicitly, makes testing harder, and creates hidden dependencies.
go
// ❌ Bad — hidden side effects
func init() {
    db, _ = sql.Open("postgres", os.Getenv("DB_URL"))
}

// ✅ Good — explicit initialization
func NewStore(dsn string) (*Store, error) {
    db, err := sql.Open("postgres", dsn)
    if err != nil {
        return nil, fmt.Errorf("open db: %w", err)
    }
    return &Store{db: db}, nil
}
Exception: registering drivers or codecs is acceptable in
init()
:
go
func init() {
    sql.Register("custom", &CustomDriver{})
}
避免使用
init()
,它会隐式执行,增加测试难度,并产生隐藏依赖。
go
// ❌ Bad — hidden side effects
func init() {
    db, _ = sql.Open("postgres", os.Getenv("DB_URL"))
}

// ✅ Good — explicit initialization
func NewStore(dsn string) (*Store, error) {
    db, err := sql.Open("postgres", dsn)
    if err != nil {
        return nil, fmt.Errorf("open db: %w", err)
    }
    return &Store{db: db}, nil
}
例外情况:注册驱动或编解码器时可以使用
init()
go
func init() {
    sql.Register("custom", &CustomDriver{})
}

Architecture Review Checklist

架构评审检查清单

  • 🔴 No circular dependencies between packages
  • 🔴 Domain types have zero infrastructure dependencies
  • 🔴 No business logic in
    cmd/
    main packages
  • 🔴 No
    init()
    with side effects (DB connections, HTTP calls)
  • 🟡
    internal/
    used for project-private packages
  • 🟡 Interfaces defined at the consumer, not the producer
  • 🟡 Configuration centralized and validated at startup
  • 🟡 Dependency direction flows inward (handlers → services → domain)
  • 🟢 Package names are short, singular, descriptive
  • 🟢 No
    utils/
    ,
    common/
    ,
    helpers/
    packages
  • 🟢 Main package is a thin composition root
  • 🔴 包之间无循环依赖
  • 🔴 领域类型无基础设施依赖
  • 🔴
    cmd/
    主包中无业务逻辑
  • 🔴 无带有副作用的
    init()
    (如数据库连接、HTTP调用)
  • 🟡 项目私有包使用
    internal/
  • 🟡 接口由消费者定义,而非生产者
  • 🟡 配置集中管理并在启动时验证
  • 🟡 依赖方向向内流动(handlers → services → domain)
  • 🟢 包名简短、单数且具有描述性
  • 🟢 无
    utils/
    common/
    helpers/
    这类包
  • 🟢 主包是精简的组合根