Announcement

👇Official Account👇

Welcome to join the group & private message

Article first/tail QR code

Skip to content

接口参数设计:多场景复用时别把自己搞死

前几天 Code Review,看到一个接口有 15 个参数,当场就绷不住了。这事得从头说起。

一个接口是怎么变成屎山的

120天,从清爽到崩溃

订单系统的接口已经没法维护了。但翻 Git 记录发现,最初这玩意儿写得挺干净:

Day 1 - 初版设计(清晰明了)

go
// 简洁、职责单一
func GetUserOrders(ctx context.Context, userID string, page, pageSize int) ([]*Order, error) {
    return orderRepo.FindByUserID(ctx, userID, page, pageSize)
}

这个接口工作得很好,直到产品经理提出第二个需求。

Day 30 - 管理后台需要查询所有订单

"能不能让这个接口也支持管理员查所有订单?加个参数就行。"

go
func GetOrders(ctx context.Context, userID string, page, pageSize int, isAdmin bool) ([]*Order, error) {
    if isAdmin {
        return orderRepo.FindAll(ctx, page, pageSize)
    }
    return orderRepo.FindByUserID(ctx, userID, page, pageSize)
}

Day 60 - 需要支持订单状态筛选

go
func GetOrders(ctx context.Context, userID string, page, pageSize int, 
    isAdmin bool, status string, startDate, endDate time.Time) ([]*Order, error) {
    // if/else 逻辑开始膨胀...
}

Day 90 - 财务部门要导出报表

go
func GetOrders(ctx context.Context, 
    userID string,
    page, pageSize int,
    isAdmin bool,
    status string,
    startDate, endDate time.Time,
    includeDeleted bool,      // 财务要看已删除订单
    includeRefund bool,        // 包含退款信息
    exportFormat string,       // 导出格式
    detailLevel string,        // 详情级别
) ([]*Order, error) {
    // 大量的 if/else 判断
    if exportFormat != "" {
        if isAdmin {
            if includeDeleted {
                // ...
            }
        }
    }
    // 代码已经不堪重负
}

Day 120 - 当前状态(谁都看不懂)

go
func GetOrders(ctx context.Context,
    userID string,
    page, pageSize *int,        // 导出时不需要分页
    isAdmin bool,
    status []string,            // 现在支持多状态
    startDate, endDate *time.Time,
    includeDeleted bool,
    includeRefund bool,
    exportFormat string,
    detailLevel string,
    source string,              // 'web' | 'app' | 'admin' | 'export'
    sortBy string,
    sortOrder string,
    merchantID string,          // 又来了一个新需求...
) (interface{}, error) {
    // 300+ 行的 if/else 逻辑
    // 每个新需求都是一次"外科手术"
    // 改一处,测三处,崩五处
}

看到没?典型的"温水煮青蛙"。每次改动单独看都没问题,但堆一起就是屎山。

为什么会变成这样

说白了就三个问题:

  1. 一个接口干三件事:用户查订单、管理员查订单、财务导数据,硬塞一个接口里
  2. 调用方靠猜:参数组合有几十种,文档?不存在的,都是口口相传
  3. 代码全是 if/elseif source == "admin" && exportFormat != "" 这种判断到处都是,改一个地方要测五个场景

你的接口中招了吗

参数爆炸

症状:参数超过 7 个,而且还在涨

这种接口调用起来是真的难受:

  • 每次调用都要翻文档(如果有的话)
  • IDE 智能提示?一屏都显示不下
  • 写单元测试要构造一堆 mock 数据,烦死了

举个例子:

go
// 见过最离谱的,18个参数
func ProcessOrder(
    orderID, userID, merchantID, status string, 
    amount float64, paymentMethod, shippingAddress, 
    billingAddress, couponCode, giftMessage, 
    priority, source, requestID string,
    timestamp time.Time, signature string,
    // 还有三个没写...
) error {
    // 光参数校验就 50 行
}

互斥参数

症状:A 和 B 不能同时传,但类型系统管不了

go
type QueryParams struct {
    UserID     string  // 查询用户订单
    MerchantID string  // 查询商户订单
    // ⚠️ 这两个参数不能同时传,但类型系统无法约束
}

// 调用方只能靠文档和经验
GetOrders(ctx, QueryParams{
    UserID:     "123",
    MerchantID: "456",  // 到底查啥?
})

问题是编译器管不了这事儿,只能在文档里写"注意:UserID 和 MerchantID 二选一"。然后新来的同学就会写出:

go
GetOrders(ctx, QueryParams{
    UserID:     "123",
    MerchantID: "456",  // ???
})

线上直接炸。

幽灵参数

症状:文档和代码对不上

go
// Status 订单状态(必填)
func GetOrders(ctx context.Context, status string) ([]*Order, error) {
    // 实际代码里有默认值
    if status == "" {
        status = "all"
    }
    // ...
}

典型场景:最开始 status 是必传的,后来产品说"全部订单也要支持",于是改成可选。文档?哪有时间改文档。

三个月后新同学入职,看文档说必传,传了个空字符串,结果查出来一堆脏数据。

场景嗅探

症状:靠参数组合猜用户想干啥

go
func GetOrders(ctx context.Context, params QueryParams) ([]*Order, error) {
    // 通过参数组合判断场景
    if params.ExportFormat != "" && params.IncludeDeleted {
        // 猜测这是财务导出
        return s.exportForFinance(ctx, params)
    } else if params.IsAdmin && params.UserID == "" {
        // 猜测这是管理后台
        return s.queryForAdmin(ctx, params)
    } else if params.Source == "app" && params.UserID != "" {
        // 猜测这是移动端
        return s.queryForMobile(ctx, params)
    }
    // 这是代码的"味道"(Code Smell)
}

这代码维护起来是真的折磨:

  • 每加一个场景,if/else 就多一层
  • 测试?参数组合有几十种,谁测得过来
  • 新人看代码直接懵逼

为什么会这样

"打补丁"心态

经典对话:

PM: "能不能支持 XX 功能?"
我: "加个参数,三分钟搞定。"

PM: "再加个 YY 功能吧。"
我: "行,再加个参数。"

PM: "还有 ZZ 功能..."
我: "我 TM... 算了,重构吧。"(三天才能搞定)

问题在哪?我们把"快速响应需求"等同于"快速改代码"了。短期爽,长期死。

把接口当工具箱

很多人(包括以前的我)觉得接口应该"功能强大、灵活多变"。错了。

接口是契约,不是工具箱:

  • 契约:明确承诺"我能做什么",调用方一看就懂
  • 工具箱:啥都能干,但调用方要研究半天

举个例子:

  • 契约:GetUserOrders(userID) - 一看就知道干啥
  • 工具箱:GetOrders(type, id, mode, flags...) - 这 TM 要传啥
契约思维工具箱思维
接口对外承诺一个明确的能力接口对外暴露一堆能力供挑选
调用方知道"我该怎么用"调用方需要"研究怎么用"
改动需要版本管理改动就是加参数
文档是合同文档?啥文档

复用的误区

"这两个场景都要查订单,肯定要复用接口啊!"

Stop!

代码复用应该发生在底层抽象,不是接口层。

go
// ❌ 错误的复用:接口层强行复用
func GetOrders(userType string, ...) ([]*Order, error) {
    // userType: "customer" | "merchant" | "admin"
}

// ✅ 正确的复用:接口层分离,底层共享
func GetCustomerOrders(...) ([]*Order, error) { 
    return orderService.Query(...) 
}

func GetMerchantOrders(...) ([]*Order, error) { 
    return orderService.Query(...) 
}

func GetAdminOrders(...) ([]*Order, error) { 
    return orderService.Query(...) 
}
// orderService.Query 是真正的可复用逻辑

怎么设计才不坑

接口是承诺,不是可能性

记住一句话:接口是"我保证能做到",不是"我可能支持"。

Postel 法则别乱用

Be conservative in what you send, be liberal in what you accept.

这句话经常被误解成"接口要啥都能接受"。错了:

错误理解

go
// 误用:接收一切,内部判断一切
func GetOrders(ctx context.Context, params map[string]interface{}) ([]*Order, error) {
    // 接收任何参数,内部自己判断
}

正确理解

go
// 对外:契约明确
type UserOrderQuery struct {
    UserID   string  // 必传
    Page     int     // 可选,有默认值
    PageSize int     // 可选,有默认值
}

// 对内:宽容处理
func GetUserOrders(ctx context.Context, query UserOrderQuery) ([]*Order, error) {
    if query.Page == 0 {
        query.Page = 1  // 内部给默认值
    }
    if query.PageSize == 0 {
        query.PageSize = 20
    }
    // ...
}

向后兼容 vs 向前设计

向后兼容:不破坏已有调用方 向前设计:提前考虑未来场景

go
// 不好的向前设计:过度预测
type OrderQuery struct {
    Filters []struct {
        Field    string
        Operator string  // "eq", "gt", "lt", "like", ...
        Value    interface{}
    }  // 提前设计了复杂查询
}

// 好的向前设计:可扩展但不过度
type OrderQuery struct {
    Status    []OrderStatus
    DateRange *DateRange
    // 如果未来需要复杂查询,再加 Filters 字段
}

几个原则

单一职责(SRP)

一个接口只做一件事:

go
// ❌ 违反 SRP:查询 + 导出 + 统计
func GetOrders(query OrderQuery, exportFormat string, calculateStats bool) (interface{}, error)

// ✅ 符合 SRP:职责清晰
func QueryOrders(ctx context.Context, query OrderQuery) ([]*Order, error)
func ExportOrders(ctx context.Context, query ExportQuery) ([]byte, error)
func GetOrderStats(ctx context.Context, query StatsQuery) (*OrderStats, error)

最小知识原则

调用方只需要知道必要的信息:

go
// ❌ 调用方需要知道太多
GetOrders(ctx, QueryParams{
    UserID:    "123",
    QueryType: "user",     // 多余:有 UserID 就知道是用户查询
    Source:    "web",      // 多余:接口不应该关心来源
    RequestID: uuid.New(), // 多余:应该由中间件注入
})

// ✅ 调用方只传业务参数
GetUserOrders(ctx, UserOrderQuery{
    UserID: "123",
    Status: []OrderStatus{OrderStatusPaid},
})

显式 > 隐式

go
// ❌ 隐式:通过参数组合猜测意图
func GetOrders(ctx context.Context, userID string, includeAll bool) ([]*Order, error) {
    if userID == "" && includeAll {
        // 猜测是管理员查询
    }
}

// ✅ 显式:用类型明确表达意图
type OrderQueryContext interface {
    isOrderQueryContext()
}

type UserContext struct {
    UserID string
}

type MerchantContext struct {
    MerchantID string
}

type AdminContext struct {
    Filters AdminFilters
}

func (UserContext) isOrderQueryContext()     {}
func (MerchantContext) isOrderQueryContext() {}
func (AdminContext) isOrderQueryContext()    {}

func GetOrders(ctx context.Context, queryCtx OrderQueryContext) ([]*Order, error) {
    switch c := queryCtx.(type) {
    case UserContext:
        return queryUserOrders(ctx, c.UserID)
    case MerchantContext:
        return queryMerchantOrders(ctx, c.MerchantID)
    case AdminContext:
        return queryAllOrders(ctx, c.Filters)
    default:
        return nil, errors.New("unknown context type")
    }
}

用对象组合参数

go
// ❌ 参数堆砌
func GetOrders(
    status []string,
    dateStart, dateEnd *time.Time,
    minAmount, maxAmount *float64,
    merchantID, categoryID string,
    sortBy, sortOrder string,
) ([]*Order, error)

// ✅ 组合模式
type OrderFilters struct {
    Status       []OrderStatus
    DateRange    *DateRange
    AmountRange  *AmountRange
    Merchant     *MerchantFilter
    Category     *CategoryFilter
}

type OrderQuery struct {
    Filters    OrderFilters
    Sort       *SortOptions
    Pagination Pagination
}

func GetOrders(ctx context.Context, query OrderQuery) ([]*Order, error)

具体怎么改

方案一:参数分层

说人话就是:把参数分成三类

  1. 通用参数:所有场景都要的(比如分页)
  2. 场景标识:标明这是啥场景(用户查询?管理员查询?)
  3. 场景专属参数:这个场景特有的参数

实现示例

go
// 第一层:核心参数
type CoreParams struct {
    Pagination Pagination
}

type Pagination struct {
    Page     int
    PageSize int
}

// 第二层:场景标识(使用接口 + 类型断言)
type QueryContext interface {
    Scene() string
}

type UserCenterContext struct {
    UserID string
}

func (u UserCenterContext) Scene() string { return "userCenter" }

type AdminPanelContext struct {
    AdminID string
}

func (a AdminPanelContext) Scene() string { return "adminPanel" }

type MerchantPortalContext struct {
    MerchantID string
}

func (m MerchantPortalContext) Scene() string { return "merchantPortal" }

type DataExportContext struct {
    Requester string
}

func (d DataExportContext) Scene() string { return "dataExport" }

// 第三层:场景扩展
type UserCenterOptions struct {
    StatusFilter []OrderStatus
    DateRange    *DateRange
}

type AdminPanelOptions struct {
    IncludeDeleted bool
    SearchKeyword  string
}

type ExportOptions struct {
    Format  string // "csv" | "excel"
    Columns []string
}

// 组合查询
type OrderQuery struct {
    Core    CoreParams
    Context QueryContext
    Options interface{} // UserCenterOptions | AdminPanelOptions | ExportOptions
}

使用示例

go
// 用户中心调用
userOrders, err := orderService.GetOrders(ctx, OrderQuery{
    Core: CoreParams{
        Pagination: Pagination{Page: 1, PageSize: 20},
    },
    Context: UserCenterContext{UserID: "123"},
    Options: UserCenterOptions{
        StatusFilter: []OrderStatus{OrderStatusPaid, OrderStatusShipped},
    },
})

// 管理后台调用
adminOrders, err := orderService.GetOrders(ctx, OrderQuery{
    Core: CoreParams{
        Pagination: Pagination{Page: 1, PageSize: 50},
    },
    Context: AdminPanelContext{AdminID: "admin_001"},
    Options: AdminPanelOptions{
        IncludeDeleted: true,
        SearchKeyword:  "iPhone",
    },
})

这方案怎么样

好处

  • 结构清晰,新人上手快
  • 类型安全,IDE 智能提示友好
  • 加新场景不用改老代码

坏处

  • 参数嵌套有点深,调用时要写好几层
  • 要定义一堆 struct

啥时候用

  • 场景多(3 个以上)
  • 团队大,需要清晰的接口契约
  • 项目要长期维护

方案二:直接拆分接口

我最喜欢这个方案。简单粗暴,效果好。

啥时候拆

一个判断标准:

两个场景参数差异 > 50%?
├─ 是 → 拆!
└─ 否
   └─ 业务含义差别大?
      ├─ 是 → 拆!
      └─ 否 → 考虑参数分层

举例:用户查订单 vs 管理员查订单,虽然都是查订单,但权限、筛选条件、返回字段差别巨大,直接拆成两个接口。

拆分策略一:按角色拆分

go
// 用户角色接口
type UserOrderHandler struct {
    orderService *OrderService
}

func (h *UserOrderHandler) GetMyOrders(ctx context.Context, req *GetMyOrdersRequest) (*GetMyOrdersResponse, error) {
    userID := GetUserIDFromContext(ctx)
    
    orders, err := h.orderService.GetUserOrders(ctx, userID, OrderQueryOptions{
        Page:   req.Page,
        Status: req.Status,
    })
    if err != nil {
        return nil, err
    }
    
    return &GetMyOrdersResponse{
        Orders: orders,
        Total:  len(orders),
    }, nil
}

// 商户角色接口
type MerchantOrderHandler struct {
    orderService *OrderService
}

func (h *MerchantOrderHandler) GetMerchantOrders(ctx context.Context, req *GetMerchantOrdersRequest) (*GetMerchantOrdersResponse, error) {
    merchantID := GetMerchantIDFromContext(ctx)
    
    orders, err := h.orderService.GetMerchantOrders(ctx, merchantID, MerchantQueryOptions{
        Page:      req.Page,
        DateRange: req.DateRange,
    })
    if err != nil {
        return nil, err
    }
    
    return &GetMerchantOrdersResponse{
        Orders: orders,
    }, nil
}

// 管理员角色接口
type AdminOrderHandler struct {
    orderService *OrderService
}

func (h *AdminOrderHandler) GetAllOrders(ctx context.Context, req *GetAllOrdersRequest) (*GetAllOrdersResponse, error) {
    admin := GetAdminFromContext(ctx)
    
    orders, err := h.orderService.QueryAllOrders(ctx, req.Filters, admin.Permissions)
    if err != nil {
        return nil, err
    }
    
    return &GetAllOrdersResponse{
        Orders: orders,
    }, nil
}

拆分策略二:按场景拆分

go
// 查询场景
func (h *OrderHandler) QueryOrders(ctx context.Context, query OrderQuery) ([]*Order, error) {
    return h.orderService.Query(ctx, query)
}

// 导出场景
func (h *OrderHandler) ExportOrders(ctx context.Context, req ExportRequest) ([]byte, error) {
    return h.orderService.Export(ctx, req)
}

// 统计场景
func (h *OrderHandler) GetOrderStatistics(ctx context.Context, query StatsQuery) (*OrderStats, error) {
    return h.orderService.GetStatistics(ctx, query)
}

底层复用:Service 层设计

go
// Service 层:可复用的查询构建器
type OrderService struct {
    repo *OrderRepository
}

// 通用查询方法(私有)
func (s *OrderService) queryOrders(ctx context.Context, conditions QueryConditions) ([]*Order, error) {
    qb := s.repo.NewQueryBuilder()
    
    if conditions.UserID != "" {
        qb.WhereUserID(conditions.UserID)
    }
    
    if len(conditions.Status) > 0 {
        qb.WhereStatusIn(conditions.Status)
    }
    
    if conditions.DateRange != nil {
        qb.WhereDateBetween(conditions.DateRange.Start, conditions.DateRange.End)
    }
    
    return qb.Find(ctx)
}

// 对外接口:用户查询
func (s *OrderService) GetUserOrders(ctx context.Context, userID string, options OrderQueryOptions) ([]*Order, error) {
    return s.queryOrders(ctx, QueryConditions{
        UserID:     userID,
        Status:     options.Status,
        Pagination: options.Pagination,
    })
}

// 对外接口:管理员查询
func (s *OrderService) GetAdminOrders(ctx context.Context, filters AdminFilters) ([]*Order, error) {
    return s.queryOrders(ctx, QueryConditions{
        Status:         filters.Status,
        DateRange:      filters.DateRange,
        IncludeDeleted: true, // 管理员特权
    })
}

BFF (Backend For Frontend) 模式

go
// Mobile BFF
type MobileOrderAPI struct {
    orderService *OrderService
}

func (api *MobileOrderAPI) GetOrders(ctx context.Context) (*MobileOrdersResponse, error) {
    user := GetUserFromContext(ctx)
    orders, err := api.orderService.GetUserOrders(ctx, user.ID, OrderQueryOptions{})
    if err != nil {
        return nil, err
    }
    
    // 返回精简数据,适配移动端
    mobileOrders := make([]*MobileOrder, len(orders))
    for i, order := range orders {
        thumbnail := ""
        if len(order.Items) > 0 {
            thumbnail = order.Items[0].Thumbnail // 只返回第一个商品缩略图
        }
        
        mobileOrders[i] = &MobileOrder{
            ID:          order.ID,
            Status:      order.Status,
            TotalAmount: order.TotalAmount,
            Thumbnail:   thumbnail,
        }
    }
    
    return &MobileOrdersResponse{Orders: mobileOrders}, nil
}

// Web BFF
type WebOrderAPI struct {
    orderService *OrderService
}

func (api *WebOrderAPI) GetOrders(ctx context.Context) (*WebOrdersResponse, error) {
    user := GetUserFromContext(ctx)
    orders, err := api.orderService.GetUserOrders(ctx, user.ID, OrderQueryOptions{})
    if err != nil {
        return nil, err
    }
    
    // 返回完整数据,适配 Web
    webOrders := make([]*WebOrder, len(orders))
    for i, order := range orders {
        webOrders[i] = &WebOrder{
            ID:             order.ID,
            Status:         order.Status,
            TotalAmount:    order.TotalAmount,
            Items:          order.Items,          // 返回所有商品
            ShippingInfo:   order.Shipping,
            PaymentDetails: order.Payment,
        }
    }
    
    return &WebOrdersResponse{Orders: webOrders}, nil
}

方案三:Builder 模式

适用场景

查询条件特别多(5 个以上),而且组合很灵活的时候,用 Builder 模式写起来会舒服很多。

看一眼就知道为什么了:

Golang 实现

go
// QueryBuilder 查询构建器
type OrderQueryBuilder struct {
    conditions QueryConditions
    service    *OrderService
}

// NewOrderQueryBuilder 创建查询构建器
func NewOrderQueryBuilder(service *OrderService) *OrderQueryBuilder {
    return &OrderQueryBuilder{
        conditions: QueryConditions{},
        service:    service,
    }
}

// ForUser 指定用户
func (b *OrderQueryBuilder) ForUser(userID string) *OrderQueryBuilder {
    b.conditions.UserID = userID
    return b
}

// ForMerchant 指定商户
func (b *OrderQueryBuilder) ForMerchant(merchantID string) *OrderQueryBuilder {
    b.conditions.MerchantID = merchantID
    return b
}

// WithStatus 指定状态
func (b *OrderQueryBuilder) WithStatus(statuses ...OrderStatus) *OrderQueryBuilder {
    b.conditions.Status = statuses
    return b
}

// CreatedBetween 指定时间范围
func (b *OrderQueryBuilder) CreatedBetween(start, end time.Time) *OrderQueryBuilder {
    b.conditions.DateRange = &DateRange{Start: start, End: end}
    return b
}

// AmountGreaterThan 金额大于
func (b *OrderQueryBuilder) AmountGreaterThan(amount float64) *OrderQueryBuilder {
    b.conditions.MinAmount = &amount
    return b
}

// IncludeSoftDeleted 包含软删除
func (b *OrderQueryBuilder) IncludeSoftDeleted() *OrderQueryBuilder {
    b.conditions.IncludeDeleted = true
    return b
}

// SortBy 排序
func (b *OrderQueryBuilder) SortBy(field string, order SortOrder) *OrderQueryBuilder {
    b.conditions.SortBy = field
    b.conditions.SortOrder = order
    return b
}

// Paginate 分页
func (b *OrderQueryBuilder) Paginate(page, pageSize int) *OrderQueryBuilder {
    b.conditions.Pagination = &Pagination{
        Page:     page,
        PageSize: pageSize,
    }
    return b
}

// Execute 执行查询
func (b *OrderQueryBuilder) Execute(ctx context.Context) ([]*Order, error) {
    return b.service.query(ctx, b.conditions)
}

// Count 统计数量
func (b *OrderQueryBuilder) Count(ctx context.Context) (int64, error) {
    return b.service.count(ctx, b.conditions)
}

使用示例

go
// 用户查询自己的订单
userOrders, err := NewOrderQueryBuilder(orderService).
    ForUser("user_123").
    WithStatus(OrderStatusPaid, OrderStatusShipped).
    CreatedBetween(lastMonth, today).
    SortBy("created_at", SortOrderDesc).
    Paginate(1, 20).
    Execute(ctx)

// 管理员查询大额订单
highValueOrders, err := NewOrderQueryBuilder(orderService).
    AmountGreaterThan(10000).
    WithStatus(OrderStatusPaid).
    IncludeSoftDeleted().
    SortBy("amount", SortOrderDesc).
    Execute(ctx)

// 统计查询
count, err := NewOrderQueryBuilder(orderService).
    ForMerchant("merchant_456").
    CreatedBetween(thisMonthStart, thisMonthEnd).
    Count(ctx)

if err != nil {
    log.Printf("query failed: %v", err)
}

与 GORM 结合

go
// 与 GORM 结合的查询构建器
type OrderQueryBuilder struct {
    db *gorm.DB
}

func NewOrderQueryBuilder(db *gorm.DB) *OrderQueryBuilder {
    return &OrderQueryBuilder{db: db.Model(&Order{})}
}

func (b *OrderQueryBuilder) ForUser(userID string) *OrderQueryBuilder {
    b.db = b.db.Where("user_id = ?", userID)
    return b
}

func (b *OrderQueryBuilder) WithStatus(statuses ...OrderStatus) *OrderQueryBuilder {
    b.db = b.db.Where("status IN ?", statuses)
    return b
}

func (b *OrderQueryBuilder) CreatedBetween(start, end time.Time) *OrderQueryBuilder {
    b.db = b.db.Where("created_at BETWEEN ? AND ?", start, end)
    return b
}

func (b *OrderQueryBuilder) Paginate(page, pageSize int) *OrderQueryBuilder {
    offset := (page - 1) * pageSize
    b.db = b.db.Limit(pageSize).Offset(offset)
    return b
}

func (b *OrderQueryBuilder) Execute(ctx context.Context) ([]*Order, error) {
    var orders []*Order
    err := b.db.WithContext(ctx).Find(&orders).Error
    return orders, err
}

评价

优点

  • 链式调用,读起来像说话一样
  • 类型安全,IDE 智能提示很爽
  • 加新条件不影响老代码
  • 想在哪停就在哪停

缺点

  • 要写一堆 boilerplate 代码(方法太多)
  • 简单查询用这个有点杀鸡用牛刀

啥时候用

  • 查询条件多(5 个以上)
  • 条件组合很灵活
  • 这个查询逻辑会被很多地方用到

方案四:策略模式

解决的问题

你有没有写过这种代码:

go
// 一个 Handler 里全是 if/else
func (h *OrderHandler) QueryOrders(ctx context.Context, params map[string]interface{}) (interface{}, error) {
    source, _ := params["source"].(string)
    exportFormat, _ := params["exportFormat"].(string)
    includeDeleted, _ := params["includeDeleted"].(bool)
    
    if source == "userCenter" {
        if exportFormat != "" {
            // 用户导出逻辑
        } else {
            // 用户查询逻辑
        }
    } else if source == "adminPanel" {
        if includeDeleted {
            // 管理员查全部
        } else {
            // 管理员查正常
        }
    } else if source == "merchantPortal" {
        // 商户逻辑
    }
    // ... 我写不下去了
}

看着都累。策略模式能解决这个问题:

go
// 1. 定义策略接口
type OrderQueryStrategy interface {
    // 验证参数
    Validate(params map[string]interface{}) error
    
    // 构建查询条件
    BuildQuery(params map[string]interface{}) QueryConditions
    
    // 格式化返回结果
    FormatResult(orders []*Order) interface{}
    
    // 权限检查(可选)
    CheckPermission(user *User) error
}

// 2. 实现具体策略
type UserCenterStrategy struct{}

func (s *UserCenterStrategy) Validate(params map[string]interface{}) error {
    userID, ok := params["userId"].(string)
    if !ok || userID == "" {
        return errors.New("userId is required")
    }
    return nil
}

func (s *UserCenterStrategy) BuildQuery(params map[string]interface{}) QueryConditions {
    userID, _ := params["userId"].(string)
    status, _ := params["status"].([]OrderStatus)
    page, _ := params["page"].(int)
    pageSize, _ := params["pageSize"].(int)
    
    if page == 0 {
        page = 1
    }
    if pageSize == 0 {
        pageSize = 20
    }
    
    return QueryConditions{
        UserID: userID,
        Status: status,
        Pagination: &Pagination{
            Page:     page,
            PageSize: pageSize,
        },
    }
}

func (s *UserCenterStrategy) FormatResult(orders []*Order) interface{} {
    // 用户只需要看到基本信息
    type SimpleOrder struct {
        ID          string    `json:"id"`
        Status      string    `json:"status"`
        TotalAmount float64   `json:"totalAmount"`
        CreatedAt   time.Time `json:"createdAt"`
    }
    
    result := make([]SimpleOrder, len(orders))
    for i, order := range orders {
        result[i] = SimpleOrder{
            ID:          order.ID,
            Status:      string(order.Status),
            TotalAmount: order.TotalAmount,
            CreatedAt:   order.CreatedAt,
        }
    }
    return result
}

func (s *UserCenterStrategy) CheckPermission(user *User) error {
    return nil // 用户查自己的订单,无需特殊权限
}

// 管理员策略
type AdminPanelStrategy struct{}

func (s *AdminPanelStrategy) Validate(params map[string]interface{}) error {
    return nil // 管理员查询不需要 userID
}

func (s *AdminPanelStrategy) BuildQuery(params map[string]interface{}) QueryConditions {
    status, _ := params["status"].([]OrderStatus)
    dateRange, _ := params["dateRange"].(*DateRange)
    keyword, _ := params["keyword"].(string)
    includeDeleted, _ := params["includeDeleted"].(bool)
    page, _ := params["page"].(int)
    pageSize, _ := params["pageSize"].(int)
    
    if page == 0 {
        page = 1
    }
    if pageSize == 0 {
        pageSize = 50 // 管理员默认更多
    }
    
    return QueryConditions{
        Status:         status,
        DateRange:      dateRange,
        SearchKeyword:  keyword,
        IncludeDeleted: includeDeleted,
        Pagination: &Pagination{
            Page:     page,
            PageSize: pageSize,
        },
    }
}

func (s *AdminPanelStrategy) FormatResult(orders []*Order) interface{} {
    // 管理员需要看到所有信息
    type FullOrder struct {
        *Order
        UserInfo     *User     `json:"user"`
        MerchantInfo *Merchant `json:"merchant"`
        InternalNote string    `json:"internalNote"`
    }
    
    result := make([]FullOrder, len(orders))
    for i, order := range orders {
        result[i] = FullOrder{
            Order:        order,
            UserInfo:     order.User,
            MerchantInfo: order.Merchant,
            InternalNote: order.Notes,
        }
    }
    return result
}

func (s *AdminPanelStrategy) CheckPermission(user *User) error {
    if user.Role != RoleAdmin {
        return errors.New("permission denied: admin only")
    }
    return nil
}

// 3. 策略工厂
type OrderQueryStrategyFactory struct {
    strategies map[string]OrderQueryStrategy
}

func NewOrderQueryStrategyFactory() *OrderQueryStrategyFactory {
    factory := &OrderQueryStrategyFactory{
        strategies: make(map[string]OrderQueryStrategy),
    }
    
    // 注册策略
    factory.Register("userCenter", &UserCenterStrategy{})
    factory.Register("adminPanel", &AdminPanelStrategy{})
    factory.Register("export", &ExportStrategy{})
    
    return factory
}

func (f *OrderQueryStrategyFactory) Register(name string, strategy OrderQueryStrategy) {
    f.strategies[name] = strategy
}

func (f *OrderQueryStrategyFactory) Get(name string) (OrderQueryStrategy, error) {
    strategy, ok := f.strategies[name]
    if !ok {
        return nil, fmt.Errorf("unknown strategy: %s", name)
    }
    return strategy, nil
}

// 4. Handler 变得干净
type OrderHandler struct {
    strategyFactory *OrderQueryStrategyFactory
    orderService    *OrderService
}

func (h *OrderHandler) QueryOrders(ctx context.Context, params map[string]interface{}) (interface{}, error) {
    user := GetUserFromContext(ctx)
    source, _ := params["source"].(string)
    
    // 获取策略
    strategy, err := h.strategyFactory.Get(source)
    if err != nil {
        return nil, err
    }
    
    // 权限检查
    if err := strategy.CheckPermission(user); err != nil {
        return nil, err
    }
    
    // 验证参数
    if err := strategy.Validate(params); err != nil {
        return nil, err
    }
    
    // 构建查询
    query := strategy.BuildQuery(params)
    
    // 执行查询
    orders, err := h.orderService.Query(ctx, query)
    if err != nil {
        return nil, err
    }
    
    // 格式化结果
    return strategy.FormatResult(orders), nil
}

开闭原则的实践

新增场景时,只需要:

  1. 实现新的 Strategy
  2. 在 Factory 中注册
  3. 不需要修改 Handler 代码
go
// 新增:商户查询策略
type MerchantPortalStrategy struct{}

func (s *MerchantPortalStrategy) Validate(params map[string]interface{}) error {
    merchantID, ok := params["merchantId"].(string)
    if !ok || merchantID == "" {
        return errors.New("merchantId is required")
    }
    return nil
}

func (s *MerchantPortalStrategy) BuildQuery(params map[string]interface{}) QueryConditions {
    merchantID, _ := params["merchantId"].(string)
    status, _ := params["status"].([]OrderStatus)
    pagination, _ := params["pagination"].(*Pagination)
    
    return QueryConditions{
        MerchantID: merchantID,
        Status:     status,
        Pagination: pagination,
    }
}

func (s *MerchantPortalStrategy) FormatResult(orders []*Order) interface{} {
    // 商户视角的数据
    return orders
}

func (s *MerchantPortalStrategy) CheckPermission(user *User) error {
    if user.Role != RoleMerchant {
        return errors.New("permission denied: merchant only")
    }
    return nil
}

// 注册新策略
strategyFactory.Register("merchantPortal", &MerchantPortalStrategy{})
// Handler 代码无需改动!

方案五:CQRS

读写分离

CQRS(Command Query Responsibility Segregation)说人话就是:查询和修改分开。

  • Command(命令):创建、更新、删除,改数据的
  • Query(查询):只读数据,不改状态

为什么要这么做

  1. 接口参数不一样了:查询关心筛选条件,创建关心数据完整性
  2. 可以分别优化:查询走从库,写入走主库
  3. 方便做事件溯源(Event Sourcing)

实现示例

go
// Query 侧:只读查询
type OrderQueryService struct {
    readModel OrderReadModel
}

func (s *OrderQueryService) GetUserOrders(ctx context.Context, query UserOrderQuery) ([]*OrderDTO, error) {
    // 从只读模型查询,可能是缓存、ElasticSearch、只读副本等
    return s.readModel.FindByUser(ctx, query)
}

func (s *OrderQueryService) GetOrderStatistics(ctx context.Context, query StatsQuery) (*OrderStats, error) {
    // 统计查询可以走预聚合的物化视图
    return s.readModel.GetStatistics(ctx, query)
}

// Command 侧:写操作
type OrderCommandService struct {
    repo     OrderRepository
    eventBus EventBus
}

func (s *OrderCommandService) CreateOrder(ctx context.Context, cmd CreateOrderCommand) (string, error) {
    // 验证
    if err := s.validate(cmd); err != nil {
        return "", err
    }
    
    // 创建订单
    order := NewOrder(cmd)
    if err := s.repo.Save(ctx, order); err != nil {
        return "", err
    }
    
    // 发布事件
    event := OrderCreatedEvent{
        OrderID:   order.ID,
        UserID:    order.UserID,
        Amount:    order.TotalAmount,
        CreatedAt: time.Now(),
    }
    if err := s.eventBus.Publish(ctx, event); err != nil {
        return "", err
    }
    
    return order.ID, nil
}

func (s *OrderCommandService) UpdateOrderStatus(ctx context.Context, cmd UpdateOrderStatusCommand) error {
    order, err := s.repo.FindByID(ctx, cmd.OrderID)
    if err != nil {
        return err
    }
    
    order.UpdateStatus(cmd.NewStatus)
    if err := s.repo.Save(ctx, order); err != nil {
        return err
    }
    
    event := OrderStatusUpdatedEvent{
        OrderID:   order.ID,
        OldStatus: cmd.OldStatus,
        NewStatus: cmd.NewStatus,
        UpdatedAt: time.Now(),
    }
    return s.eventBus.Publish(ctx, event)
}

Query 和 Command 的参数差异

go
// Query:关注查询条件和返回格式
type UserOrderQuery struct {
    UserID     string
    Status     []OrderStatus
    DateRange  *DateRange
    Pagination Pagination
    Includes   []string // []string{"items", "shipping", "payment"} 控制返回字段
}

// Command:关注业务意图和数据完整性
type CreateOrderCommand struct {
    UserID          string
    Items           []OrderItemData
    ShippingAddress AddressData
    PaymentMethod   PaymentMethodData
    CouponCode      string
    // 没有分页、没有查询条件,只有创建订单所需的数据
}

事件驱动下的接口演进

go
// 订单创建后,可能触发多个下游操作
type OrderEventHandler struct {
    notificationService *NotificationService
    inventoryService    *InventoryService
    statisticsService   *StatisticsService
}

func (h *OrderEventHandler) HandleOrderCreated(ctx context.Context, event OrderCreatedEvent) error {
    // 发送通知
    if err := h.notificationService.SendOrderConfirmation(ctx, event.OrderID); err != nil {
        log.Printf("send notification failed: %v", err)
    }
    
    // 更新库存
    if err := h.inventoryService.ReserveItems(ctx, event.OrderID); err != nil {
        log.Printf("reserve inventory failed: %v", err)
    }
    
    // 更新统计
    if err := h.statisticsService.IncrementOrderCount(ctx, event.UserID); err != nil {
        log.Printf("update statistics failed: %v", err)
    }
    
    // 这些操作不需要在创建订单接口里处理
    // 接口参数更简洁
    return nil
}

// 注册事件处理器
func RegisterEventHandlers(eventBus EventBus, handler *OrderEventHandler) {
    eventBus.Subscribe("order.created", handler.HandleOrderCreated)
}

进阶内容

DDD 视角看接口设计

上下文边界

如果你的项目用了 DDD,记住一点:不同上下文(Bounded Context)的接口应该是独立的。

go
// 订单上下文
package ordercontext

type Order struct {
    ID     OrderID
    User   UserID // 只保存引用
    Items  []OrderItem
    Total  Money
}

type OrderQuery struct {
    UserID UserID
    Status OrderStatus
}

// 库存上下文
package inventorycontext

type StockReservation struct {
    OrderID   OrderID // 只保存引用
    SKU       SKU
    Quantity  int
}

type StockQuery struct {
    SKU         SKU
    WarehouseID WarehouseID
}

// 两个上下文的接口参数完全独立
// 不会出现在订单接口里传 WarehouseID 的情况

防腐层(Anti-Corruption Layer)

当接口需要对接外部系统时,防腐层负责参数转换:

go
// 内部领域模型
type InternalOrder struct {
    ID          string
    CustomerRef CustomerReference
    Items       []OrderItem
    Pricing     PricingInfo
}

// 外部系统的模型(第三方 ERP)
type ExternalERPOrder struct {
    OrderNo      string   `json:"order_no"`
    CustomerCode string   `json:"customer_code"`
    LineItems    []struct {
        ProductID string  `json:"product_id"`
        Qty       int     `json:"qty"`
        UnitPrice float64 `json:"unit_price"`
    } `json:"line_items"`
}

// 防腐层:转换器
type OrderAdapter struct{}

func (a *OrderAdapter) ToExternal(internal *InternalOrder) *ExternalERPOrder {
    external := &ExternalERPOrder{
        OrderNo:      internal.ID,
        CustomerCode: internal.CustomerRef.Code,
        LineItems:    make([]struct {
            ProductID string  `json:"product_id"`
            Qty       int     `json:"qty"`
            UnitPrice float64 `json:"unit_price"`
        }, len(internal.Items)),
    }
    
    for i, item := range internal.Items {
        external.LineItems[i].ProductID = item.SKU
        external.LineItems[i].Qty = item.Quantity
        external.LineItems[i].UnitPrice = item.Price.Amount
    }
    
    return external
}

func (a *OrderAdapter) FromExternal(external *ExternalERPOrder) *InternalOrder {
    // 反向转换
    internal := &InternalOrder{
        ID: external.OrderNo,
        CustomerRef: CustomerReference{
            Code: external.CustomerCode,
        },
        Items: make([]OrderItem, len(external.LineItems)),
    }
    
    for i, item := range external.LineItems {
        internal.Items[i] = OrderItem{
            SKU:      item.ProductID,
            Quantity: item.Qty,
            Price:    Money{Amount: item.UnitPrice},
        }
    }
    
    return internal
}

// 接口层使用防腐层
type OrderHandler struct {
    orderAdapter *OrderAdapter
    erpClient    *ERPClient
}

func (h *OrderHandler) SyncToERP(ctx context.Context, order *InternalOrder) error {
    erpOrder := h.orderAdapter.ToExternal(order)
    return h.erpClient.CreateOrder(ctx, erpOrder)
}

领域事件 vs 接口调用

有时候,不需要接口参数,而是通过事件传递:

go
// 不好的设计:通过接口传递太多上下文
type NotifyShippingParams struct {
    OrderID          string
    UserID           string
    Items            []OrderItem
    Address          Address
    PreferredCarrier string
    Urgency          string // "normal" | "urgent"
}

func (s *ShippingService) NotifyShipping(ctx context.Context, params NotifyShippingParams) error {
    // ...
}

// 更好的设计:发布领域事件
type OrderPaidEvent struct {
    OrderID    string
    OccurredAt time.Time
}

func (s *OrderService) MarkAsPaid(ctx context.Context, orderID string) error {
    // 更新订单状态
    if err := s.repo.UpdateStatus(ctx, orderID, OrderStatusPaid); err != nil {
        return err
    }
    
    // 发布事件
    event := OrderPaidEvent{
        OrderID:    orderID,
        OccurredAt: time.Now(),
    }
    return s.eventBus.Publish(ctx, "order.paid", event)
}

// 物流服务订阅事件,自己查询需要的信息
type ShippingEventHandler struct {
    orderRepo       OrderRepository
    shippingService *ShippingService
}

func (h *ShippingEventHandler) HandleOrderPaid(ctx context.Context, event OrderPaidEvent) error {
    order, err := h.orderRepo.FindByID(ctx, event.OrderID)
    if err != nil {
        return err
    }
    
    return h.shippingService.CreateShipment(ctx, order)
}

接口版本管理

版本策略

三种常见方式:

方式示例优点缺点
URL/v1/orders, /v2/orders直观客户端要改 URL
HeaderAPI-Version: 2URL 不变不直观
参数/orders?v=2灵活容易漏传

我个人倾向用 URL 版本,简单粗暴。

怎么下线老参数

go
// V1 查询参数
type OrderQueryV1 struct {
    UserID         string
    Status         OrderStatus
    IncludeDetails bool // V1 使用 boolean
}

// V2 查询参数
type OrderQueryV2 struct {
    UserID      string
    Status      OrderStatus
    DetailLevel string // V2 改为枚举:'none' | 'basic' | 'full'
}

// 兼容层:支持两个版本
type OrderHandler struct {
    service *OrderService
}

func (h *OrderHandler) GetOrders(ctx context.Context, r *http.Request) (interface{}, error) {
    apiVersion := r.Header.Get("API-Version")
    
    if apiVersion == "2" || apiVersion == "" {
        // 使用 V2
        var query OrderQueryV2
        if err := json.NewDecoder(r.Body).Decode(&query); err != nil {
            return nil, err
        }
        return h.getOrdersV2(ctx, query)
    } else {
        // V1 参数转换为 V2
        var queryV1 OrderQueryV1
        if err := json.NewDecoder(r.Body).Decode(&queryV1); err != nil {
            return nil, err
        }
        
        detailLevel := "none"
        if queryV1.IncludeDetails {
            detailLevel = "full"
        }
        
        queryV2 := OrderQueryV2{
            UserID:      queryV1.UserID,
            Status:      queryV1.Status,
            DetailLevel: detailLevel,
        }
        return h.getOrdersV2(ctx, queryV2)
    }
}

func (h *OrderHandler) getOrdersV2(ctx context.Context, query OrderQueryV2) ([]*Order, error) {
    // 统一的实现
    return h.service.GetOrders(ctx, query)
}

灰度发布

go
// 功能开关 + 版本控制
type OrderHandler struct {
    service       *OrderService
    featureToggle *FeatureToggle
}

func (h *OrderHandler) GetOrders(ctx context.Context, user *User, params OrderQuery) ([]*Order, error) {
    // 检查用户是否在灰度名单
    useNewVersion, err := h.featureToggle.IsEnabled(ctx, "order-query-v2", user.ID)
    if err != nil {
        return nil, err
    }
    
    if useNewVersion {
        return h.getOrdersV2(ctx, params)
    } else {
        return h.getOrdersV1(ctx, params)
    }
}

性能和安全

参数校验分层做

三层校验,各司其职:

go
// 第一层:格式校验(Handler 层,用框架能力)
type OrderQueryDTO struct {
    UserID   string   `json:"userId" binding:"required"`
    Status   string   `json:"status" binding:"omitempty,oneof=pending paid shipped"`
    PageSize int      `json:"pageSize" binding:"required,min=1,max=100"`
}

func (h *OrderHandler) GetOrders(c *gin.Context) {
    var dto OrderQueryDTO
    if err := c.ShouldBindJSON(&dto); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    // ...
}

// 第二层:业务逻辑校验(Service 层)
type OrderService struct {
    userRepo  UserRepository
    orderRepo OrderRepository
}

func (s *OrderService) GetUserOrders(ctx context.Context, query OrderQueryDTO) ([]*Order, error) {
    // 业务规则校验
    user, err := s.userRepo.FindByID(ctx, query.UserID)
    if err != nil {
        return nil, fmt.Errorf("user not found: %w", err)
    }
    
    if user.IsBlocked {
        return nil, errors.New("user is blocked")
    }
    
    // 权限校验
    currentUser := GetUserFromContext(ctx)
    if query.UserID != currentUser.ID && currentUser.Role != RoleAdmin {
        return nil, errors.New("permission denied")
    }
    
    return s.orderRepo.FindByUser(ctx, query)
}

// 第三层:数据层校验(一致性)
type OrderRepository struct {
    db *gorm.DB
}

func (r *OrderRepository) FindByUser(ctx context.Context, query OrderQueryDTO) ([]*Order, error) {
    // 防止查询爆炸
    if query.PageSize > 1000 {
        return nil, errors.New("page size too large")
    }
    
    var orders []*Order
    err := r.db.WithContext(ctx).
        Where("user_id = ?", query.UserID).
        Where("status = ?", query.Status).
        Limit(query.PageSize).
        Find(&orders).Error
        
    return orders, err
}

敏感参数的处理

go
// 不要在查询参数里传递敏感信息
// ❌ 不好
// GET /orders?userId=123&password=abc123&creditCard=xxxx

// ✅ 好:敏感信息通过认证 token
// GET /orders?userId=123
// Headers: Authorization: Bearer <token>

// 接口内部从 token 提取用户信息
func (h *OrderHandler) GetOrders(c *gin.Context) {
    userID := c.Query("userId")
    
    // 从 context 中获取已认证的用户
    currentUser, exists := c.Get("currentUser")
    if !exists {
        c.JSON(401, gin.H{"error": "unauthorized"})
        return
    }
    
    user := currentUser.(*User)
    
    // 验证 userID 是否和 token 中的用户匹配
    if userID != user.ID && user.Role != RoleAdmin {
        c.JSON(403, gin.H{"error": "forbidden"})
        return
    }
    
    orders, err := h.orderService.GetUserOrders(c.Request.Context(), userID)
    if err != nil {
        c.JSON(500, gin.H{"error": err.Error()})
        return
    }
    
    c.JSON(200, orders)
}

批量接口的参数设计

go
// 批量查询
type BatchOrderQuery struct {
    OrderIDs []string `json:"orderIds" binding:"required,max=100"`
    Fields   []string `json:"fields,omitempty"`
}

func (h *OrderHandler) GetBatchOrders(c *gin.Context) {
    var query BatchOrderQuery
    if err := c.ShouldBindJSON(&query); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    
    // 限制批量大小
    if len(query.OrderIDs) > 100 {
        c.JSON(400, gin.H{"error": "max 100 orders per request"})
        return
    }
    
    orders, err := h.orderService.FindByIDs(c.Request.Context(), query.OrderIDs)
    if err != nil {
        c.JSON(500, gin.H{"error": err.Error()})
        return
    }
    
    // 字段过滤,减少数据传输
    if len(query.Fields) > 0 {
        filteredOrders := h.filterFields(orders, query.Fields)
        c.JSON(200, filteredOrders)
        return
    }
    
    c.JSON(200, orders)
}

func (h *OrderHandler) filterFields(orders []*Order, fields []string) []map[string]interface{} {
    result := make([]map[string]interface{}, len(orders))
    for i, order := range orders {
        item := make(map[string]interface{})
        for _, field := range fields {
            switch field {
            case "id":
                item["id"] = order.ID
            case "status":
                item["status"] = order.Status
            case "totalAmount":
                item["totalAmount"] = order.TotalAmount
            // ... 其他字段
            }
        }
        result[i] = item
    }
    return result
}

真实案例:任务管理系统重构

背景

去年帮一个 SaaS 公司重构任务管理系统。这个系统已经运行 2 年了,最开始就一个简单的查询接口,后来... 你懂的。

重构前长啥样

先看看这个"艺术品":

go
// 19 个参数,我数了三遍
func (h *TaskHandler) GetTasks(c *gin.Context) {
    userID := c.Query("userId")
    projectID := c.Query("projectID")
    status := c.Query("status")
    priority := c.Query("priority")
    assignee := c.Query("assignee")
    creator := c.Query("creator")
    tags := c.Query("tags")
    startDate := c.Query("startDate")
    endDate := c.Query("endDate")
    keyword := c.Query("keyword")
    sortBy := c.Query("sortBy")
    sortOrder := c.Query("sortOrder")
    page := c.Query("page")
    pageSize := c.Query("pageSize")
    includeArchived := c.Query("includeArchived")
    includeSubtasks := c.Query("includeSubtasks")
    groupBy := c.Query("groupBy")
    source := c.Query("source") // 'web' | 'mobile' | 'api'
    exportFormat := c.Query("exportFormat")
    
    // 200+ 行的 if/else 逻辑
    if source == "web" {
        if exportFormat != "" {
            // 导出逻辑
        } else {
            // 查询逻辑
        }
    }
    // ...
}

问题一眼就能看出来:

  • 参数太多,调用方根本记不住
  • sourceexportFormat 还会互相影响
  • 文档早就和代码对不上了

怎么重构的

第一步:搞清楚谁在用

画了个表:

调用方场景核心需求特殊需求
Web 用户端看自己的任务userID, status, keyword
移动端看今日任务userID, dueDate返回要精简
项目管理页看项目任务projectID, assignee要 groupBy
管理后台看所有任务全量查询includeArchived
报表导出导数据dateRange, format不要分页

一看差异超过 60%,拆!

第二步:拆分接口

go
// 用户查询自己的任务
type MyTaskQuery struct {
    Status   []TaskStatus `json:"status"`
    Keyword  string       `json:"keyword"`
    DueDate  *DateFilter  `json:"dueDate"`
    Page     int          `json:"page" binding:"required,min=1"`
    PageSize int          `json:"pageSize" binding:"required,min=1,max=100"`
}

func (h *TaskHandler) GetMyTasks(c *gin.Context) {
    user := GetUserFromContext(c)
    
    var query MyTaskQuery
    if err := c.ShouldBindJSON(&query); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    
    tasks, err := h.taskService.GetUserTasks(c.Request.Context(), user.ID, query)
    if err != nil {
        c.JSON(500, gin.H{"error": err.Error()})
        return
    }
    
    c.JSON(200, tasks)
}

// 项目任务查询
type ProjectTaskQuery struct {
    Assignee string       `json:"assignee"`
    Status   []TaskStatus `json:"status"`
    GroupBy  string       `json:"groupBy"` // "status" | "assignee" | "priority"
    Page     int          `json:"page" binding:"required"`
    PageSize int          `json:"pageSize" binding:"required"`
}

func (h *TaskHandler) GetProjectTasks(c *gin.Context) {
    projectID := c.Param("projectId")
    
    var query ProjectTaskQuery
    if err := c.ShouldBindJSON(&query); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    
    tasks, err := h.taskService.GetProjectTasks(c.Request.Context(), projectID, query)
    if err != nil {
        c.JSON(500, gin.H{"error": err.Error()})
        return
    }
    
    c.JSON(200, tasks)
}

// 管理员查询
type AdminTaskQuery struct {
    Filters struct {
        UserID    string       `json:"userId"`
        ProjectID string       `json:"projectId"`
        Status    []TaskStatus `json:"status"`
        DateRange *DateRange   `json:"dateRange"`
    } `json:"filters"`
    IncludeArchived bool       `json:"includeArchived"`
    Pagination      Pagination `json:"pagination"`
}

func (h *TaskHandler) GetAdminTasks(c *gin.Context) {
    // 权限检查
    user := GetUserFromContext(c)
    if user.Role != RoleAdmin {
        c.JSON(403, gin.H{"error": "admin only"})
        return
    }
    
    var query AdminTaskQuery
    if err := c.ShouldBindJSON(&query); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    
    tasks, err := h.taskService.QueryAllTasks(c.Request.Context(), query)
    if err != nil {
        c.JSON(500, gin.H{"error": err.Error()})
        return
    }
    
    c.JSON(200, tasks)
}

// 导出接口
type TaskExportRequest struct {
    Filters TaskFilters `json:"filters"`
    Format  string      `json:"format" binding:"required,oneof=csv excel"`
    Columns []string    `json:"columns" binding:"required"`
    // 没有分页参数
}

func (h *TaskHandler) ExportTasks(c *gin.Context) {
    var req TaskExportRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    
    data, err := h.taskService.ExportTasks(c.Request.Context(), req)
    if err != nil {
        c.JSON(500, gin.H{"error": err.Error()})
        return
    }
    
    c.Data(200, "application/octet-stream", data)
}

第三步:重构 Service 层

go
// 底层:通用查询构建器
type TaskQueryBuilder struct {
    db *gorm.DB
}

func NewTaskQueryBuilder(db *gorm.DB) *TaskQueryBuilder {
    return &TaskQueryBuilder{db: db.Model(&Task{})}
}

func (b *TaskQueryBuilder) ForUser(userID string) *TaskQueryBuilder {
    b.db = b.db.Where("user_id = ?", userID)
    return b
}

func (b *TaskQueryBuilder) InProject(projectID string) *TaskQueryBuilder {
    b.db = b.db.Where("project_id = ?", projectID)
    return b
}

func (b *TaskQueryBuilder) WithStatus(statuses []TaskStatus) *TaskQueryBuilder {
    if len(statuses) > 0 {
        b.db = b.db.Where("status IN ?", statuses)
    }
    return b
}

func (b *TaskQueryBuilder) SearchKeyword(keyword string) *TaskQueryBuilder {
    if keyword != "" {
        b.db = b.db.Where("title LIKE ? OR description LIKE ?",
            "%"+keyword+"%", "%"+keyword+"%")
    }
    return b
}

func (b *TaskQueryBuilder) Paginate(page, pageSize int) *TaskQueryBuilder {
    offset := (page - 1) * pageSize
    b.db = b.db.Limit(pageSize).Offset(offset)
    return b
}

func (b *TaskQueryBuilder) Execute(ctx context.Context) ([]*Task, error) {
    var tasks []*Task
    err := b.db.WithContext(ctx).Find(&tasks).Error
    return tasks, err
}

// Service 层:对外提供语义化方法
type TaskService struct {
    db *gorm.DB
}

func (s *TaskService) GetUserTasks(ctx context.Context, userID string, query MyTaskQuery) ([]*Task, error) {
    builder := NewTaskQueryBuilder(s.db)
    
    builder.ForUser(userID)
    
    if len(query.Status) > 0 {
        builder.WithStatus(query.Status)
    }
    
    if query.Keyword != "" {
        builder.SearchKeyword(query.Keyword)
    }
    
    return builder.Paginate(query.Page, query.PageSize).Execute(ctx)
}

func (s *TaskService) GetProjectTasks(ctx context.Context, projectID string, query ProjectTaskQuery) ([]*Task, error) {
    builder := NewTaskQueryBuilder(s.db)
    
    builder.InProject(projectID)
    
    if query.Assignee != "" {
        builder.db = builder.db.Where("assignee = ?", query.Assignee)
    }
    
    return builder.Paginate(query.Page, query.PageSize).Execute(ctx)
}

效果如何

数据说话:

指标重构前重构后改善
接口参数数19 个4-6 个少了 70%
Handler 代码250 行15-30 行少了 60%
调用错误率8.5%1.2%降了 86%
新需求开发3 天半天快了 6 倍
测试覆盖率42%87%翻倍

最爽的是:加新功能不用担心影响老功能了。

踩的坑

坑1:拆过头了

一开始太激进,拆了 30 多个接口。结果维护成本反而更高了,改个东西要改好几个文件。

后来把相似度 > 80% 的接口合并,稳定在 12 个。记住:拆分是为了降低复杂度,不是为了炫技。

坑2:底层没抽象好

接口是拆了,但每个 Service 方法里都在写 QueryBuilder 的逻辑,代码重复得一塌糊涂。

后来专门抽了一个 TaskQueryBuilder,Service 只负责调用。拆分之前,先把底层抽象搞好。

坑3:老接口没下线

新接口写好了,老接口还在那儿。结果调用方不知道用哪个,两个都在用。

后来给老接口加了 Deprecated 标记,返回里也加了提示,设了 3 个月下线期。该狠心的时候得狠心。


Checklist

新接口上线前自查

需求

设计

实现

文档和测试

安全

Code Review 看啥

接口层

  • 命名是否语义化?(GetUserOrders > GetOrders
  • 一个接口是不是干了好几件事?
  • 参数超过 7 个了吗?

Service 层

  • if/else 是不是太多了?
  • 有没有重复代码?

文档

  • 有没有使用示例?
  • 错误码有说明吗?
  • 废弃字段有标注吗?

8.3 文档规范建议

OpenAPI / Swagger 示例

yaml
paths:
  /api/v1/users/{userId}/orders:
    get:
      summary: 查询用户订单
      description: 用户查看自己的订单列表,支持按状态和日期筛选
      tags:
        - 订单
      parameters:
        - name: userId
          in: path
          required: true
          description: 用户ID
          schema:
            type: string
        - name: status
          in: query
          required: false
          description: 订单状态,多个状态用逗号分隔
          schema:
            type: string
            enum: [pending, paid, shipped, completed, cancelled]
        - name: page
          in: query
          required: false
          description: 页码,从1开始
          schema:
            type: integer
            default: 1
            minimum: 1
      responses:
        '200':
          description: 成功
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Order'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
              example:
                data:
                  - id: "order_123"
                    status: "paid"
                    totalAmount: 299.00
                    createdAt: "2024-01-15T10:30:00Z"
                pagination:
                  page: 1
                  pageSize: 20
                  total: 45
        '400':
          description: 参数错误
        '401':
          description: 未授权
        '403':
          description: 无权限访问此用户的订单

总结一下

核心就几点

  1. 接口是契约,不是工具箱

    明确承诺能做啥,别啥都往里塞。

  2. 该拆就拆

    场景差异 > 50%,果断拆。接口层分开,底层复用。

  3. 显式 > 隐式

    用类型约束,用枚举标识场景,别让调用方猜。

  4. 分层设计

    • Handler:路由、权限
    • Service:业务逻辑
    • Repository:数据访问

    复用发生在底层,不是接口层。

方案怎么选

场景推荐方案理由
参数 < 5个,场景单一无需特殊处理简单就是美
参数 5-10个,场景2-3个参数分层架构保持单接口,结构清晰
场景 > 3个,差异明显接口拆分契约清晰,维护性好
查询条件复杂多变Builder 模式可读性强,易扩展
大量 if/else 判断场景策略模式开闭原则,可测试性好
读写场景完全不同CQRS读写分离,性能优化空间大

几句掏心窝的话

设计时多想 30 分钟,能省 3 天重构时间

我现在写接口前都会停下来问自己:谁会用?怎么用?会不会有第二个场景?这半小时真的值。

发现第二个调用方就该警惕了

别等到第五个、第六个才想起来重构。第二个场景出现时,就该停下来想想是不是该拆了。

先写文档,再写代码

我知道很多人(包括以前的我)都是代码写完补文档。但试试反过来:先把接口文档写清楚,再写实现。会发现很多问题在设计阶段就能暴露。

别怕重构

代码是写给人看的,不是写给机器看的。看着难受就重构,别拖。

参考资料

这些资料对我帮助很大:

  • 《领域驱动设计》 - Eric Evans(理论有点重,但值得啃)
  • 《企业应用架构模式》 - Martin Fowler(经典)
  • 《Go语言高级编程》 - 柴树杉、曹春晖(Go 进阶必读)

在线资源

工具

  • Swagger / OpenAPI - 接口文档标准
  • Gin / Echo - Go Web 框架
  • GORM - ORM 框架
  • go-validator - 参数校验

下次 PM 跟你说"能不能在这个接口加个参数"的时候,先别急着改。问三个问题:

  1. 这是同一个场景吗?
  2. 参数差异有多大?
  3. 是不是该拆分了?

想清楚了再动手。

接口设计没有银弹,只有适合自己项目的方案。这篇文章提到的方法都是我踩坑踩出来的,希望能帮你少踩点坑。

最后,记住:接口是写给人看的,不是写给机器看的。


写于上海,凌晨 3 点,刚改完一个屎山接口

2025.12

上次更新于: