最近在编程一个问题的时候突然生出了对这个问题的思考。
在 Golang 中,String 的本质是什么呢?
官方十几年前有一篇文章写的很不错《Strings, bytes, runes and characters in Go》。
读完这篇文章,给出一些总结。
字符串的本质
在 golang 中,字符串本质上是一个只读的字节切片/不可修改的 byte 数组(read-only slice of bytes):
- 字符串可以包含任意字节,不一定是 UTF-8 编码或任何特定格式;
- 从内容角度来看,字符串等价于一个字符切片;
- 字符串是不可变的(immutable)
下面是一段代码,用于展示 golang 中的字符串的作用:
package main
import "fmt"
func main() {
// 创建包含特殊字节的字符串
// 这里使用十六进制表示法定义了一个字节序列
// \xbd\xb2\x3d\xbc\x20\xe2\x8c\x98 分别代表不同的十六进制字节值
// 注意:这个字符串可能包含非有效UTF-8序列
s := "\xbd\xb2\x3d\xbc\x20\xe2\x8c\x98"
// 按字节访问字符串
// 在Go中,字符串实际上是只读的字节序列,可以通过索引访问单个字节
// 输出的是该字节的十进制值189 (对应十六进制0xbd)
fmt.Println(s[0]) // 输出 189(十进制),即 0xbd
// 尝试打印完整字符串的字节序列
fmt.Print("原始字符串的字节: ")
for i := 0; i < len(s); i++ {
fmt.Printf("%x ", s[i])
}
fmt.Println()
// 无法修改字符串中的字节
// s[0] = 'x' // 编译错误:字符串不可变
// Go中的字符串是不可变的,尝试直接修改会导致编译错误
// 可以转换为字节切片进行修改
// []byte(s) 会创建字符串内容的一个副本作为字节切片
b := []byte(s)
// 修改字节切片的第一个元素为'x'的ASCII码(120)
b[0] = 'x'
// 将修改后的字节切片转换回字符串
// string(b)会创建新的字符串
s2 := string(b)
fmt.Println("修改后的字符串:", s2)
// 打印修改后字符串的字节序列
fmt.Print("修改后字符串的字节: ")
for i := 0; i < len(s2); i++ {
fmt.Printf("%x ", s2[i])
}
fmt.Println()
}
代码主要是为了体现字符串的一些特性:
- 字符串的内部表示: Go中的字符串实际上是一个只读的字节序列,它们以UTF-8编码存储 5。可以通过索引直接访问字节,但不能修改 1。
- 字节访问: 当使用
s[0]
这样的索引访问时,你获取的是该位置的原始字节值,而不是字符 1。 - 字符串不可变性: 字符串在Go中是不可变的,代码中注释掉的
s[0] = 'x'
会导致编译错误 1。 - 字符串和字节切片的转换:
- 字节切片的修改: 由于字节切片是可变的,你可以修改切片中的任何元素,例如代码中的
b[0] = 'x'
。 - 从字节切片转回字符串:
string(b)
会将修改后的字节切片转换回一个新的字符串 4。
字符串与 UTF-8
Go语言中的字符串通常采用UTF-8编码:
- UTF-8是一种变长编码,一个Unicode字符可能由1-4个字节表示
- 字符串长度(len函数)返回的是字节数,不是字符数
- 在UTF-8中,ASCII字符占用1个字节,其他字符可能占用多个字节
package main
import "fmt"
func main() {
s1 := "hello"
s2 := "你好"
fmt.Printf("s1: %s, 字节长度: %d\n", s1, len(s1)) // 输出 s1: hello, 字节长度: 5
fmt.Printf("s2: %s, 字节长度: %d\n", s2, len(s2)) // 输出 s2: 你好, 字节长度: 6
// 遍历字节
for i := 0; i < len(s2); i++ {
fmt.Printf("%x ", s2[i]) // 输出 e4 bd a0 e5 a5 bd
}
fmt.Println()
}
输出内容如下:
s1: hello, 字节长度: 5
s2: 你好, 字节长度: 6
e4 bd a0 e5 a5 bd
Process finished with the exit code 0
字节、符文与字符的区别
理解这三个概念对于处理Go字符串至关重要:
- 字节(byte):8位无符号整数,
uint8
的别名,表示数据的最小单位 - 符文(rune):表示一个Unicode码点,
int32
的别名,表示一个Unicode字符 - 字符(character):人类语言的基本单位,在Unicode中通常与一个码点对应
package main
import "fmt"
func main() {
s := "Hello, 世界"
// 按字节遍历
fmt.Println("按字节遍历:")
for i := 0; i < len(s); i++ {
fmt.Printf("%d: %x\n", i, s[i])
}
// 按rune遍历
fmt.Println("\n按rune遍历:")
for i, r := range s {
fmt.Printf("字节位置:%d, Unicode码点:%U, 字符:%c\n", i, r, r)
}
}
运行结果如下:
按字节遍历:
0: 48
1: 65
2: 6c
3: 6c
4: 6f
5: 2c
6: 20
7: e4
8: b8
9: 96
10: e7
11: 95
12: 8c
按rune遍历:
字节位置:0, Unicode码点:U+0048, 字符:H
字节位置:1, Unicode码点:U+0065, 字符:e
字节位置:2, Unicode码点:U+006C, 字符:l
字节位置:3, Unicode码点:U+006C, 字符:l
字节位置:4, Unicode码点:U+006F, 字符:o
字节位置:5, Unicode码点:U+002C, 字符:,
字节位置:6, Unicode码点:U+0020, 字符:
字节位置:7, Unicode码点:U+4E16, 字符:世
字节位置:10, Unicode码点:U+754C, 字符:界
Process finished with the exit code 0
字符串操作和展示
Go提供了多种方式查看字符串的内容:
字符串直接打印
s := "\xbd\xb2\x3d\xbc\x20\xe2\x8c\x98"
fmt.Println(s) // 输出可能包含乱码字符:��=� ⌘
以十六进制查看字节
fmt.Printf("%x\n", s) // 无空格:bdb23dbc20e28c98
fmt.Printf("% x\n", s) // 带空格:bd b2 3d bc 20 e2 8c 98
转义显示
fmt.Printf("%q\n", s) // 转义不可打印字符:"\xbd\xb2=\xbc ⌘"
fmt.Printf("%+q\n", s) // 额外转义非ASCII字符:"\xbd\xb2=\xbc \u2318"
字符串遍历与处理
在处理UTF-8字符串时,有两种常见的遍历方式:
按字节遍历(获取原始字节)
s := "Hello, 世界"
for i := 0; i < len(s); i++ {
fmt.Printf("%d:%x ", i, s[i])
}
// 输出: 0:48 1:65 2:6c 3:6c 4:6f 5:2c 6:20 7:e4 8:b8 9:96 10:e7 11:95 12:8c
按 rune 遍历(获取 Unicode 字符)
s := "Hello, 世界"
for i, r := range s {
fmt.Printf("%d:%c ", i, r)
}
// 输出: 0:H 1:e 2:l 3:l 4:o 5:, 6: 7:世 10:界
注意上面的索引是不连续的,因为range
给出的是字符(rune)在字节序列中的起始位置。
字符串转换
Go提供了字符串与其他类型的转换:
package main
import "fmt"
func main() {
// 字符串与字节切片的转换
s := "Hello, 世界"
b := []byte(s)
s2 := string(b)
fmt.Println(s2)
// 字符串与rune切片的转换
r := []rune(s)
fmt.Printf("字符串长度: %d, rune切片长度: %d\n", len(s), len(r))
// 输出: 字符串长度: 13, rune切片长度: 9
s3 := string(r)
fmt.Println(s3) // Hello, 世界
}
展示结果如下:
Hello, 世界
字符串长度: 13, rune切片长度: 9
Hello, 世界
Process finished with the exit code 0
字符串操作常用技巧
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
s := "Hello, 世界"
// 1. 获取字符串字节长度
byteLen := len(s)
fmt.Printf("字节长度: %d\n", byteLen) // 13
// 2. 获取字符串Unicode字符数量
runeCount := utf8.RuneCountInString(s)
fmt.Printf("字符数: %d\n", runeCount) // 9
// 3. 截取子串(注意:按字节截取,可能截断Unicode字符)
sub1 := s[:5]
fmt.Println(sub1) // Hello
// 4. 安全地截取Unicode字符
// 先转换为rune切片,再截取,再转回字符串
runes := []rune(s)
sub2 := string(runes[:6]) // 前6个Unicode字符
fmt.Println(sub2) // Hello,
}
运行结果如下:
字节长度: 13
字符数: 9
Hello
Hello,
Process finished with the exit code 0
总结
目前凭借我的经验,实在是没有太多好说的,这个玩意,还是属于简短的没啥好说的~~~
上面可以当做我的翻译文章。
下面的代码可以作为一点思考的代码体现:
package main
import "fmt"
func main() {
s := "golang"
fmt.Println(s)
// 字符串本质,不可修改的 byte 数组
fmt.Println(len(s)) // 6
a := "golang你好"
fmt.Println(len(a)) // 12
arr := []rune(a)
fmt.Println(len(arr))
fmt.Println(string(arr))
for _, ele := range arr {
fmt.Printf("%c\n", ele)
}
b := `golang
hello
你好`
fmt.Println(b)
}
输出结果如下:
golang
6
12
8
golang你好
g
o
l
a
n
g
你
好
golang
hello
你好
Process finished with the exit code 0