最稳妥方案是用golang.org/x/text/message配合message.printer做错误翻译,错误值保持不变,仅在展示时翻译;定义localizableerror包装类型,用英文key和基础类型参数,严格匹配language规范注册bundle,仅对业务错误翻译,状态码硬编码。

用 golang.org/x/text/message 做错误翻译最稳妥
Go 官方生态里没有内置的错误国际化机制,硬套 fmt.Errorf + 多语言 map 容易漏翻译、难维护。真正能落地的方案是用 golang.org/x/text/message 配合 message.Printer,它专为本地化格式化设计,支持复数、性别、嵌套参数,且不侵入业务错误结构。
常见错误现象:errors.New("用户不存在") 直接写死中文,切换语言时只能改代码或加 if 判断,一加多语言就崩;或者自己封装 ErrUserNotFound(en, zh, ja),但字段膨胀、调用时总忘传 locale。
- 使用场景:HTTP handler 返回错误响应、CLI 工具输出提示、gRPC status message
- 关键点:错误值本身保持不变(仍是
error接口),只在最终展示时做翻译 - 不要把翻译逻辑塞进
error.Error()方法——那会让错误日志也变成本地化文本,排查时反而看不懂
定义可翻译的错误类型,别动原始 error 接口
直接给 error 加方法或重写 Error() 是陷阱。正确做法是定义一个带翻译能力的包装类型,比如 LocalizableError,内部存 key 和参数,交由 Printer 渲染。
示例:
type LocalizableError struct {
Key string
Args []interface{}
}
func (e *LocalizableError) Error() string {
return e.Key // 仅作 debug 用,不用于展示
}
// 展示时才调用:
func (e *LocalizableError) Format(p *message.Printer) string {
return p.Sprintf(e.Key, e.Args...)
}
-
Key推荐用英文短语如"user_not_found",不是句子,方便翻译器理解上下文 -
Args里避免传结构体,只传基础类型(string,int,time.Time)——message.Printer对它们有标准格式化规则 - 不要在
Args里塞已翻译的字符串,否则多语言叠加时会乱码
加载语言资源时,locale 字符串必须严格匹配 golang.org/x/text/language 规范
传 "zh-CN" 却没注册对应 bundle,或传 "zh" 却只准备了 "zh-Hans",都会 fallback 到默认语言,而且不报错,极难发现。
实操建议:
- 用
language.Make("zh-CN")构造 tag,别手拼字符串 - bundle 注册必须覆盖所有可能输入:比如用户传
"zh",你得同时注册language.Chinese和language.SimplifiedChinese - HTTP 场景下,从
Accept-Language解析要用language.ParseAcceptLanguage,别用strings.Split - 性能影响:bundle 是全局复用的,初始化一次即可;
message.Printer是无状态的,可按请求新建
HTTP handler 中返回本地化错误时,别在中间件里统一 translate
看到“统一错误处理”就想把所有 error 拦下来翻译?危险。很多错误根本不需要本地化:数据库连接失败、JSON 解析错误、权限校验拒绝——这些对前端/用户没意义,应该原样记日志,只对业务语义错误(如“余额不足”“验证码错误”)做翻译。
所以:
- 定义明确的业务错误类型,比如
ErrInsufficientBalance,实现LocalizableError接口 - 非业务错误(
io.EOF,sql.ErrNoRows)直接透传,或转成统一系统错误码,不翻译 - handler 内部用
printer.Printf替代fmt.Errorf构造响应体,而不是在 middleware 里对任意error调用Format - 容易被忽略的一点:HTTP 状态码不能靠翻译决定——
"user_not_found"对应 404,"invalid_token"对应 401,这个映射必须硬编码,和语言无关
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











