一次诡异的报错排查:为什么时间戳变成了 ١٧٥٦٦٣٢٧٨

在做服务端登录校验时,我们线上遇到了一个很奇怪的报错:

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 默认就用阿拉伯数字,有的还是拉丁数字,所以有时根本复现不了。

解决方案

客户端改造(推荐)

  1. 强制使用 US Locale

    DecimalFormat df = (DecimalFormat) NumberFormat.getNumberInstance(Locale.US);
    df.applyPattern("#.######");
    df.setGroupingUsed(false);
    String ts = df.format(timeMillis / 1000.0);
    
  2. 避免字符串传输

    • JSON 用 number 类型:

      { "ts": 175663278 }
      
    • 而不是字符串:

      { "ts": "١٧٥٦٦٣٢٧٨" }
      
  3. 如果只需要整数时间戳,直接:

    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)) 就不会出错了。


总结经验

  1. 国际化的坑很多:不要依赖默认 Locale,显式指定才安全。
  2. 测试要全面:仅切语言不够,还要切地区;不同系统实现也可能有差异。
  3. 服务端要健壮:客户端可能各种情况,服务端要兜底。
  4. 最佳实践:跨端传递时间戳、ID 等数据,推荐直接用 数值,而不是字符串。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值