unicode.isdigit(r) 正确判断数字字符,需传入 rune 类型(如 for _, r := range s),而非 byte;它包含 ASCII ‘0’–’9′ 及 unicode 数字字符,但直接索引字符串取 byte 会出错。

怎么用 unicode.IsDigit 判断真正的数字字符
别直接拿 unicode.IsDigit 去扫字符串里“是不是数字”,它只认 Unicode 数字属性(比如 U+0660 阿拉伯-印地数字 ٠),不认 ASCII 的 '0'–'9'。很多中文或阿拉伯语环境下的数字会误判为 false。
-
unicode.IsDigit检查的是UnicodeCategory.Decimal_Number,范围远超 0–9,但也不包含 0–9 —— 等等,它其实包含!但仅当传入的是rune(不是byte)且该 rune 确实被归类为数字时才返回 true;ASCII 数字'0'–'9'是满足的,但容易在字符串遍历时被当成byte错误处理 - 常见错误:用
for i := range s { if unicode.IsDigit(s[i]) {...}}—— 这里s[i]是byte,不是rune,会导致非 ASCII 区域崩溃或恒为 false - 正确写法必须先转
rune:for _, r := range s { if unicode.IsDigit(r) { ... }} - 如果只要 ASCII 数字,用
r >= '0' && r 更快、更明确,无 Unicode 开销
unicode.IsLetter 为什么有时漏掉中文/日文字符
它不会漏——只要字符在 Unicode 中被标记为字母(Letter_Uppercase / Letter_Lowercase / Letter_Titlecase / Letter_Modifier / Letter_Other),unicode.IsLetter 就返回 true。中文、日文汉字、平假名、片假名全在 Letter_Other 里,肯定能过。
- 真正出问题的地方是:你用了
strings.Fields或strings.Split后按字节切分,再对byte调用unicode.IsLetter,结果 panic 或返回 false - 另一个坑:某些“看起来像字母”的符号(如罗马数字Ⅰ、Ⅴ、Ⅹ)属于
Number_Roman,不是 Letter,unicode.IsLetter返回 false - 若需兼容字母+罗马数字+带圈数字等,得组合判断:
unicode.IsLetter(r) || unicode.IsNumber(r) && (unicode.IsRoman(r) || unicode.IsDecimal(r))——但标准库没
IsRoman,得自己查表或用unicode.Is+unicode.Roman(不存在),实际只能靠范围判断或第三方包
标点符号判断别只盯 unicode.IsPunct
unicode.IsPunct 只覆盖 Pc(Connector_Punctuation)、Pd(Dash_Punctuation)到 Pz(Close_Punctuation)这几类,漏了大量常用符号:英文单引号 '、反引号 `、ASCII 斜杠 /、甚至中文顿号 、 和书名号《》都不在其中。
-
''属于Pc,'.'是Pc,但'/ '是Po(Other_Punctuation),而unicode.IsPunct不包含Po - 更稳妥的方式是用
unicode.Is+ 显式类别:unicode.Is(unicode.P, r) // P = 所有标点大类(Pc/Pd/Pe/Pf/Pi/Po/Pr/Ps) - 注意:
unicode.P会把中文逗号、句号、问号、感叹号、省略号(…)全包进来,但也会包括一些你可能不想算作“标点”的字符,比如「々」(重复记号,JIS X 0213 中属Po) - 如果业务只要 ASCII 标点,直接写白名单更可控:
func isASCIIPunct(r rune) bool { return r >= '!' && r <= '/' || r >= ':' && r <= '@' || r >= '[' && r <= '`' || r >= '{' && r <= '~' }
性能敏感时,unicode.Is* 函数能不能 inline
不能。这些函数内部调用的是生成的查找表和位运算逻辑,Go 编译器不会内联它们——benchmark 显示,纯 ASCII 判断下,手写 r >= 'a' && r 比 <code>unicode.IsLower 快 3–5 倍;混合 Unicode 字符时差距缩小,但仍有 1.5 倍左右开销。
- 高频路径(如 parser、tokenizer 内层循环)建议对 ASCII 范围做快速前置判断,再 fallback 到
unicode.Is* - 例如判断是否为标识符首字符:
if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r == '_' { /* fast path */ } else { return unicode.IsLetter(r) || r == '$' /* Go 允许 $ 在 identifier 中 */ } - 别为了“统一”强行全用
unicode.Is*—— 它们设计目标是正确性,不是速度
实际写 parser 或输入校验时,最麻烦的从来不是“哪个函数能用”,而是混用 byte/rune、忽略 ASCII 与 Unicode 范围差异、以及想当然认为某个符号“应该算标点”却没查 Unicode 类别。查 UnicodeData.txt 两分钟,比调半天 IsPunct 省三小时。