golang.org/x/text/message 是 go 官方推荐的 i18n 方案,需配合 language 包做语言匹配,用 printer per-request 构造以保证并发安全,通过 gotext 工具链加载 .po 翻译,严禁 fmt.sprintf 拼接多语言文本。

Go 语言用 golang.org/x/text/message 做 i18n 最稳妥
标准库不带 i18n,golang.org/x/text/message 是官方维护、生产可用的方案,比自己拼字符串或用第三方轻量库更可靠。它支持复数、性别、嵌套消息、格式化占位符,且能配合 golang.org/x/text/language 做自动语言匹配。
常见错误是直接用 fmt.Sprintf 拼接翻译文本——中文没问题,但德语、阿拉伯语等会因词序、格变化、复数规则崩掉。比如 “You have 1 message” 和 “You have 5 messages” 在英语里只差一个 s,但在俄语里动词变位、名词格、形容词性数格全得跟着变。
- 必须用
message.Printer实例调用Printf/Sprintf,不能绕过 - 消息模板写在
.po或硬编码进message.Catalog,别散落在代码各处 - 语言标签用
language.Make("zh-CN"),别传"zh"或"cn"这种非标准值,否则匹配失败静默回退到默认语种
如何加载多语言消息(.po 文件)
golang.org/x/text/message 本身不读文件,需搭配 golang.org/x/text/message/pipeline 或手动解析 .po。更现实的做法是用 gotext 工具链:它把 .po 编译成 Go 源码,再静态注入 message.Catalog。
典型流程:go generate 触发 gotext extract 扫描 msg 调用 → 生成 messages.gotext.json → 翻译人员填 .po → gotext generate 输出 catalog.go。
- 不要手写
catalog.go,字段名/结构体嵌套稍错就 panic -
gotext默认只识别msg.Printf形式,若用msg.Sprintf需加-srccomment并在注释写//go:generate gotext ... - 编译后消息体是二进制数据,体积影响小;但每次增删语言都要重新
go generate,CI 流程里别漏掉
HTTP 请求中怎么自动选语言
靠解析 Accept-Language 头,用 language.ParseAcceptLanguage 得到优先级列表,再用 language.NewMatcher 匹配你支持的语言集。别自己 split + strings.Contains,RFC 7231 里权重(q 值)、范围匹配(zh 匹配 zh-CN)、区域子标签(en-Latn-US)都得按标准走。
- Matcher 初始化一次即可,别每次请求都 new,它内部有缓存
- 用户未带头时,fallback 到服务端默认语言(如
language.English),别返回空或 panic - 如果前端传了
lang=ja查询参数,要和Accept-Language做优先级协商,通常参数 > 头 > 默认
为什么 message.Printer 必须 per-request 构造
因为 Printer 持有语言信息和格式化上下文,是**非线程安全**的。多个 goroutine 共享一个 Printer 实例,可能在并发调用 Printf 时混用语言或格式器,输出错乱文本(比如中文界面突然冒出半句德语)。
- 在 HTTP handler 里,从 context 取语言 → new
message.Printer→ 用完丢弃,别塞进 struct 字段长期持有 - 如果用了 Gin/Echo,可以写个中间件注入
*message.Printer到c.Request.Context(),但注意别跨 goroutine 传递 - 日志场景例外:全局 logger 可用
message.NewPrinter(language.Und)+ 显式传语言,但普通业务逻辑不推荐
最易被忽略的是:即使所有用户都用同一种语言,只要存在并发请求,Printer 就不能复用。这不是性能问题,是正确性红线。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











