上周做代码评审,同事指着一行函数签名问我:“GuildBusinessStreamerListReq 和 GuildStreamerListReq 这两个请求结构体,能不能干脆都改成 interface{}?反正调用方传哪个都行,底层再做类型断言,两个列表接口就能共用一个入参了。”
会议室安静了几秒。这个问题我太熟了——刚转 Go 那会儿我也这么想过,当时的逻辑也很朴素:结构体太死板,每加一个字段就要动一遍定义,传参的时候还得精确匹配类型,用 interface{} 多自由,传什么进来都行,底层再自己判断。直到后来线上环境用一次真实事故让我明白,这种“自由”的代价,比想象中贵得多。今天就用这两个直播场景里的请求对象当例子,把这个事彻底说清楚。
1. 先看清两个结构体:它们到底是干什么的
1.1 从命名拆解业务语义
GuildBusinessStreamerListReq 和 GuildStreamerListReq 这种命名,混过直播平台后端的人应该一眼就能认出来:Guild 是公会,Streamer 是主播,ListReq 就是列表查询请求。两个结构体对应的是公会场景下两个看起来高度相似的接口——一个查公会下的主播列表,一个查公会下的商业化主播列表。
我见到这两类请求结构体时的典型定义长这样:
go复制// GuildStreamerListReq 公会主播列表查询请求
type GuildStreamerListReq struct {
GuildID int64 `json:"guild_id"`
Page int32 `json:"page"`
PageSize int32 `json:"page_size"`
Status int32 `json:"status"`
Keyword string `json:"keyword"`
}
// GuildBusinessStreamerListReq 公会商业化主播列表查询请求
type GuildBusinessStreamerListReq struct {
GuildID int64 `json:"guild_id"`
Page int32 `json:"page"`
PageSize int32 `json:"page_size"`
BusinessType int32 `json:"business_type"`
SortBy string `json:"sort_by"`
Order string `json:"order"`
Keyword string `json:"keyword"`
}
两个结构体确实存在明显的共同字段:GuildID、Page、PageSize、Keyword,后面这个还多了 BusinessType、SortBy、Order 等字段。这也就解释了那个问题为什么会被提出来——两个结构体既然“差不多”,处理逻辑也“差不多”,那为什么不把参数统一收成一个 interface{},让同一个函数既能接收 A 又能接收 B,省得写两套处理流程?
这个思路在工程上有它合理的地方:减少重复代码、提高扩展性。但问题在于,interface{} 并不是实现这个目标的正确工具,它只是把问题从一个地方搬到了另一个地方,还顺手埋了一堆雷。
1.2 “统一入参”的痛点确实存在
先说句公道话,有这种想法说明你在认真思考接口设计。在实际业务里,公会主播列表和商业化主播列表经常共享大量查询逻辑,比如分页、关键词过滤、按主播 ID 批量查三方数据、渠道数据聚合等。如果为两个结构体各写一遍完整链路,代码会非常臃肿。
我当时遇到的情况比这个更典型:第三个列表需求已经来了,要加一个“公会新主播列表”,参数又和前面两个高度重合。看着同一段分页逻辑被复制了三遍,任何人都会想:能不能让入口参数“通用”一点?
所以问题的本质不是“结构体能不能改成 inter
