Golang架构目录设计与设计模式教程

Golang 架构目录设计与设计模式教程

本文档面向已掌握 Go 基础语法、正在做中小型服务或准备重构项目 的读者。

读完并完成文末练习后,你应该能够:为项目选择合适的目录结构、理解各层职责边界、在 Go 中落地常见设计模式,并避免过度设计。


目录

  1. 为什么目录结构很重要
  2. [Go 官方与社区约定](#Go 官方与社区约定)
  3. 五种常见目录布局
  4. 分层架构与依赖方向
  5. 设计模式总览
  6. 创建型模式
  7. 结构型模式
  8. 行为型模式
  9. [Go 特有惯用法](#Go 特有惯用法)
  10. 反模式与常见坑
  11. 动手练习
  12. 参考资源

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() ⭐⭐⭐⭐⭐
创建型 建造者 链式 BuilderFunctional Options ⭐⭐⭐⭐
创建型 单例 sync.Once ⭐⭐⭐
结构型 适配器 包装外部类型实现本地 interface ⭐⭐⭐⭐⭐
结构型 装饰器 嵌套 http.Handler、中间件 ⭐⭐⭐⭐⭐
结构型 代理 缓存代理、鉴权代理 ⭐⭐⭐
行为型 策略 interface + 多实现,运行时注入 ⭐⭐⭐⭐⭐
行为型 观察者 channelsync.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 首选)

标准库 grpcuber-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

要求:

  1. Book 实体含 ID、Title、Price
  2. 实现 POST /booksGET /books/{id}
  3. service 不 import database/sql

练习 2:Functional Options

HTTP Client 封装:

  • WithTimeout
  • WithRetry
  • WithHeader

默认超时 30s,重试 0 次。


练习 3:策略 + 依赖注入

实现 ShippingService

  • StandardShipping:固定 10 元
  • ExpressShipping:固定 25 元
  • 下单时根据用户选择注入不同策略

练习 4:缓存代理

BookRepositoryCachedBookRepo

  • 读走 Redis,写穿透删缓存
  • 单元测试用 mock Repository,不连真实 Redis

练习 5(进阶):Clean Architecture 迁移

把练习 1 的 book 域改成布局 B:

  1. Repository 接口移到 usecase/book/ports.go
  2. postgres.go 移到 adapter/repository/postgres/
  3. 确认 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)+ 工作队列模式。

相关推荐
省四收割者1 小时前
从硬件中断到分布式协程:全景解构高并发机制与 C / Golang 的巅峰对决
c++·分布式·嵌入式硬件·golang
pixcarp11 小时前
知识库系统的内容资产闭环怎么设计
服务器·数据库·后端·golang
workflower12 小时前
使用大语言模型处理用户需求
大数据·人工智能·设计模式·重构·动态规划
张忠琳14 小时前
【Go 1.26.4】Golang Select 深度解析
开发语言·后端·golang
提笔了无痕16 小时前
如何用Go实现整套RAG流程
开发语言·后端·golang
wlsh1516 小时前
Go 错误处理
golang
geovindu17 小时前
go: Generators Pattern
开发语言·后端·设计模式·golang·生成器模式
GuWenyue21 小时前
前端异步请求踩坑?3种方式搞定Ajax数据交互,从XHR到async/await
前端·javascript·设计模式
青春喂了后端1 天前
Go Sidecar Status 性能优化
开发语言·性能优化·golang