接口参数设计:多场景复用时别把自己搞死
前几天 Code Review,看到一个接口有 15 个参数,当场就绷不住了。这事得从头说起。
一个接口是怎么变成屎山的
120天,从清爽到崩溃
订单系统的接口已经没法维护了。但翻 Git 记录发现,最初这玩意儿写得挺干净:
Day 1 - 初版设计(清晰明了)
// 简洁、职责单一
func GetUserOrders(ctx context.Context, userID string, page, pageSize int) ([]*Order, error) {
return orderRepo.FindByUserID(ctx, userID, page, pageSize)
}这个接口工作得很好,直到产品经理提出第二个需求。
Day 30 - 管理后台需要查询所有订单
"能不能让这个接口也支持管理员查所有订单?加个参数就行。"
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 - 需要支持订单状态筛选
func GetOrders(ctx context.Context, userID string, page, pageSize int,
isAdmin bool, status string, startDate, endDate time.Time) ([]*Order, error) {
// if/else 逻辑开始膨胀...
}Day 90 - 财务部门要导出报表
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 - 当前状态(谁都看不懂)
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 逻辑
// 每个新需求都是一次"外科手术"
// 改一处,测三处,崩五处
}看到没?典型的"温水煮青蛙"。每次改动单独看都没问题,但堆一起就是屎山。
为什么会变成这样
说白了就三个问题:
- 一个接口干三件事:用户查订单、管理员查订单、财务导数据,硬塞一个接口里
- 调用方靠猜:参数组合有几十种,文档?不存在的,都是口口相传
- 代码全是 if/else:
if source == "admin" && exportFormat != ""这种判断到处都是,改一个地方要测五个场景
你的接口中招了吗
参数爆炸
症状:参数超过 7 个,而且还在涨
这种接口调用起来是真的难受:
- 每次调用都要翻文档(如果有的话)
- IDE 智能提示?一屏都显示不下
- 写单元测试要构造一堆 mock 数据,烦死了
举个例子:
// 见过最离谱的,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 不能同时传,但类型系统管不了
type QueryParams struct {
UserID string // 查询用户订单
MerchantID string // 查询商户订单
// ⚠️ 这两个参数不能同时传,但类型系统无法约束
}
// 调用方只能靠文档和经验
GetOrders(ctx, QueryParams{
UserID: "123",
MerchantID: "456", // 到底查啥?
})问题是编译器管不了这事儿,只能在文档里写"注意:UserID 和 MerchantID 二选一"。然后新来的同学就会写出:
GetOrders(ctx, QueryParams{
UserID: "123",
MerchantID: "456", // ???
})线上直接炸。
幽灵参数
症状:文档和代码对不上
// Status 订单状态(必填)
func GetOrders(ctx context.Context, status string) ([]*Order, error) {
// 实际代码里有默认值
if status == "" {
status = "all"
}
// ...
}典型场景:最开始 status 是必传的,后来产品说"全部订单也要支持",于是改成可选。文档?哪有时间改文档。
三个月后新同学入职,看文档说必传,传了个空字符串,结果查出来一堆脏数据。
场景嗅探
症状:靠参数组合猜用户想干啥
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!
代码复用应该发生在底层抽象,不是接口层。
// ❌ 错误的复用:接口层强行复用
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.
这句话经常被误解成"接口要啥都能接受"。错了:
错误理解:
// 误用:接收一切,内部判断一切
func GetOrders(ctx context.Context, params map[string]interface{}) ([]*Order, error) {
// 接收任何参数,内部自己判断
}正确理解:
// 对外:契约明确
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 向前设计
向后兼容:不破坏已有调用方 向前设计:提前考虑未来场景
// 不好的向前设计:过度预测
type OrderQuery struct {
Filters []struct {
Field string
Operator string // "eq", "gt", "lt", "like", ...
Value interface{}
} // 提前设计了复杂查询
}
// 好的向前设计:可扩展但不过度
type OrderQuery struct {
Status []OrderStatus
DateRange *DateRange
// 如果未来需要复杂查询,再加 Filters 字段
}几个原则
单一职责(SRP)
一个接口只做一件事:
// ❌ 违反 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)最小知识原则
调用方只需要知道必要的信息:
// ❌ 调用方需要知道太多
GetOrders(ctx, QueryParams{
UserID: "123",
QueryType: "user", // 多余:有 UserID 就知道是用户查询
Source: "web", // 多余:接口不应该关心来源
RequestID: uuid.New(), // 多余:应该由中间件注入
})
// ✅ 调用方只传业务参数
GetUserOrders(ctx, UserOrderQuery{
UserID: "123",
Status: []OrderStatus{OrderStatusPaid},
})显式 > 隐式
// ❌ 隐式:通过参数组合猜测意图
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")
}
}用对象组合参数
// ❌ 参数堆砌
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)具体怎么改
方案一:参数分层
说人话就是:把参数分成三类
- 通用参数:所有场景都要的(比如分页)
- 场景标识:标明这是啥场景(用户查询?管理员查询?)
- 场景专属参数:这个场景特有的参数
实现示例
// 第一层:核心参数
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
}使用示例
// 用户中心调用
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 管理员查订单,虽然都是查订单,但权限、筛选条件、返回字段差别巨大,直接拆成两个接口。
拆分策略一:按角色拆分
// 用户角色接口
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
}拆分策略二:按场景拆分
// 查询场景
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 层设计
// 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) 模式
// 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 实现
// 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)
}使用示例
// 用户查询自己的订单
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 结合
// 与 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 个以上)
- 条件组合很灵活
- 这个查询逻辑会被很多地方用到
方案四:策略模式
解决的问题
你有没有写过这种代码:
// 一个 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" {
// 商户逻辑
}
// ... 我写不下去了
}看着都累。策略模式能解决这个问题:
// 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
}开闭原则的实践
新增场景时,只需要:
- 实现新的 Strategy
- 在 Factory 中注册
- 不需要修改 Handler 代码
// 新增:商户查询策略
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(查询):只读数据,不改状态
为什么要这么做
- 接口参数不一样了:查询关心筛选条件,创建关心数据完整性
- 可以分别优化:查询走从库,写入走主库
- 方便做事件溯源(Event Sourcing)
实现示例
// 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 的参数差异
// 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
// 没有分页、没有查询条件,只有创建订单所需的数据
}事件驱动下的接口演进
// 订单创建后,可能触发多个下游操作
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)的接口应该是独立的。
// 订单上下文
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)
当接口需要对接外部系统时,防腐层负责参数转换:
// 内部领域模型
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 接口调用
有时候,不需要接口参数,而是通过事件传递:
// 不好的设计:通过接口传递太多上下文
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 |
| Header | API-Version: 2 | URL 不变 | 不直观 |
| 参数 | /orders?v=2 | 灵活 | 容易漏传 |
我个人倾向用 URL 版本,简单粗暴。
怎么下线老参数
// 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)
}灰度发布
// 功能开关 + 版本控制
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)
}
}性能和安全
参数校验分层做
三层校验,各司其职:
// 第一层:格式校验(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
}敏感参数的处理
// 不要在查询参数里传递敏感信息
// ❌ 不好
// 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)
}批量接口的参数设计
// 批量查询
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 年了,最开始就一个简单的查询接口,后来... 你懂的。
重构前长啥样
先看看这个"艺术品":
// 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 {
// 查询逻辑
}
}
// ...
}问题一眼就能看出来:
- 参数太多,调用方根本记不住
source和exportFormat还会互相影响- 文档早就和代码对不上了
怎么重构的
第一步:搞清楚谁在用
画了个表:
| 调用方 | 场景 | 核心需求 | 特殊需求 |
|---|---|---|---|
| Web 用户端 | 看自己的任务 | userID, status, keyword | 无 |
| 移动端 | 看今日任务 | userID, dueDate | 返回要精简 |
| 项目管理页 | 看项目任务 | projectID, assignee | 要 groupBy |
| 管理后台 | 看所有任务 | 全量查询 | includeArchived |
| 报表导出 | 导数据 | dateRange, format | 不要分页 |
一看差异超过 60%,拆!
第二步:拆分接口
// 用户查询自己的任务
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 层
// 底层:通用查询构建器
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 示例
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: 无权限访问此用户的订单总结一下
核心就几点
接口是契约,不是工具箱
明确承诺能做啥,别啥都往里塞。
该拆就拆
场景差异 > 50%,果断拆。接口层分开,底层复用。
显式 > 隐式
用类型约束,用枚举标识场景,别让调用方猜。
分层设计
- Handler:路由、权限
- Service:业务逻辑
- Repository:数据访问
复用发生在底层,不是接口层。
方案怎么选
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 参数 < 5个,场景单一 | 无需特殊处理 | 简单就是美 |
| 参数 5-10个,场景2-3个 | 参数分层架构 | 保持单接口,结构清晰 |
| 场景 > 3个,差异明显 | 接口拆分 | 契约清晰,维护性好 |
| 查询条件复杂多变 | Builder 模式 | 可读性强,易扩展 |
| 大量 if/else 判断场景 | 策略模式 | 开闭原则,可测试性好 |
| 读写场景完全不同 | CQRS | 读写分离,性能优化空间大 |
几句掏心窝的话
设计时多想 30 分钟,能省 3 天重构时间
我现在写接口前都会停下来问自己:谁会用?怎么用?会不会有第二个场景?这半小时真的值。
发现第二个调用方就该警惕了
别等到第五个、第六个才想起来重构。第二个场景出现时,就该停下来想想是不是该拆了。
先写文档,再写代码
我知道很多人(包括以前的我)都是代码写完补文档。但试试反过来:先把接口文档写清楚,再写实现。会发现很多问题在设计阶段就能暴露。
别怕重构
代码是写给人看的,不是写给机器看的。看着难受就重构,别拖。
参考资料
这些资料对我帮助很大:
书
- 《领域驱动设计》 - Eric Evans(理论有点重,但值得啃)
- 《企业应用架构模式》 - Martin Fowler(经典)
- 《Go语言高级编程》 - 柴树杉、曹春晖(Go 进阶必读)
在线资源
- Microsoft API 设计指南 - 写得很详细
- Google API 设计指南 - 简洁实用
- Uber Go 代码规范 - Go 项目必看
工具
- Swagger / OpenAPI - 接口文档标准
- Gin / Echo - Go Web 框架
- GORM - ORM 框架
- go-validator - 参数校验
下次 PM 跟你说"能不能在这个接口加个参数"的时候,先别急着改。问三个问题:
- 这是同一个场景吗?
- 参数差异有多大?
- 是不是该拆分了?
想清楚了再动手。
接口设计没有银弹,只有适合自己项目的方案。这篇文章提到的方法都是我踩坑踩出来的,希望能帮你少踩点坑。
最后,记住:接口是写给人看的,不是写给机器看的。
写于上海,凌晨 3 点,刚改完一个屎山接口
2025.12

