1338 字
约 4 分钟
8
S09-B02 · 变量与基本类型:为什么 `len("中文")` 是 6 不是 2
2024-12-31
2026-11-07

适用读者:被 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. 自检

  1. len("中文abc") 是多少?为什么?
  2. str[0] 返回什么类型?值是多少(对"中")?
  3. for i, r := range s 里的 i 是什么?
  4. 为什么按字节截断会切坏中文?
  5. 声明 var p *int 后 p 的值是什么?

8. 下一篇

S09-B03 · 常量、iota 与指针

下一篇讲 Go 的两个"反直觉":常量可以无类型(untyped),指针不能做算术——以及它们各自带来的好处。


本篇属于系列《S09 · Go 语言》。旧文来源:A-1445–A-1450;扩展点 E-S09-002/003。所有输出为 Go 1.22.2 实测。

S09-B02 · 变量与基本类型:为什么 `len("中文")` 是 6 不是 2
https://www.clxhxhhr.top/posts/5300/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。