字符串基础
本教程共 80 篇 · 第 12 篇 · 更新于 2026-07-27 · 约 8 分钟阅读
12. 字符串基础
本节目标:理解 Go 字符串的本质,掌握两种字面量写法,学会索引访问、切片和拼接,搞懂 UTF-8 编码。
字符串是什么
在 Go 里,字符串本质上是不可变的字节序列。
注意两个关键词:不可变和字节序列。
- 不可变:字符串创建后,内容不能修改
- 字节序列:字符串底层是一串字节,不是一串字符
这个定义很重要。在 Go 里,一个中文字符可能占 3 个字节,所以字符串的「长度」和「字符数」不一定相等。
s := "Hello"
fmt.Println(len(s)) // 5,每个字符占1字节
s2 := "你好"
fmt.Println(len(s2)) // 6,每个中文字符占3字节
字符串字面量
Go 有两种写字符串的方式。
普通字符串(双引号)
用双引号包围,支持转义字符,不能跨行:
s := "Hello\nWorld" // \n 是换行
fmt.Println(s)
// 输出:
// Hello
// World
常用转义字符:
| 转义 | 含义 |
|---|---|
\n | 换行 |
\t | 制表符 |
\" | 双引号 |
\\ | 反斜杠 |
\r | 回车 |
原始字符串(反引号)
用反引号包围,不处理转义,可以跨行:
s := `这是一个
多行字符串
\t不会被转义,原样输出`
fmt.Println(s)
输出:
这是一个
多行字符串
\t不会被转义,原样输出
原始字符串特别适合写正则表达式、HTML 模板、JSON 等包含大量反斜杠的内容:
regex := `\d+\.\d+` // 不用写 \\d
html := `<div class="container">Hello</div>`
Tip如果你发现自己在普通字符串里写了很多
\\,换成反引号就清爽了。
字符串的长度
len() 返回字符串的字节数,不是字符数:
s := "Go语言"
fmt.Println(len(s)) // 8,不是4
“Go” 占 2 字节,“语言” 各占 3 字节,总共 2 + 3 + 3 = 8 字节。
如果你想统计字符数(Unicode 码点数),需要把字符串转成 []rune:
s := "Go语言"
fmt.Println(len([]rune(s))) // 4,4个字符
或者用 utf8.RuneCountInString:
import "unicode/utf8"
s := "Go语言"
fmt.Println(utf8.RuneCountInString(s)) // 4
索引访问
用 s[i] 访问第 i 个字节(从 0 开始):
s := "Hello"
fmt.Println(s[0]) // 72('H' 的 ASCII 码)
注意返回的是 byte 类型(uint8),不是字符。要得到字符,需要转换:
fmt.Println(string(s[0])) // H
Warning如果字符串包含中文,直接用索引访问可能会拿到半个字符:
s := "你好"
fmt.Println(s[0]) // 228,是"你"的第一个字节,不是完整的字符
这是因为中文字符占多个字节,s[0] 只取了第一个字节。要正确遍历字符,后面会讲 for-range。
字符串切片
可以用切片语法截取子串:
s := "Hello, World"
sub := s[0:5] // "Hello"
sub2 := s[7:] // "World"
sub3 := s[:5] // "Hello"
切片操作返回的还是 string 类型,底层数据共享。
Note对中文字符串做切片时要小心,如果切在了一个字符的中间,会得到无效的 UTF-8 序列。
字符串不可变
字符串创建后不能修改:
s := "Hello"
s[0] = 'h' // 报错:cannot assign to s[0]
如果你想修改,需要先转成 []byte 或 []rune,改完再转回来:
s := "Hello"
b := []byte(s)
b[0] = 'h'
s = string(b)
fmt.Println(s) // hello
不过这个操作会分配新内存,有性能开销。如果频繁修改,考虑直接用 []byte。
字符串拼接
用 + 号
s := "Hello" + ", " + "World"
fmt.Println(s) // Hello, World
用 += 追加
s := "Hello"
s += ", World"
fmt.Println(s) // Hello, World
用 fmt.Sprintf
name := "Go"
s := fmt.Sprintf("Hello, %s!", name)
用 strings.Join
拼接多个字符串时,strings.Join 更高效:
parts := []string{"Hello", "World", "Go"}
s := strings.Join(parts, ", ")
fmt.Println(s) // Hello, World, Go
用 strings.Builder
循环里大量拼接字符串时,用 strings.Builder 性能最好:
var builder strings.Builder
for i := 0; i < 100; i++ {
builder.WriteString("item")
}
result := builder.String()
Tip少量拼接用
+就行,循环里拼接一定要用strings.Builder,否则每次拼接都会分配新内存,性能很差。
UTF-8 编码
Go 字符串使用 UTF-8 编码。UTF-8 是变长编码:
| 字符范围 | 字节数 |
|---|---|
| ASCII(0-127) | 1 字节 |
| 拉丁文等 | 2 字节 |
| 中文等常见字符 | 3 字节 |
| 表情符号等 | 4 字节 |
这意味着同一个字符串里,不同字符占的字节数可能不同。
s := "A中"
// 'A' 占 1 字节,'中' 占 3 字节
fmt.Println(len(s)) // 4
Go 标准库 unicode/utf8 提供了 UTF-8 相关的工具:
import "unicode/utf8"
s := "Hello, 世界"
// 统计字符数
fmt.Println(utf8.RuneCountInString(s)) // 9
// 解码第一个字符
r, size := utf8.DecodeRuneInString(s)
fmt.Printf("第一个字符:%c,占%d字节\n", r, size) // H,占1字节
字符串比较
字符串可以用 ==、!=、<、> 等比较:
fmt.Println("abc" == "abc") // true
fmt.Println("abc" < "abd") // true,按字典序比较
fmt.Println("abc" != "ABC") // true,区分大小写
比较是按字节逐个对比的,所以中文比较的结果可能不符合你的预期。如果要按人类语言习惯排序,需要用 golang.org/x/text/collate 包(不在标准库里,得单独拉取)。
字符串遍历
按字节遍历
s := "Hello"
for i := 0; i < len(s); i++ {
fmt.Printf("%c ", s[i]) // H e l l o
}
按字符遍历(for-range)
s := "Hello,世界"
for i, r := range s {
fmt.Printf("位置%d: %c\n", i, r)
}
for-range 会自动按 UTF-8 解码,每次迭代得到一个 rune(字符)。i 是字节位置,r 是 Unicode 码点。
Note
for-range遍历字符串时,i不是连续递增的。遇到多字节字符,i会跳过相应的字节数。
常见问题
len() 返回的不是字符数
这是初学者最容易误解的地方。len(s) 返回字节数,不是字符数。需要字符数用 utf8.RuneCountInString(s)。
修改字符串报错
字符串不可变,直接 s[0] = 'x' 会报错。要修改就转成 []byte 或 []rune。
拼接性能差
循环里用 + 拼接字符串,每次都会分配新内存。改用 strings.Builder。
学完这节你能做什么
- 理解字符串是不可变的字节序列
- 用双引号和反引号写不同类型的字符串
- 用
len()获取字节数,用utf8.RuneCountInString获取字符数 - 用切片和索引访问字符串
- 高效拼接字符串
下一节讲 rune 和 byte 的区别。