Golang 架构目录设计与设计模式教程
本文档面向已掌握 Go 基础语法、正在做中小型服务或准备重构项目 的读者。
读完并完成文末练习后,你应该能够:为项目选择合适的目录结构、理解各层职责边界、在 Go 中落地常见设计模式,并避免过度设计。
目录
- 为什么目录结构很重要
- [Go 官方与社区约定](#Go 官方与社区约定)
- 五种常见目录布局
- 分层架构与依赖方向
- 设计模式总览
- 创建型模式
- 结构型模式
- 行为型模式
- [Go 特有惯用法](#Go 特有惯用法)
- 反模式与常见坑
- 动手练习
- 参考资源
1. 为什么目录结构很重要
目录结构不是「好看」的问题,它直接决定:
| 维度 | 好结构带来的收益 |
|---|---|
| 可读性 | 新人 10 分钟知道代码在哪 |
| 可测试性 | 业务逻辑不绑 HTTP/DB,单测容易写 |
| 可替换性 | 换 MySQL → PostgreSQL 只动 repository 层 |
| 协作效率 | 多人改不同包,冲突少 |
| 演进成本 | 单体拆微服务时,边界已经画好 |
核心原则:
按「变化原因」分包,而不是按「技术名词」分包。
- ❌
controllers/、services/、daos/三层万能套 ------ 很快变成「上帝 Service」 - ✅
user/、order/按业务域分包,域内再分handler/service/repo
2. Go 官方与社区约定
2.1 标准项目骨架
Go 社区最广泛接受的是 Standard Go Project Layout(非官方标准,但是事实标准):
myapp/
├── cmd/ # 可执行程序入口(每个子目录一个 main)
│ ├── api/
│ │ └── main.go
│ └── worker/
│ └── main.go
├── internal/ # 私有代码,外部模块无法 import
│ ├── user/
│ ├── order/
│ └── platform/ # 跨域基础设施:db、cache、logger
├── pkg/ # 可被外部引用的公共库(慎用,能 internal 就 internal)
├── api/ # OpenAPI / protobuf 定义
├── configs/ # 配置模板
├── deployments/ # K8s / Docker / Helm
├── scripts/ # 构建、迁移脚本
├── test/ # 集成测试、testdata
├── go.mod
└── README.md
2.2 关键规则
| 目录 | 规则 |
|---|---|
cmd/ |
只有 main:解析参数、组装依赖、启动服务,不写业务 |
internal/ |
Go 编译器强制:外部项目 import 会报错 |
pkg/ |
真正要给别的仓库用的才放这里;大多数项目不需要 |
api/ |
.proto、OpenAPI YAML,与实现分离 |
2.3 internal 的嵌套技巧
internal/
├── user/
│ ├── handler.go # HTTP/gRPC 适配
│ ├── service.go # 业务逻辑
│ ├── repository.go # 数据访问
│ └── model.go # 领域模型
└── platform/
├── database/
├── middleware/
└── config/
依赖方向(永远向内):
cmd → handler → service → repository → database
↓
model(被各层引用,但不依赖任何层)
3. 五种常见目录布局
3.1 布局 A:按业务域分包(推荐,中小型服务)
适合:电商、SaaS、后台 API,团队 3~15 人。
internal/
├── user/
│ ├── handler/
│ │ └── http.go
│ ├── service/
│ │ └── user.go
│ ├── repository/
│ │ └── postgres.go
│ └── domain/
│ └── user.go
├── order/
│ └── ...
└── app/
├── router.go # 注册路由
└── wire.go # 依赖注入(可选)
优点 :边界清晰,拆微服务时几乎按目录拆。
缺点:跨域逻辑(如「下单扣库存」)需要显式定义接口。
3.2 布局 B:Clean Architecture / 洋葱架构
适合:业务规则复杂、需要长期演进的核心系统。
internal/
├── domain/ # 最内层:实体、值对象、领域错误
│ ├── user.go
│ └── order.go
├── usecase/ # 应用层:用例、编排
│ ├── user/
│ │ ├── create.go
│ │ └── ports.go # 接口定义(Repository、Notifier)
│ └── order/
├── adapter/ # 外层适配器
│ ├── http/
│ ├── grpc/
│ ├── repository/
│ │ └── postgres/
│ └── mq/
└── app/
└── main.go
依赖规则(Clean Architecture 铁律):
#mermaid-svg-vaXLjGV5rRbQq45c{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-vaXLjGV5rRbQq45c .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vaXLjGV5rRbQq45c .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vaXLjGV5rRbQq45c .error-icon{fill:#552222;}#mermaid-svg-vaXLjGV5rRbQq45c .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vaXLjGV5rRbQq45c .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vaXLjGV5rRbQq45c .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vaXLjGV5rRbQq45c .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vaXLjGV5rRbQq45c .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vaXLjGV5rRbQq45c .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vaXLjGV5rRbQq45c .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vaXLjGV5rRbQq45c .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vaXLjGV5rRbQq45c .marker.cross{stroke:#333333;}#mermaid-svg-vaXLjGV5rRbQq45c svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vaXLjGV5rRbQq45c p{margin:0;}#mermaid-svg-vaXLjGV5rRbQq45c .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-vaXLjGV5rRbQq45c .cluster-label text{fill:#333;}#mermaid-svg-vaXLjGV5rRbQq45c .cluster-label span{color:#333;}#mermaid-svg-vaXLjGV5rRbQq45c .cluster-label span p{background-color:transparent;}#mermaid-svg-vaXLjGV5rRbQq45c .label text,#mermaid-svg-vaXLjGV5rRbQq45c span{fill:#333;color:#333;}#mermaid-svg-vaXLjGV5rRbQq45c .node rect,#mermaid-svg-vaXLjGV5rRbQq45c .node circle,#mermaid-svg-vaXLjGV5rRbQq45c .node ellipse,#mermaid-svg-vaXLjGV5rRbQq45c .node polygon,#mermaid-svg-vaXLjGV5rRbQq45c .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vaXLjGV5rRbQq45c .rough-node .label text,#mermaid-svg-vaXLjGV5rRbQq45c .node .label text,#mermaid-svg-vaXLjGV5rRbQq45c .image-shape .label,#mermaid-svg-vaXLjGV5rRbQq45c .icon-shape .label{text-anchor:middle;}#mermaid-svg-vaXLjGV5rRbQq45c .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-vaXLjGV5rRbQq45c .rough-node .label,#mermaid-svg-vaXLjGV5rRbQq45c .node .label,#mermaid-svg-vaXLjGV5rRbQq45c .image-shape .label,#mermaid-svg-vaXLjGV5rRbQq45c .icon-shape .label{text-align:center;}#mermaid-svg-vaXLjGV5rRbQq45c .node.clickable{cursor:pointer;}#mermaid-svg-vaXLjGV5rRbQq45c .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-vaXLjGV5rRbQq45c .arrowheadPath{fill:#333333;}#mermaid-svg-vaXLjGV5rRbQq45c .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-vaXLjGV5rRbQq45c .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-vaXLjGV5rRbQq45c .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vaXLjGV5rRbQq45c .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-vaXLjGV5rRbQq45c .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vaXLjGV5rRbQq45c .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-vaXLjGV5rRbQq45c .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-vaXLjGV5rRbQq45c .cluster text{fill:#333;}#mermaid-svg-vaXLjGV5rRbQq45c .cluster span{color:#333;}#mermaid-svg-vaXLjGV5rRbQq45c div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-vaXLjGV5rRbQq45c .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-vaXLjGV5rRbQq45c rect.text{fill:none;stroke-width:0;}#mermaid-svg-vaXLjGV5rRbQq45c .icon-shape,#mermaid-svg-vaXLjGV5rRbQq45c .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vaXLjGV5rRbQq45c .icon-shape p,#mermaid-svg-vaXLjGV5rRbQq45c .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-vaXLjGV5rRbQq45c .icon-shape .label rect,#mermaid-svg-vaXLjGV5rRbQq45c .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vaXLjGV5rRbQq45c .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-vaXLjGV5rRbQq45c .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-vaXLjGV5rRbQq45c :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Domain
Usecase
外层 Adapter
interface
implements
HTTP Handler
Postgres Repo
CreateUser
User Entity
domain不依赖任何外层usecase只依赖domain+ 自己定义的port接口adapter实现port,依赖usecase
3.3 布局 C:DDD 战术结构
适合:领域概念多、有聚合根/限界上下文的中大型系统。
internal/
├── boundedcontext/
│ ├── billing/
│ │ ├── aggregate/
│ │ ├── entity/
│ │ ├── valueobject/
│ │ ├── repository/
│ │ ├── service/
│ │ └── event/
│ └── inventory/
│ └── ...
├── shared/
│ └── kernel/ # 共享内核:Money、Email 等
└── infrastructure/
├── persistence/
└── messaging/
何时用 DDD 文件夹名 :团队真在实践 DDD 时;否则用「布局 A」的 domain/ 即可,避免名词堆砌。
3.4 布局 D:微服务 Monorepo
适合:多个服务同仓库、共享库、统一 CI。
├── services/
│ ├── user-api/
│ │ ├── cmd/
│ │ └── internal/
│ ├── order-api/
│ │ └── ...
│ └── notification-worker/
│ └── ...
├── libs/ # 共享 pkg(日志、tracing、errors)
│ ├── httputil/
│ └── trace/
├── proto/ # 跨服务 protobuf
└── deployments/
每个 services/* 内部仍用布局 A 或 B。
3.5 布局 E:K8s Controller / Operator
适合:Informer、Reconciler、Webhook 类项目(与 client-go 生态一致)。
├── cmd/
│ └── manager/
│ └── main.go
├── api/
│ └── v1alpha1/ # CRD 类型定义
├── internal/
│ ├── controller/ # Reconcile 逻辑
│ ├── webhook/
│ └── indexer/
├── config/
│ ├── crd/
│ ├── rbac/
│ └── manager/
└── hack/
3.6 选型速查表
| 场景 | 推荐布局 |
|---|---|
| CRUD API、管理后台 | A 按业务域 |
| 复杂业务规则、多数据源 | B Clean Architecture |
| 多限界上下文、事件驱动 | C DDD |
| 5+ 微服务同仓 | D Monorepo |
| K8s Operator | E Controller 标准 |
4. 分层架构与依赖方向
4.1 经典四层
┌─────────────────────────────────────┐
│ Delivery(HTTP / gRPC / CLI) │ ← 解析请求、返回 DTO
├─────────────────────────────────────┤
│ Service / Usecase(业务编排) │ ← 事务、权限、流程
├─────────────────────────────────────┤
│ Repository(持久化抽象) │ ← interface 定义在此
├─────────────────────────────────────┤
│ Infrastructure(DB / Cache / MQ) │ ← interface 实现
└─────────────────────────────────────┘
4.2 完整示例:用户注册
domain/user.go --- 纯领域,无第三方 import:
go
package domain
import "time"
type User struct {
ID int64
Email string
Password string // 存储哈希后的值
CreatedAt time.Time
}
var ErrEmailExists = errors.New("email already exists")
repository 接口 --- 由 service 定义或单独 ports.go:
go
package user
import "myapp/internal/user/domain"
type Repository interface {
Create(ctx context.Context, u *domain.User) error
FindByEmail(ctx context.Context, email string) (*domain.User, error)
}
service/user.go --- 业务逻辑:
go
package service
type Service struct {
repo Repository
}
func (s *Service) Register(ctx context.Context, email, plainPassword string) (*domain.User, error) {
if _, err := s.repo.FindByEmail(ctx, email); err == nil {
return nil, domain.ErrEmailExists
}
hash, err := bcrypt.GenerateFromPassword([]byte(plainPassword), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
u := &domain.User{Email: email, Password: string(hash), CreatedAt: time.Now()}
if err := s.repo.Create(ctx, u); err != nil {
return nil, err
}
return u, nil
}
handler/http.go --- 只负责 HTTP 协议:
go
func (h *Handler) Register(w http.ResponseWriter, r *http.Request) {
var req registerRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, err)
return
}
user, err := h.svc.Register(r.Context(), req.Email, req.Password)
if errors.Is(err, domain.ErrEmailExists) {
writeError(w, http.StatusConflict, err)
return
}
// ...
}
cmd/api/main.go --- 组装依赖:
go
func main() {
db := database.Connect(cfg.DSN)
repo := postgres.NewUserRepo(db)
svc := service.NewUserService(repo)
handler := http.NewUserHandler(svc)
mux := http.NewServeMux()
mux.HandleFunc("POST /api/users", handler.Register)
http.ListenAndServe(":8080", mux)
}
5. 设计模式总览
Go 没有类的继承,很多 Gang of Four 模式在 Go 里会「变形」------ 用 组合 + interface + 函数 实现。
| 类型 | 模式 | Go 中的典型实现 | 使用频率 |
|---|---|---|---|
| 创建型 | 工厂 / 简单工厂 | 构造函数 NewT() |
⭐⭐⭐⭐⭐ |
| 创建型 | 建造者 | 链式 Builder 或 Functional Options |
⭐⭐⭐⭐ |
| 创建型 | 单例 | sync.Once |
⭐⭐⭐ |
| 结构型 | 适配器 | 包装外部类型实现本地 interface | ⭐⭐⭐⭐⭐ |
| 结构型 | 装饰器 | 嵌套 http.Handler、中间件 |
⭐⭐⭐⭐⭐ |
| 结构型 | 代理 | 缓存代理、鉴权代理 | ⭐⭐⭐ |
| 行为型 | 策略 | interface + 多实现,运行时注入 |
⭐⭐⭐⭐⭐ |
| 行为型 | 观察者 | channel、sync.Cond、事件总线 |
⭐⭐⭐⭐ |
| 行为型 | 模板方法 | 未导出骨架函数 + 可覆盖的函数参数 | ⭐⭐⭐ |
| 行为型 | 责任链 | middleware 链 |
⭐⭐⭐⭐⭐ |
| --- | 依赖注入 | 构造函数注入(非 DI 框架) | ⭐⭐⭐⭐⭐ |
| --- | Repository | 持久化抽象 | ⭐⭐⭐⭐⭐ |
下面按类型给出可运行的简化示例。
6. 创建型模式
6.1 简单工厂(Constructor)
Go 惯用 NewXxx 代替 new 关键字,隐藏创建细节。
go
package logger
type Logger interface {
Info(msg string, keysAndValues ...any)
Error(msg string, keysAndValues ...any)
}
// 工厂:根据配置返回不同实现
func NewLogger(level string) Logger {
switch level {
case "json":
return newJSONLogger()
case "text":
return newTextLogger()
default:
return newNopLogger()
}
}
项目中的位置 :internal/platform/logger/factory.go
6.2 建造者模式 + Functional Options(Go 首选)
标准库 grpc、uber-go/zap 都用 Options 模式,比 Java 式 Builder 更地道。
go
package server
type Server struct {
addr string
timeout time.Duration
logger logger.Logger
}
type Option func(*Server)
func WithTimeout(d time.Duration) Option {
return func(s *Server) { s.timeout = d }
}
func WithLogger(l logger.Logger) Option {
return func(s *Server) { s.logger = l }
}
func New(addr string, opts ...Option) *Server {
s := &Server{
addr: addr,
timeout: 30 * time.Second,
logger: logger.NewNop(),
}
for _, opt := range opts {
opt(s)
}
return s
}
// 使用
srv := server.New(":8080",
server.WithTimeout(10*time.Second),
server.WithLogger(log),
)
要点:
- 默认值在
New里设好 - 每个
Option只改一个字段 - 对外只暴露
New+WithXxx,不暴露裸结构体字段
6.3 单例(sync.Once)
go
package database
var (
instance *sql.DB
once sync.Once
initErr error
)
func GetInstance(dsn string) (*sql.DB, error) {
once.Do(func() {
instance, initErr = sql.Open("postgres", dsn)
if initErr == nil {
initErr = instance.Ping()
}
})
return instance, initErr
}
注意 :单例不利于测试;更推荐在 main 里创建一次,通过参数注入。
7. 结构型模式
7.1 适配器(Adapter)
把第三方库适配成你的 domain 接口 ------ Go 项目里最常见的模式之一。
go
// 你的业务接口
type PaymentGateway interface {
Pay(ctx context.Context, orderID string, amount int64) error
}
// 第三方 Stripe SDK 类型
type stripeClient struct { /* ... */ }
// 适配器
type StripeAdapter struct {
client *stripeClient
}
func (a *StripeAdapter) Pay(ctx context.Context, orderID string, amount int64) error {
// 调用 Stripe API,转换错误类型
return a.client.Charge(orderID, amount)
}
目录建议 :internal/adapter/payment/stripe.go
7.2 装饰器(Decorator)--- 中间件
http.Handler 装饰是 Go 标准写法:
go
func LoggingMiddleware(log logger.Logger, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Info("request",
"method", r.Method,
"path", r.URL.Path,
"duration", time.Since(start),
)
})
}
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
// 组装:Auth 在外层,Logging 在内层
handler := LoggingMiddleware(log, AuthMiddleware(mux))
7.3 代理(Proxy)--- 缓存示例
go
type UserRepository interface {
GetByID(ctx context.Context, id int64) (*User, error)
}
// 缓存代理:实现同一接口,内部委托真实 Repo
type CachedUserRepo struct {
inner UserRepository
cache *redis.Client
ttl time.Duration
}
func (c *CachedUserRepo) GetByID(ctx context.Context, id int64) (*User, error) {
key := fmt.Sprintf("user:%d", id)
if data, err := c.cache.Get(ctx, key).Bytes(); err == nil {
var u User
if json.Unmarshal(data, &u) == nil {
return &u, nil
}
}
u, err := c.inner.GetByID(ctx, id)
if err != nil {
return nil, err
}
if b, err := json.Marshal(u); err == nil {
c.cache.Set(ctx, key, b, c.ttl)
}
return u, nil
}
Service 层只依赖 UserRepository 接口,测试时注入 mock,生产环境注入 CachedUserRepo。
8. 行为型模式
8.1 策略(Strategy)
go
type PricingStrategy interface {
Calculate(price int64) int64
}
type RegularPricing struct{}
func (RegularPricing) Calculate(price int64) int64 { return price }
type VIPPricing struct{}
func (VIPPricing) Calculate(price int64) int64 { return price * 90 / 100 }
type OrderService struct {
pricing PricingStrategy
}
func (s *OrderService) Checkout(price int64) int64 {
return s.pricing.Calculate(price)
}
// main 里按用户类型注入不同策略
orderSvc := &OrderService{pricing: VIPPricing{}}
也可以用函数类型简化(Go 1.18+ 常见):
go
type PricingFunc func(price int64) int64
func (fn PricingFunc) Calculate(price int64) int64 { return fn(price) }
// 使用
svc := &OrderService{pricing: PricingFunc(func(p int64) int64 { return p * 80 / 100 })}
8.2 观察者(Observer)--- 事件总线
go
type Event struct {
Name string
Payload any
}
type Subscriber func(Event)
type EventBus struct {
mu sync.RWMutex
subscribers map[string][]Subscriber
}
func NewEventBus() *EventBus {
return &EventBus{subscribers: make(map[string][]Subscriber)}
}
func (b *EventBus) Subscribe(eventName string, fn Subscriber) {
b.mu.Lock()
defer b.mu.Unlock()
b.subscribers[eventName] = append(b.subscribers[eventName], fn)
}
func (b *EventBus) Publish(e Event) {
b.mu.RLock()
subs := b.subscribers[e.Name]
b.mu.RUnlock()
for _, fn := range subs {
fn(e) // 生产环境建议 goroutine + recover
}
}
// 用法:订单创建后通知积分、邮件、库存
bus.Subscribe("order.created", func(e Event) { /* send email */ })
bus.Publish(Event{Name: "order.created", Payload: order})
更重的场景 :用 Kafka / NATS;域内用 EventBus 解耦即可。
8.3 责任链(Chain of Responsibility)--- 校验链
go
type Validator func(ctx context.Context, req *CreateOrderRequest) error
func Chain(validators ...Validator) Validator {
return func(ctx context.Context, req *CreateOrderRequest) error {
for _, v := range validators {
if err := v(ctx, req); err != nil {
return err
}
}
return nil
}
}
var validateOrder = Chain(
validateNonEmptyItems,
validateStock,
validatePaymentMethod,
)
func (s *Service) CreateOrder(ctx context.Context, req *CreateOrderRequest) error {
if err := validateOrder(ctx, req); err != nil {
return err
}
// ...
}
8.4 模板方法 --- 用函数参数代替继承
Go 没有继承,用回调 或未导出流程函数实现:
go
type Exporter struct {
Fetch func(ctx context.Context) ([]Row, error)
Transform func(rows []Row) ([]byte, error)
Save func(ctx context.Context, data []byte) error
}
func (e *Exporter) Run(ctx context.Context) error {
rows, err := e.Fetch(ctx)
if err != nil {
return err
}
data, err := e.Transform(rows)
if err != nil {
return err
}
return e.Save(ctx, data)
}
// CSV 导出
csvExporter := &Exporter{
Fetch: fetchUsersFromDB,
Transform: rowsToCSV,
Save: saveToS3,
}
9. Go 特有惯用法
9.1 依赖注入(Constructor Injection)
推荐 :显式构造函数,不用反射容器(除非大型项目用 wire / fx)。
go
// 集中组装 --- internal/app/app.go
type App struct {
UserHandler *user.Handler
DB *sql.DB
}
func NewApp(cfg Config) (*App, error) {
db, err := database.Open(cfg.DSN)
if err != nil {
return nil, err
}
userRepo := postgres.NewUserRepo(db)
userSvc := user.NewService(userRepo)
userHandler := user.NewHandler(userSvc)
return &App{UserHandler: userHandler, DB: db}, nil
}
Google Wire(编译期 DI,无反射):
go
//go:build wireinject
func InitializeApp(cfg Config) (*App, error) {
wire.Build(
database.Open,
postgres.NewUserRepo,
user.NewService,
user.NewHandler,
wire.Struct(new(App), "*"),
)
return nil, nil
}
9.2 Repository 模式
go
// internal/order/repository.go
type Repository interface {
Save(ctx context.Context, o *domain.Order) error
FindByID(ctx context.Context, id string) (*domain.Order, error)
ListByUser(ctx context.Context, userID int64, page, size int) ([]*domain.Order, error)
}
// internal/order/repository/postgres.go
type PostgresRepo struct { db *sql.DB }
func (r *PostgresRepo) Save(ctx context.Context, o *domain.Order) error {
_, err := r.db.ExecContext(ctx,
`INSERT INTO orders (id, user_id, amount) VALUES ($1, $2, $3)`,
o.ID, o.UserID, o.Amount,
)
return err
}
测试 :repository/memory.go 内存实现,集成测试用 testcontainers。
9.3 错误处理模式 --- 哨兵错误 + 包装
go
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
)
func (r *PostgresRepo) FindByID(ctx context.Context, id string) (*Order, error) {
// ...
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("order %s: %w", id, ErrNotFound)
}
return nil, err
}
// handler 层
if errors.Is(err, order.ErrNotFound) {
writeError(w, http.StatusNotFound, err)
}
9.4 Context 传递模式
go
// 不要把 context 存在 struct 里
type Service struct { repo Repository } // ✅
func (s *Service) GetUser(ctx context.Context, id int64) (*User, error) {
return s.repo.FindByID(ctx, id) // ctx 作为第一个参数
}
10. 反模式与常见坑
| 反模式 | 问题 | 建议 |
|---|---|---|
所有逻辑写在 main.go |
无法测试、无法复用 | 业务进 internal/ |
巨型 util 包 |
垃圾桶,循环依赖 | 按职责拆 platform/ |
interface{} 满天飞 |
丢失类型安全 | 用泛型或具体类型 |
| 过度抽象 | 3 个实现才需要 interface | 接口由消费方定义 |
pkg/ 塞满业务代码 |
误暴露内部 API | 默认 internal |
Service 直接 *sql.DB |
换存储要改业务 | Repository 接口 |
| 全局变量配置 | 测试污染 | main 注入 |
| 每个实体一个「Manager」 | 上帝类 | 按用例拆 Service 方法 |
接口定义位置(Go 铁律)
go
// ✅ 消费者定义小接口(package service)
type UserReader interface {
FindByID(ctx context.Context, id int64) (*User, error)
}
// ❌ 生产者定义巨大接口(package repository)
type Repository interface {
// 20 个方法...
}
11. 动手练习
练习 1:搭建目录骨架
用 布局 A 创建一个 bookstore 项目:
bookstore/
├── cmd/api/main.go
└── internal/
├── book/
│ ├── domain/book.go
│ ├── service/book.go
│ ├── repository/postgres.go
│ └── handler/http.go
└── platform/database/postgres.go
要求:
Book实体含ID、Title、Price- 实现
POST /books、GET /books/{id} service不 importdatabase/sql
练习 2:Functional Options
为 HTTP Client 封装:
WithTimeoutWithRetryWithHeader
默认超时 30s,重试 0 次。
练习 3:策略 + 依赖注入
实现 ShippingService:
StandardShipping:固定 10 元ExpressShipping:固定 25 元- 下单时根据用户选择注入不同策略
练习 4:缓存代理
给 BookRepository 加 CachedBookRepo:
- 读走 Redis,写穿透删缓存
- 单元测试用 mock
Repository,不连真实 Redis
练习 5(进阶):Clean Architecture 迁移
把练习 1 的 book 域改成布局 B:
- 把
Repository接口移到usecase/book/ports.go postgres.go移到adapter/repository/postgres/- 确认
domain包零外部依赖(go list -deps自检)
12. 参考资源
| 资源 | 说明 |
|---|---|
| Standard Go Project Layout | 社区目录约定 |
| Go Code Review Comments | 官方代码审查建议 |
| Effective Go | 语言惯用法 |
| Google Wire | 编译期依赖注入 |
| Clean Architecture (Uncle Bob) | 分层思想原文 |
| Domain-Driven Design Reference | DDD 概念参考 |
| Go 项目布局:标准布局 | 中文说明 |
附录:模式与目录对照速查
创建型
NewXxx() → platform/*/factory.go
Functional Options → 各模块 New() 旁 options.go
sync.Once → platform/database(慎用)
结构型
Adapter → internal/adapter/<第三方>/
Decorator → platform/middleware/
Proxy → repository/cache_*.go
行为型
Strategy → service/ 注入 interface
Observer → platform/event/ 或 域内 event/
Chain → middleware/ 或 validator/
Template → 批处理 job 包
惯用法
Repository → <domain>/repository/
DI → cmd/main.go 或 app/wire.go
Sentinel Errors → domain/errors.go
文档版本:v1.0 | 可与《client-go 源码学习教程》配合阅读:Operator 项目用布局 E + 观察者(Informer)+ 工作队列模式。