错误必须用结构体封装键、参数和翻译能力,因go的error接口仅含error()方法且不支持i18n;推荐使用go-i18n/v2实现按请求隔离的localizederror结构体。

错误消息不能靠 fmt.Errorf 硬编码,必须用结构体封装键+参数+翻译能力
为什么 error 接口本身不支持 i18n
Go 的 error 接口只规定一个 Error() 方法返回 string,它不携带语言标签、不存占位符、也不支持运行时切换。你写 fmt.Errorf("用户不存在"),日志里永远是中文;写 fmt.Errorf("User not found"),API 返回永远是英文——没有中间态,也没法动态选。
常见错误现象:Error() 返回值被 HTTP 中间件、gRPC 错误码转换、日志采集器直接消费,一旦硬编码,就锁死语言,后续加多语言等于重写所有错误路径。
- 别在 DAO 层或 model 层调用
fmt.Errorf或errors.New构造带自然语言的错误 - 别把
context.Context一路透传到错误构造点再解析Accept-Language——中间任意一层 panic 或跳过 middleware,locale就断了 - 别用全局
*message.Printer或单例Localizer,goroutine 不安全,且无法按请求隔离语言
推荐方案:go-i18n/v2 + LocalizedError 结构体
用 github.com/nicksnyder/go-i18n/v2/i18n 是目前最稳的选择:它原生支持按请求绑定语言、CLDR 复数规则、JSON/TOML 文件热加载,且错误键(MessageID)和翻译内容完全解耦。
定义错误类型时,不要重写 Error() 做翻译,而是暴露 Translate(lang string) string 方法:
type LocalizedError struct {
cause error
key string
args map[string]interface{}
}
func (e *LocalizedError) Error() string {
return e.key // 默认返回键名,方便日志追踪
}
func (e *LocalizedError) Translate(lang string) string {
cfg := &i18n.LocalizeConfig{
MessageID: e.key,
TemplateData: e.args,
}
msg, _ := localizer.Localize(cfg) // localizer 按请求构造
return msg
}
- 文件命名必须严格为
active.zh-CN.json,不是zh.json或cn.json,否则bundle.LoadMessageFile静默失败 - JSON 内容必须含
description和translation字段,仅{"user_not_found": "用户不存在"}无效 -
args中的 key 名大小写必须与翻译文件中模板变量完全一致,{Name: "Alice"}对应"Hello {Name}",写成{name: "Alice"}就不替换
HTTP handler 中如何安全注入语言上下文
语言信息必须在 handler 入口解析并构造 Localizer,而不是让错误自己去猜。错误对象只负责持有键和参数,不碰 HTTP 头、不依赖 context。
典型流程:
- 从
r.Header.Get("Accept-Language")提取候选语言列表 → 用language.ParseAcceptLanguage解析 - 用
language.NewMatcher(supportedLangs)匹配第一个合法language.Tag(如zh-CNfallback 到zh) - 调
i18n.NewLocalizer(bundle, tag.String())得到 request-scopedLocalizer - 把这个
Localizer作为参数传给 service 函数,或存入requestCtx(context.WithValue),但绝不跨 goroutine 复用
关键点:Localizer 实例不能复用,但也不必每次 new —— 它轻量,构造开销可忽略;真正要避免的是把它存在全局变量或中间件外的包级变量里。
容易被忽略的细节:panic、日志、错误链
panic 不该做国际化,只记录错误键("user_not_found")和参数(map[string]interface{}{"ID": 123})即可。日志系统看到键,再去查当前语言环境下的翻译,而不是在 panic 时强行调 Localize()。
错误链(%w)也要小心:如果上层用 fmt.Errorf("failed to create user: %w", err),而 err 是 *LocalizedError,那 Error() 返回的仍是键名,不是翻译后文本——这反而是对的,因为日志/调试需要原始键来定位问题。
真正需要翻译的,只有最终透出到用户侧的响应体(如 JSON API 的 message 字段),那里才显式调 err.(*LocalizedError).Translate(lang)。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











