引入

字符串里提到 UTF-8 编码和 range 遍历拿到的是字符而不是字节。那单个字符在 Go 里到底是什么类型?答案是 runebyterune 代表一个 Unicode 码点,byte 代表一个字节。搞清楚这两个类型的区别,才能正确处理中文、emoji 等多字节字符。

正文

定义

定义

runeint32 的别名,代表一个 Unicode 码点(即一个字符)。byteuint8 的别名,代表一个字节。字符串在内存中以 byte 序列存储(UTF-8 编码),但逻辑上由 rune 序列组成。

语法

// rune:单个字符(Unicode 码点)
var r rune = 'A'      // 65
var r2 rune = ''    // 20320
 
// byte:单个字节
var b byte = 'A'      // 65
var b2 byte = 0xFF    // 255

例子

rune 是什么

var r1 rune = 'A'
var r2 rune = ''
var r3 rune = '😀'
 
fmt.Printf("%c %U %d\n", r1, r1, r1)  // A U+0041 65
fmt.Printf("%c %U %d\n", r2, r2, r2)  // 你 U+4F60 20320
fmt.Printf("%c %U %d\n", r3, r3, r3)  // 😀 U+1F600 128512

rune 本质上就是 int32,每个 rune 值对应 Unicode 标准里的一个码点。用单引号 '…' 写出来的是 rune 字面量。

字符Unicode 码点rune 值
'A'U+004165
'你'U+4F6020320
'😀'U+1F600128512

byte 是什么

var b1 byte = 'A'   // 65
var b2 byte = 255   // byte 最大值
// var b3 byte = 256  // 编译报错:溢出
 
fmt.Printf("%c %d\n", b1, b1)  // A 65

byte 就是 uint8,范围 0 ~ 255。它代表一个字节的数据,是字符串在内存中的基本单位。

字符串的 byte 和 rune

s := "你好"
 
// 按 byte 遍历(拿到每个字节)
for i := 0; i < len(s); i++ {
    fmt.Printf("byte[%d] = %d\n", i, s[i])
}
// byte[0] = 228
// byte[1] = 189
// byte[2] = 160
// byte[3] = 229
// byte[4] = 165
// byte[5] = 177
 
// 按 rune 遍历(拿到每个字符)
for i, r := range s {
    fmt.Printf("rune[%d] = %c (%U)\n", i, r, r)
}
// rune[0] = 你 (U+4F60)
// rune[3] = 好 (U+597D)

len(s) 返回字节数(6),range 遍历拿到的是 rune(2 个字符)。注意 range 返回的索引是字节偏移量,不是字符索引。

UTF-8 编码

s := "A你😀"
 
// 每个字符占几个字节?
fmt.Printf("字节数: %d\n", len(s))                    // 8
fmt.Printf("字符数: %d\n", utf8.RuneCountInString(s))  // 3
字符UTF-8 字节数
ASCII(A-Z, 0-9 等)1 字节
中文、日文等3 字节
emoji 等补充平面字符4 字节

UTF-8 是变长编码,ASCII 字符占 1 字节,和 ASCII 完全兼容。

字符串和 rune 切片互转

s := "你好"
 
// 字符串转 rune 切片
runes := []rune(s)  // [20320 22909]
fmt.Println(len(runes))  // 2(字符数)
 
// rune 切片转字符串
s2 := string(runes)  // "你好"
 
// 用 rune 切片修改字符串中的某个字符
runes[0] = ''
s3 := string(runes)  // "我好" → 不对,是"我好"

因为字符串不可变,要修改某个字符就得先转成 []rune,改完再转回来。

unicode 包

import "unicode"
 
unicode.IsLetter('A')    // true
unicode.IsDigit('9')     // true
unicode.IsUpper('A')     // true
unicode.IsLower('a')     // true
unicode.IsSpace(' ')     // true
unicode.Is(unicode.Han, '')  // true(判断是否为汉字)

unicode 包提供了判断和操作 Unicode 字符的函数。

常见写法

写法说明
'A'rune 字面量
var r rune = '你'声明 rune 变量
var b byte = 65声明 byte 变量
[]rune(s)字符串转 rune 切片
[]byte(s)字符串转字节切片
string(r)rune 转字符串
utf8.RuneCountInString(s)获取字符数
range s按 rune 遍历字符串

特点

  • rune 就是 int32 的别名,代表一个 Unicode 码点
  • byte 就是 uint8 的别名,代表一个字节
  • 单引号 'A'rune,双引号 "A"string
  • 字符串按 UTF-8 编码存储,不同字符占 1~4 字节不等
  • len(s) 返回字节数utf8.RuneCountInString(s) 返回字符数
  • range 遍历字符串时拿到的是 rune,索引是字节偏移量
  • 要修改字符串中的字符,需要先转 []rune 再转回来

理解

runebyte 的本质区别就是字符 vs 字节byte 是存储层面的最小单位,rune 是逻辑层面的一个字符。Go 的字符串底层是 byte 序列,但 range 会自动按 UTF-8 解码成 rune。处理纯 ASCII 文本时 byte 就够了,但涉及中文、emoji 就得用 rune,否则 len() 和索引都会出错。

引出

runeint32byteuint8,这些不同类型之间经常需要互相转换。接下来看 Go 类型转换,学习 Go 里不同类型之间怎么做显式转换、strconv 包的用法以及类型断言。