Go 并发编程(基础入门)第 7 课:字符串与 rune
1. 学习目标
完成本课学习后,你将能够:
- 理解 Go 语言中
string的底层结构与特性(UTF-8 编码的字节序列)。 - 区分
byte(字节)与rune(Unicode码点)的概念,并知道何时使用哪一个。 - 掌握使用
for range循环正确遍历包含多字节字符(如中文)的字符串。 - 运用标准库
strings包和切片操作对字符串进行基本处理。 - 避免初学者在字符串操作中最常见的几个错误。
2. 核心概念
字符串 (string):不可变的字节序列
在 Go 中,字符串本质上是一个只读的字节切片 ([]byte)。它的内容使用 UTF-8 编码。这意味着一个英文字符通常占用1个字节,而一个中文字符通常占用3个字节。
var s string = "Hello, 世界"
字符串 s 在内存中是一串字节:72 101 108 108 111 44 32 228 184 150 231 149 140。它有两个关键特性:
- 不可变性:一旦创建,字符串的内容不能被修改。任何看起来像“修改”的操作(如
+=拼接)都会创建一个新的字符串。 - 长度:
len(s)返回的是字符串的字节数,而不是字符数。
byte 与 rune:字符的两种视角
byte:是uint8的别名,代表一个字节。它是网络I/O、文件读写等底层操作的基本单位。rune:是int32的别名,代表一个 Unicode码点(可以理解为一个抽象的“字符”)。它是处理文本、字符、字符串时更合适的单位。
我们通过 []byte(s) 和 []rune(s) 可以将字符串分别转换为字节切片和码点切片。
s := "Hi, 世"
byteSlice := []byte(s) // [72 105 44 32 228 184 150] 7个字节
runeSlice := []rune(s) // [72 105 44 32 19990] 5个码点
for range 循环:遍历字符串的正确方式
直接用 for i := 0; i < len(s); i++ 遍历字符串,得到的是字节,对中文等多字节字符会产生乱码。for range 循环会自动按UTF-8编码将字符串解析为一个个 rune,是处理文本的首选。
s := "世界, Hello"
for index, r := range s {
fmt.Printf("索引: %d, 码点: %d, 字符: %c\n", index, r, r)
}
// 输出:
// 索引: 0, 码点: 19990, 字符: 世
// 索引: 3, 索引: 30028, 字符: 界
// ...
注意:index 是该 rune 起始字节的索引。
3. 代码示例
示例1:理解长度与遍历
package main
import "fmt"
func main() {
str := "Go语言,你好!"
// 1. 长度的区别
fmt.Printf("字符串: %s\n", str)
fmt.Printf("字节长度 (len): %d\n", len(str)) // 13个字节 (英文1字节,中文3字节)
fmt.Printf("字符长度 (rune): %d\n", len([]rune(str))) // 7个字符
// 2. 使用 for range 遍历字符
fmt.Println("\n遍历 rune (字符):")
for i, char := range str {
// i 是该字符起始字节的索引,char 是对应的 rune
fmt.Printf("索引: %2d, 字符: %c\n", i, char)
}
// 3. 对比:使用索引遍历字节(错误方式)
fmt.Println("\n按字节索引遍历 (可能出错):")
for i := 0; i < len(str); i++ {
fmt.Printf("%c ", str[i]) // 对中文字符会输出乱码(字节的一部分)
}
fmt.Println()
}
示例2:字符串与rune切片操作
package main
import (
"fmt"
"strings"
)
func main() {
s := "Hello, 世界!"
runeSlice := []rune(s)
// 1. 从 rune 切片中提取子串
sub := string(runeSlice[7:9]) // 从第8个字符开始,取2个字符
fmt.Println("提取的子串:", sub) // 输出: 世界
// 2. 反转字符串(中文也能正确处理)
runes := []rune(s)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
reversed := string(runes)
fmt.Println("反转后的字符串:", reversed) // 输出: !界世 ,olleH
// 3. 使用 strings 包进行查找和替换
fmt.Println("包含“世界”吗?", strings.Contains(s, "世界"))
newStr := strings.ReplaceAll(s, "世界", "World")
fmt.Println("替换后:", newStr) // 输出: Hello, World!
}
4. 实践练习
练习1:统计字符类型
编写一个函数 CountChars(s string) (letters, digits, others int),统计输入字符串 s 中英文字母、数字和其他字符的数量。
- 要求:使用
for range遍历,并正确判断 Unicode 字符类型(提示:使用unicode包的IsLetter,IsDigit函数)。 - 输入:
"你好,Go 2024!" - 预期输出:
Letters: 2, Digits: 4, Others: 4(“你好”是2个字母, “Go”是2个字母,但统计单位是rune,所以是2个中文+2个英文字母=4?这里需要明确:unicode.IsLetter对中文字符返回 true)。
练习2:提取域名
给定一个邮箱地址字符串 email := "[email protected]",编写代码提取出域名部分(example.com)。
- 要求:使用
strings.Index找到@的位置,然后截取后面的子串。 - 输入:
"[email protected]" - 预期输出:
my-domain.net
练习3:验证回文
编写一个函数 IsPalindrome(s string) bool,判断一个字符串是否为回文(正读反读都一样)。要求忽略所有非字母数字字符,并且不区分大小写。
- 输入示例:
"A man, a plan, a canal: Panama" - 预期输出:
true - 提示:先清理字符串,只保留字母数字并转为小写,然后使用
rune切片进行反转比较。
5. 常见错误
-
误用
len获取字符数:- 错误:
fmt.Println("长度:", len("世界"))// 输出6,而不是2。 - 正确:
fmt.Println("长度:", len([]rune("世界")))// 输出2。
- 错误:
-
用索引遍历中文字符串:
- 错误:
for i:=0; i<len(str); i++ { fmt.Printf("%c", str[i]) }// 中文变成乱码。 - 正确:
for _, r := range str { fmt.Printf("%c", r) }。
- 错误:
-
认为
string是字符数组:- 错误:
str[0] = 'H'// 编译错误,字符串不可变。 - 正确:创建新的字符串,或转换为
[]byte或[]rune操作后再转回string。
- 错误:
-
混淆
byte和rune的使用场景:- 处理文本内容(显示、比较、查找字符)时,使用
rune。 - 处理二进制协议、文件I/O等底层数据流时,使用
byte。
- 处理文本内容(显示、比较、查找字符)时,使用
6. 小结
本课我们学习了Go语言处理文本的核心概念:
- 字符串 (
string) 是不可变的UTF-8编码字节序列。len()返回字节数。 byte(uint8) 代表一个字节,用于底层数据操作。rune(int32) 代表一个Unicode码点,用于文本处理。- 使用
for range循环可以正确、安全地遍历字符串中的每个字符(rune)。 - 将字符串转换为
[]rune切片是进行复杂文本操作(如反转、按字符索引)的基础。 - 使用
strings标准库可以高效完成常见的字符串查找、分割、替换等操作。 - 始终牢记:处理的是字节 (
byte) 还是字符 (rune),决定了你的操作是否正确。
练习编辑器
go
Loading...