适用读者:被 Go 的字符串长度、
byte/rune搞晕的人。
前置:S09-B01。
实测环境:Go 1.22.2(2026-11-05)。
覆盖旧文:A-1445(64) A-1446(22) A-1447(32) A-1448(16) A-1449(128) A-1450(27) = 289 点。
覆盖扩展:E-S09-002(UTF-8 变长编码)、E-S09-003(零值设计)。
上一篇:S09-B01 · Go 的定位、安装与第一个程序
下一篇:S09-B03 · 常量、iota 与指针。
1. 一句话结论
Go 的字符串是"只读字节序列",len() 返回字节数,不是字符数。 中文在 UTF-8 里占 3 字节,所以 len("中")=3。
实测:
str := "中文abc"
fmt.Printf("len(bytes)=%d len(runes)=%d str[0]=%d\n", len(str), len([]rune(str)), str[0])
// 输出:len(bytes)=9 len(runes)=5 str[0]=228
| 表达式 | 结果 | 含义 |
|---|---|---|
len(str) |
9 | 字节数(2 个中文 ×3 + 3 个 ASCII) |
len([]rune(str)) |
5 | 字符数 |
str[0] |
228 | "中"的 UTF-8 首字节(0xE4) |
2. 变量声明:三种写法与零值
| 写法 | 例子 | 说明 |
|---|---|---|
| 显式类型 | var a int = 1 |
最啰嗦 |
| 类型推断 | var a = 1 |
常用 |
| 短声明(函数内) | a := 1 |
最常用;只能在函数内 |
| 批量 | var ( a int; b string ) |
包级变量常用 |
零值(E-S09-003):Go 没有"未初始化"概念,声明即赋零值。
| 类型 | 零值 |
|---|---|
| 数值 | 0 |
| 字符串 | "" |
| 布尔 | false |
| 指针/切片/map/通道/接口/函数 | nil |
这解释了 map 的行为(B05):m["missing"] 返回零值而不是 panic。
3. 数值类型:整型与浮点
3.1 整型
| 类型 | 范围 | 用途 |
|---|---|---|
int / uint |
平台相关(64 位机器上 64 位) | 默认用 int |
int8/16/32/64 |
明确位宽 | 协议、二进制格式 |
uint8= byte |
0–255 | 字节 |
rune = int32 |
Unicode 码点 | 字符 |
关键:byte 与 rune 不是"两个新类型",是 uint8 与 int32 的别名——但语义完全不同(B02 §6)。
3.2 浮点
实测(Go 1.22.2)三个容易搞错的比较:
var f32 float32 = 0.1
var f64 float64 = 0.1
fmt.Println(f32 == float32(f64)) // true ← 都先转成 float32,值相同
fmt.Println(float64(f32) == f64) // false ← 转成 float64 后差 1.49e-09
const c1, c2 = 0.1, 0.2
fmt.Println(c1+c2 == 0.3) // true ← 常量运算在编译期精确计算
var a, b float64 = 0.1, 0.2
fmt.Println(a+b == 0.3) // false ← 运行时是 0.30000000000000004441
| 结论 | 说明 |
|---|---|
float64(float32) 会引入误差 |
1.49e-09,足以让 == 失败 |
| 常量运算精确 | Go 的无类型常量用高精度计算,所以 0.1+0.2==0.3 在常量层面成立 |
| 变量运算不精确 | 一到运行时就是 IEEE 754,0.30000000000000004441 |
规则:默认用 float64;浮点比较不要用 ==(尤其涉及变量运算),要用误差范围。"0.1+0.2≠0.3"这句流行说法在 Go 的常量里是错的——这个区别很多人不知道。
4. 布尔与字符串
bool只有true/false,没有隐式转换(if 1 {}编译报错)。- 字符串不可变:
s[0] = 'x'编译报错;要改只能重新拼接或转[]byte。
5. byte 与 rune:遍历的两种方式(E-S09-002)
str := "中文abc"
// ① 按字节
for i := 0; i < len(str); i++ { fmt.Printf("%d ", str[i]) }
// ② 按 rune
for i, r := range str { fmt.Printf("%d:%c ", i, r) }
实测差异:
| 方式 | 输出特点 |
|---|---|
| 按字节 | 逐个打印 0–255 的数值,中文会被拆成 3 个"半个字" |
| 按 rune | 每次一个完整字符,i 是该字符的起始字节偏移 |
这就是"为什么 range 字符串返回 (字节偏移, rune)"的原因——不是 (索引, 字符)。
5.1 UTF-8 编码规则(为什么中文是 3 字节)
| 字符范围 | 字节数 | 首字节模式 |
|---|---|---|
| ASCII(U+0000–007F) | 1 | 0xxxxxxx |
| 拉丁扩展 | 2 | 110xxxxx |
| 中文常用(U+4E00–9FFF) | 3 | 1110xxxx |
| emoji 等 | 4 | 11110xxx |
实测:str[0]=228 = 0xE4 = 11100100 → 匹配 1110xxxx,说明从字节 0 开始是一个 3 字节字符。这就是"按字节截断会切坏中文"的原理。
6. 五个误区
| 误区 | 真相 |
|---|---|
"len(s) 是字符数" |
是字节数;字符数用 len([]rune(s)) 或 utf8.RuneCountInString |
"s[i] 返回字符" |
返回第 i 个字节(byte) |
"range 给的是索引" |
给的是字节偏移,不是字符序号 |
| "字符串可以原地修改" | 不可变,要改先转 []byte |
"byte 和 rune 一样" |
一个是字节(uint8),一个是字符(int32) |
7. 自检
len("中文abc")是多少?为什么?str[0]返回什么类型?值是多少(对"中")?for i, r := range s里的i是什么?- 为什么按字节截断会切坏中文?
- 声明
var p *int后p的值是什么?
8. 下一篇
S09-B03 · 常量、iota 与指针
下一篇讲 Go 的两个"反直觉":常量可以无类型(untyped),指针不能做算术——以及它们各自带来的好处。
本篇属于系列《S09 · Go 语言》。旧文来源:A-1445–A-1450;扩展点 E-S09-002/003。所有输出为 Go 1.22.2 实测。