go-architecture-review
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseGo 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
└── Makefilemyproject/
├── 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
└── MakefileKey Rules:
核心规则:
- enforces encapsulation at the compiler level. Use it aggressively.
internal/ - is for genuinely reusable packages. When in doubt, use
pkg/.internal/ - main packages should be thin — wire dependencies and call
cmd/.Run() - One per binary, minimal logic inside.
main.go
- 在编译器层面强制实现封装,请积极使用。
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:
- imports NOTHING from the project. No
domain/, nostore, nohandler.config - depends on
service/types and interfaces, NOT on concrete stores.domain/ - depends on
handler/interfaces.service/ - implements interfaces defined in
store/orservice/.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.gogo
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 for compile-time DI code generation.
wiremain.gogo
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的 进行编译期DI代码生成。
wire4. 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 behaviorgo
// ✅ 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 behaviorAvoid package stuttering
避免包名重复
go
// ❌ Bad — package name repeated in type
package user
type UserService struct{} // user.UserService
// ✅ Good
package user
type Service struct{} // user.Servicego
// ❌ Bad — package name repeated in type
package user
type UserService struct{} // user.UserService
// ✅ Good
package user
type Service struct{} // user.ServicePackage 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 . It runs implicitly, makes testing harder, and creates hidden dependencies.
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
}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 main packages
cmd/ - 🔴 No with side effects (DB connections, HTTP calls)
init() - 🟡 used for project-private packages
internal/ - 🟡 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/packageshelpers/ - 🟢 Main package is a thin composition root
- 🔴 包之间无循环依赖
- 🔴 领域类型无基础设施依赖
- 🔴 主包中无业务逻辑
cmd/ - 🔴 无带有副作用的 (如数据库连接、HTTP调用)
init() - 🟡 项目私有包使用
internal/ - 🟡 接口由消费者定义,而非生产者
- 🟡 配置集中管理并在启动时验证
- 🟡 依赖方向向内流动(handlers → services → domain)
- 🟢 包名简短、单数且具有描述性
- 🟢 无 、
utils/、common/这类包helpers/ - 🟢 主包是精简的组合根