Go错误值不支持i18n,需用占位符ID和message.Printer实现可翻译结构体,避免硬编码字符串、%w包装及全局locale依赖,确保错误构造时显式注入Printer。

Go 错误值本身不支持 i18n,必须包装或重构
Go 的 error 接口只规定了 Error() 方法返回 string,它不携带语言上下文、参数或翻译能力。直接用 fmt.Errorf("用户不存在") 无法动态切语言——你拿到的永远是硬编码中文。真要 i18n,得绕过“错误即字符串”的惯性思维,把错误建模成可翻译的结构体。
- 别在
errors.New或fmt.Errorf里写自然语言;改用占位符 ID,比如"user_not_found" - 错误类型需实现自定义
Error(),内部调用翻译函数(传入当前 locale 和参数) - 避免在 error 实现里做 I/O 或同步锁——翻译函数必须快且无副作用
- HTTP handler 中的 locale 通常来自 header(
Accept-Language)或路由参数,别从 error 里反推
用 golang.org/x/text/message 做运行时翻译最轻量
不用引入 full-fledged i18n 框架(如 go-i18n),message 包足够:它专注格式化,不绑定存储、热加载或模板语法,适合错误消息这种短文本场景。
- 预定义 message.Catalog,为每个错误 ID 注册多语言模板:
catalog.Set(language.English, "user_not_found", "User {{.ID}} not found") - 在 error 实现中持有
*message.Printer,调用p.Sprintf("user_not_found", map[string]interface{}{"ID": 123}) - 注意:同一个
Printer实例不能跨 goroutine 复用(它含 state);建议每次请求 new 一个,或用With构造临时实例 - 若错误需透出到 API 响应,别让
Error()返回翻译后字符串——而是暴露Translate(lang language.Tag) string方法,由上层统一调用
HTTP handler 中传递 locale 到错误构造处容易断链
常见错误是把 locale 存在全局变量或 context 跨多层传递,结果某次 panic 或中间件跳过导致 fallback 成 English。更稳的方式是:错误对象自己不依赖 context,而由创建者显式注入 printer。
- 定义错误构造函数如
NewUserNotFoundError(id int, p *message.Printer) error,强制调用方提供翻译能力 - 在 handler 入口解析 locale → 创建
*message.Printer→ 一路透传(或存入 request-scoped struct) - 避免在 dao 层或 model 层 new error;错误构造推迟到 handler 或 service 边界,那里才有 locale 上下文
- 如果用了 Gin/Echo,可在 middleware 中把
*message.Printer写入c.Set("printer", p),后续 handler 用c.MustGet("printer").(*message.Printer)
嵌套错误和 %w 包装会丢失翻译能力
用 fmt.Errorf("failed to save: %w", err) 时,如果 err 是你自定义的可翻译 error,它的 Error() 会被调用,但外层 fmt.Errorf 仍按默认逻辑拼接字符串——结果是英文前缀 + 中文后缀,或全乱码。
立即学习“go语言免费学习笔记(深入)”;
- 不要用
%w包装可翻译 error;改用自定义 wrapper 类型,实现自己的Unwrap()和Error() - 例如:
type TranslatedError struct { Cause error; Printer *message.Printer },其Error()返回printer.Sprintf(...) - 若必须兼容标准库行为,至少确保 wrapper 的
Error()不调用被包 error 的Error(),而是走翻译路径 - log 打印错误时,用
%+v可能触发未预期的Error()调用——生产环境日志建议只记录 error ID 和参数,翻译留到展示层










