Go 字符串
十二月其一,学习 Go 的字符串相关知识。
本文主要总结 Go 字符串的设计、相关 API 用法,此外还会涉及到一些字符编码知识。
有关 Go 字符串的知识有点散乱,大致可分为下面几点:
字符串是基本数据类型
在 Go 语言中,字符串(string)是基本数据类型,零值是空字符串 ""
(而不是 nil)。
字符串可以用 “” 和 `` 两种形式表示
有两种表示字面值的形式:”xxx”(解释字符串)、`xxx`(非解释字符串)。
1 | println("hello, \t pz") // 打印:hello, pz |
解释字符串会替换转义字符,包含下面几种:
转义字符 | 转义成 |
---|---|
\t | 制表符 |
\r | 回车符 |
\n | 换行符 |
\\ | 字符 \ |
\“ | 字符 " |
\u 或 \U | Unicode 字符,例如 “\u8A79” 转义成字符 ‘詹’ |
字符串可以看做一个只读数组
Go 的字符串在底层是一片连续的内存空间,可以理解为一个由字符组成的数组。
字符串在运行时使用 reflect.StringHeader
表示,长得很像 Slice 在运行时的结构:
1 | type StringHeader struct { |
跟其他语言类似,Go 的字符串不能修改,我们可以通过 string 与 []byte 的来回转换,达到修改字符串的目的,但其实得到的是一个新字符串。
有关 Go 字符串数据结构相关的知识,可以移步《Go 语言设计与实现 - 字符串》。
字符串使用 len() 获取长度
通过 len() 函数获取字符串所占字节的长度(the number of bytes),例如 len(str)
。
字符串可以使用 + 拼接
两个字符串 s1
和 s2
可以通过 s := s1 + s2
拼接在一起。
字符串还可以使用 +=
尾接其他的字符串。
接下来总结 Go 字符串编码相关的知识。
前置知识:UniCode & UTF-8
不可避免地,要先讲一点编码前置知识,主要是搞懂两个名词:Unicode、UTF-8。
Unicode 是字符集,它为世界上的每一个字符赋予了一个编码(当然没那么全,现在收录了 14w+ 个字符),是计算机领域的业界标准。比如在 Unicode 中,编码 U+0061
表示字符 a
,编码 U+5F20
表示字符 张
,编码 U+1F600
表示字符 😀
(emoji 也是字符)。
UTF-8 是 Unicode 的编码方式(8-bit Unicode Transformation Format)。
Unicode 是一个大字符集,但是没有规定里面的每个字符编码,应该表示成什么形式。比如字符 a
的 Unicode 码是 U+0061
,用二进制表示就是 01100001
,但是当实际编码时,是编码成 01100001
,0000000001100001
,还是 000000000000000001100001
,这是不确定的。Unicode 有 14w+ 个字符,像字符 a
这样编码值小的,用一个字节就可以表示,但是字符 😀
却至少需要三个字节才能表示完。
UTF-8 是 Unicode 众多编码方式的其中一种,也是使用率最高的一种,它使用 1~4 个字节表示一个符号(变长编码)。
有关 Unicode 和 UTF-8 更多的内容,比如 UTF-8 是怎么编码的,移步《字符编码笔记:ASCII,Unicode 和 UTF-8》。
(字符和字节的区别不说了,不会还有人不知道吧,不会吧不会吧 :D)
字符串以 UTF-8 存储,下标是字节
Go 的字符串以 UTF-8 编码方式存储,也就是说,字符串中的一个字符占用的长度是不一定的,可能是 1~4 个字节。
字符串通过下标获取值(str[i]
),得到的不是字符,而是字节。
对于像 ASCII 码这样一个字节长度的字符,通过下标获取值并没有关系,但是像汉字这种 UTF-8 需要三个字节编码的字符,使用下标获取就会出现乱码。以 str := "test测试😀"
这个字符串为例拆解:
字符 | t | e | s | t | 测 | 试 | 😀 | |||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Unicode | U+0074 | U+0065 | U+0073 | U+0074 | U+6D4B | U+8BD5 | U+1F600 | |||||||
UTF-8 | 01110100 | 01100101 | 01110011 | 01110100 | 11100110 | 10110101 | 10001011 | 11101000 | 10101111 | 10010101 | 11110000 | 10011111 | 10011000 | 10000000 |
下标 | str[0] | str[1] | str[2] | str[3] | str[4] | str[5] | str[6] | str[7] | str[8] | str[9] | str[10] | str[11] | str[12] | str[13] |
顺带一提,str
的长度(len(str)
)是 14,它指的也是字节数,而不是字符数。
byte & rune
Go 有两个跟 string 相关的数据类型:byte(字节)和 rune(字符),这两种数据类型的定义如下:
1 | // byte is an alias for uint8 and is equivalent to uint8 in all ways. It is |
可以看出,byte 是 uint8 的类型别名,rune 是 int32 的类型别名(UTF-8 最多 4 个字节,因此字符用 int32 表示即可)。
下面总结了一下两种类型的使用场景:
byte
字符串下标
1
str[0]
ASCII 字符
1
var c byte = 'a'
字符串转换成 byte[]
1
var bytes []byte = []byte(str)
rune
声明一个字符(ASCII 字符变量在没有指明类型时,默认也是 rune 类型)
1
2var c1 rune = '詹'
var c2 = 'a' // 注意:此时c2也是rune类型的字符串转换成字符数组
1
var runes []rune = []rune(str)
字符串 For-range
1
2
3
4
5
6str := "test测试"
// 第一个变量表示下标,第二个变量表示字符
for i, c := range str {
fmt.Printf("%d:%c ", i, c)
}
// 打印结果:0:t 1:e 2:s 3:t 4:测 7:试(但是为什么要这么设计?我没查到说法)
博文《Go 字符串拼接的 7 种姿势》总结了字符串的拼接方法,并测试了性能。
1 | str := "foo" |
下面整理了一下 strings
包的 API(截止到版本 1.17.4)。
1 | // 统计字符串出现的次数 |
下面整理了一下 strconv
包的 API(截止到版本 1.17.4)。
另外这篇文章也整理得挺好的:《Golang学习 - strconv 包》
1 | // 格式化 int,给定进制(进制 ∈ [2, 36]) |