Go 遍历中文字符串乱码:byte 和 rune 到底差在哪,len 为什么不对
Go 遍历中文字符串乱码:byte 和 rune 到底差在哪,len 为什么不对写一个功能:截取用户昵称的前 5 个字给头像占位。英文测试一切正常,一上中文昵称,截出来的是半个乱码方块,len(name)返回的数字也比肉眼数的字数大好几倍。这是几乎每个 Go 新手都会撞上的坑——问题不在你的截取逻辑,而在于你把「字节」当成了「字符」。这篇把 byte 和 rune 的区别、以及遍历/截取中文的正确姿势一次讲清。先复现:len 和遍历都「不对」packagemainimportfmtfuncmain(){s:你好Gofmt.Println(len(s))// 你以为是 4,实际输出 8// 想逐个字符打印fori:0;ilen(s);i{fmt.Printf(%c ,s[i])// 中文全变成乱码}}输出:8 ä ½ å ¥ ½ G olen(你好Go)是 8 不是 4,而且用下标s[i]打出来中文全乱了。要搞清为什么,得先知道 Go 的 string 到底存的是什么。原理:string 底层是 UTF-8 字节序列Go 的 string 本质是一串只读的字节(byte),而且约定用UTF-8编码存放文本。UTF-8 是变长编码:一个 ASCII 字符(英文、数字)占1 字节;一个常见汉字占3 字节;有些 emoji 占4 字节。所以 “你好Go” 你(3 字节)好(3 字节)G(1 字节)o(1 字节) 8 字节。len(s)返回的是字节数,不是字符数。而s[i]取的是第 i 个字节,你把一个汉字的三分之一拎出来单独打印,当然是乱码。这里就要引出两个关键类型:byte:其实是uint8的别名,表示一个字节(0~255)。s[i]得到的就是 byte。rune:是int32的别名,表示一个Unicode 码点(code point),也就是我们直觉上的「一个字符」。一个汉字是一个 rune,尽管它在 UTF-8 里占 3 个 byte。记住这句话:byte 是存储单位,rune 是字符单位。中文场景下二者不是一回事。正确遍历:用 for-range,拿到的是 runeGo 的for range遍历 string 时,很贴心地做了 UTF-8 解码——它每次吐给你的是一个rune,而且索引i是该字符首字节在字节序列里的下标(所以索引会跳):s:你好Gofori,r:ranges{// i 是字节下标(会跳),r 是 rune(完整字符)fmt.Printf(下标 %d - %c (码点 %d)\n,i,r,r)}输出:下标 0 - 你 (码点 20320) 下标 3 - 好 (码点 22909) 下标 6 - G (码点 71) 下标 7 - o (码点 111)看下标:0、3、6、7——每个汉字占了 3,英文占 1,和 UTF-8 的字节宽度完全对上。只要你想按「字符」处理中文,就用 for-range,不要用for i:0; ilen(s); i加下标。正确取字符数:utf8.RuneCountInString 或转 []rune想知道「这个字符串有几个字」,别用len,用标准库:importunicode/utf8s:你好Gofmt.Println(utf8.RuneCountInString(s))// 4,正确的字符数或者把 string 转成[]rune,它的长度就是字符数:runes:[]rune(s)fmt.Println(len(runes))// 4正确截取:先转 []rune 再切回到开头「取前 5 个字」的需求。直接s[:5]是按字节切,会把一个汉字拦腰砍断产生乱码。正确做法是转成[]rune,按字符下标切,再转回 string:funcfirstN(sstring,nint)string{r:[]rune(s)iflen(r)n{returns// 不足 n 个字,原样返回,避免越界}returnstring(r[:n])// 按 rune 切,不会切坏中文}funcmain(){fmt.Println(firstN(你好世界Go语言,5))// 你好世界G}[]rune(s)会一次性把整个字符串解码成字符数组,截完再string(...)编码回 UTF-8。对昵称这种短字符串完全没问题;但要注意如果字符串非常大、又只取开头几个字,转整个[]rune会有一次全量分配的开销,那种场景可以改用utf8.DecodeRuneInString逐个手动往前挪,不过日常需求用[]rune足够清晰。一个进阶坑:byte 和 rune 的强转别搞混s:Go语言// 转 []byte:得到原始 UTF-8 字节,长度是字节数b:[]byte(s)fmt.Println(len(b))// 8// 转 []rune:得到字符,长度是字符数r:[]rune(s)fmt.Println(len(r))// 4什么时候用哪个?做网络传输、写文件、算哈希这类「按字节处理」的场景用[]byte;做文本处理——数字数、截断、反转、逐字符判断——用[]rune。比如反转一个含中文的字符串,必须按 rune 反转,按 byte 反转会把每个汉字的 3 个字节也倒过来,直接乱码:funcreverse(sstring)string{r:[]rune(s)fori,j:0,len(r)-1;ij;i,ji1,j-1{r[i],r[j]r[j],r[i]}returnstring(r)}funcmain(){fmt.Println(reverse(abc中文))// 文中cba,正确}小结Go 的 string 底层是UTF-8 编码的只读字节序列。len(s)返回字节数不是字符数;s[i]取的是第 i 个字节,直接拿它处理中文必乱码。byte(uint8)是存储单位,rune(int32)是字符单位。一个汉字 1 个 rune 3 个 byte。按字符遍历用for-range(自动解码成 rune,索引按字节跳);数字符数用utf8.RuneCountInString或len([]rune(s));按字符截取/反转先转[]rune再操作。选型口诀:按字节传输/存储用 []byte,按字符做文本处理用 []rune。一句话记忆点:在 Go 里处理中文,凡是涉及「一个字」的逻辑,就把 string 转成 []rune,别信 len,别用下标。