一次诡异的报错排查:为什么时间戳变成了 ١٧٥٦٦٣٢٧٨
在做服务端登录校验时,我们线上遇到了一个很奇怪的报错:
strconv.Atoi: parsing "١٧٥٦٦٣٢٧٨": invalid syntax
字符串看起来和数字个数对得上,但又像是乱码,Go 服务无法解析这段字符。这背后,其实是一个 国际化 (i18n) 的大坑。
现象复盘
-
服务端使用 Go,解析客户端传来的时间戳:
v, err := strconv.Atoi(tsString)
-
部分用户(主要在阿联酋)登录时,报错
invalid syntax
。 -
打印出来的时间戳长这样:
١٧٥٦٦٣٢٧٨٫١٧٢
。
看上去就是 175663278.172
,为什么不行?
真相揭晓:Locale 搞的鬼
排查后发现:
-
用户手机设置为 阿拉伯语 (Arabic) + 地区 = 阿联酋 (United Arab Emirates);
-
Android/Java 默认的
DecimalFormat
会跟随 Locale 决定数字符号; -
在
ar_AE
下,数字会被格式化成 阿拉伯-印地数字:١٧٥٦٦٣٢٧٨
=175663278
٫
= 小数点
所以客户端传过来的根本不是 ASCII 数字,而是另一套 Unicode 数字。Go 的 Atoi
当然解析失败。
为什么我们测试没复现?
我们在国内测试时,把系统语言切成阿拉伯语,却始终输出 123456...
。
原因是:
- 只改 语言 不够,还要改 地区;
- 必须同时是 语言 = Arabic,地区 = 阿联酋 (ar_AE),并且启用「本地数字」选项,才会显示
١٢٣٤٥٦...
; - MIUI 等国产 ROM 把“地区”设置藏得比较深(设置 → 更多设置 → 地区),所以一开始没找到。
- 更坑的是,不同手机厂商 / Android 版本的 ICU/CLDR 数据不同,有的
ar_AE
默认就用阿拉伯数字,有的还是拉丁数字,所以有时根本复现不了。
解决方案
客户端改造(推荐)
-
强制使用 US Locale
DecimalFormat df = (DecimalFormat) NumberFormat.getNumberInstance(Locale.US); df.applyPattern("#.######"); df.setGroupingUsed(false); String ts = df.format(timeMillis / 1000.0);
-
避免字符串传输
-
JSON 用 number 类型:
{ "ts": 175663278 }
-
而不是字符串:
{ "ts": "١٧٥٦٦٣٢٧٨" }
-
-
如果只需要整数时间戳,直接:
String ts = Long.toString(timeMillis / 1000);
服务端兜底
即使客户端改了,服务端也要健壮,能容错。加个数字规范化函数,把阿拉伯/波斯数字转成 ASCII:
func normalizeDigits(s string) string {out := make([]rune, 0, len(s))for _, r := range s {switch {case r >= '\u0660' && r <= '\u0669': // Arabic-Indic ٠..٩out = append(out, '0'+(r-'\u0660'))case r >= '\u06F0' && r <= '\u06F9': // Persian ۰..۹out = append(out, '0'+(r-'\u06F0'))default:out = append(out, r)}}return string(out)
}
这样再 strconv.Atoi(normalizeDigits(ts))
就不会出错了。
总结经验
- 国际化的坑很多:不要依赖默认 Locale,显式指定才安全。
- 测试要全面:仅切语言不够,还要切地区;不同系统实现也可能有差异。
- 服务端要健壮:客户端可能各种情况,服务端要兜底。
- 最佳实践:跨端传递时间戳、ID 等数据,推荐直接用 数值,而不是字符串。