新模块接住旧客户端请求的关键是完整兼容旧请求的每个字节:字段增删改需用指针/omitempty/双字段过渡,路径与方法变更须路由层兜底,v2模块需独立路径隔离,错误响应结构与状态码必须冻结不变。

旧客户端不动,新模块怎么接得住请求
关键不是“新模块能不能跑”,而是“旧客户端发什么,服务端能不能照单全收”。Go 的 json.Unmarshal 默认忽略未知字段,但对缺失字段、类型变更或删字段极其敏感——旧客户端不传新字段没问题,但若你把 Count int 改成 Count string,它就会静默赋零值或直接解析失败。
- 新增字段一律用指针或
omitempty:比如UpdatedAt *time.Time `json:"updated_at,omitempty"`,避免旧请求不带该字段时被塞进零时间 - 删字段不能真删,先加
json:"-"标签并注释,例如OldField int `json:"-" // deprecated since v2.1` - 类型变更必须双字段过渡:保留
CountInt int `json:"count"`和新增CountStr string `json:"count_str,omitempty"`,在UnmarshalJSON方法里做转换逻辑 - 所有入参结构体禁用
json:",required"——Go 的json包根本不识别这个标签,写了等于没写
路径和 HTTP 方法变了,老请求怎么不 404
路由层必须兜底,不能指望客户端升级。一旦 /v1/users 改成 /v2/users,旧客户端立刻 404;改成 POST 就直接 405。中间件统一转格式不可靠,因为 query 转 body、参数重命名、嵌套结构扁平化等逻辑高度路径特异。
- 用
gorilla/mux或gin.Engine显式注册旧路径:r.HandleFunc("/v1/users", newUsersHandler).Methods("GET") - 旧 handler 里手动解析原始请求(
r.URL.Query()或io.ReadAll(r.Body)),映射到新结构体,再调新业务函数 - 绝对不用 HTTP 301/302 重定向——移动端常禁用重定向,且 POST 重定向后变 GET,body 丢失
- 响应头加
X-Deprecated: true,并在日志里记录旧路径访问频次,给客户端团队明确下线信号
v2 模块升级后,旧 import 还能用吗
不能。Go 要求 v2+ 模块必须改路径,example.com/lib 和 example.com/lib/v2 是两个完全独立的模块。旧代码里写的 import "example.com/lib" 不会自动指向 v2,也不会报错,只是继续用 v1——除非你主动改导入路径。
- 升级前,新模块路径必须含
/v2:module example.com/lib/v2,否则go mod tidy会拒绝解析 - 所有引用处要同步改 import:
import "example.com/lib/v2",不能只改go.mod - 想让 v1 和 v2 共存?可以,但得靠路径隔离——
example.com/lib(v1)和example.com/lib/v2(v2)互不影响 -
go list -m all查实际加载版本,go mod graph | grep lib看谁拉进了哪个版本,别只信go.mod里的 require 行
错误码和响应结构一动,旧客户端就 crash
很多团队只测成功路径,结果把 {"error": "xxx"} 换成 {"code": 1001, "message": "xxx"},或把 400 改成 422,旧客户端 JSON 解析失败、字段取不到、switch 分支崩掉——这不是兼容,是埋雷。
- 错误响应结构必须冻结:字段名、类型、嵌套层级全部保持原样,哪怕字段语义已过时
- HTTP 状态码别乱换:400 就始终是 400,401 就始终是 401,别用 422 替代 400,除非客户端明确适配了
- 新增错误码可以,但旧错误码含义不能变;新增字段加
omitempty,避免旧客户端解析时 panic - 所有错误响应走统一封装函数,禁止在 handler 里手拼 map 或 struct,防止某处漏掉字段或改错类型
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











