Go 语言的 string 类型
string的基本概念
Go 语言中的字符串类型 string
是一个非常常用的数据类型,它用于表示文本信息。以下是关于 Go 语言中字符串的详细介绍:
-
基本类型:字符串是 Go 语言中的基本类型之一,属于预定义类型,无需额外导入包即可使用。
-
不可变性:字符串是不可变的,即一旦创建,其内容不可更改。要修改字符串,需要将其转换为可变类型(如
[]byte
)进行操作,然后再转换回字符串。 -
字面值:字符串可以使用双引号
"
或反引号 `来表示。双引号字符串中可以包含转义字符(例如\n
表示换行),而反引号字符串则原样输出。1 2
str1 := "Hello, World!" str2 := `This is a raw string literal\n`
-
UTF-8 编码:Go 语言中的字符串是以 UTF-8 编码的,因此支持多种字符集和语言。
-
字符串操作:Go 提供了一系列内置函数和方法来操作字符串,如
len()
获取字符串长度、+
连接字符串、strings
包中的函数用于搜索、替换、分割等操作等。len() 返回的字节长度,而不是字符长度。
-
索引访问:可以通过索引访问字符串中的单个字符,索引从 0 开始,可以使用类似数组的访问方式。
-
字符串比较:可以使用
==
和!=
运算符来比较字符串是否相等。 -
字符串的内存分配:字符串在 Go 语言中是一个不可变的字节切片,因此创建字符串时会分配内存,但字符串的内容不可修改。
-
字符串格式化:可以使用
fmt.Sprintf()
或strconv
包来进行字符串格式化。
len()
|
|
len 内建函数返回 v 的长度,这取决于具体类型:
|
|
特别注意:对字符串使用len()
函数,得到的是v的字节数
而不是字符串长度。如:
|
|
如果想获取字符串中Unicode字符的数量(字符数),而不是字节数,可以使用utf8.RuneCountInString
函数。这将返回字符串中的Unicode字符数。
|
|
字符集
字符编码是一种将字符映射到数字代码的方式,使得计算机能够理解和处理文本数据。在计算机内部,所有的文本字符最终都会以数字形式表示,这些数字称为字符编码。字符编码可以将字符映射到唯一的数字标识,从而在计算机中进行存储和处理。
Unicode(统一码、万国码、单一码)是一个标准,它为世界上几乎所有的文字字符分配了唯一的数字代码,以便能够在不同的计算机系统和软件中互操作。Unicode 定义了一个包含几乎所有书面语言的字符集,每个字符都被分配了一个唯一的代码点(code point)。Unicode 的设计目标之一是为了解决不同字符集之间的兼容性问题,使得文本数据在全球范围内能够被正确地表示和处理。
Unicode 字符集是按照标准进行组织和编排的,其中包括了各种语言的字母、数字、标点符号、符号、控制字符等。Unicode 中的每个字符都有一个唯一的代码点,用来表示该字符,通常以 U+ 开头,后跟一个或多个十六进制数字。例如,拉丁字母 A 的 Unicode 代码点是 U+0041。
Unicode 字符集的实现有多种编码方式,最常见的是 UTF-8、UTF-16 和 UTF-32。这些编码方式将 Unicode 中的代码点编码成字节序列,以便在计算机中存储和传输。其中,UTF-8 是最常用的 Unicode 编码方式,它使用可变长度的编码,能够节省存储空间,并且兼容 ASCII 字符集。UTF-16 使用 16 位编码表示 Unicode 字符,而 UTF-32 则使用固定长度的 32 位编码。
UTF-8、UTF-16 和 UTF-32 是三种不同的 Unicode 编码方式,它们之间的主要区别在于编码的单位和字节顺序。
-
UTF-8:
- 编码单位:UTF-8 使用 8 位(1 字节)为一个编码单位。
- 变长编码:UTF-8 是一种变长编码方式,不同的字符使用不同长度的字节序列来表示,字符的编码长度为 1 到 4 个字节不等。
- 兼容性:UTF-8 是一种兼容 ASCII 字符集的编码方式,ASCII 字符集中的字符使用单个字节编码,因此 ASCII 文本在 UTF-8 中保持不变。
- 节省空间:对于大多数文本数据,UTF-8 通常比 UTF-16 和 UTF-32 更节省空间,因为它可以使用较少的字节数表示相同的字符。
-
UTF-16:
- 编码单位:UTF-16 使用 16 位(2 字节)为一个编码单位。
- 定长编码:UTF-16 是一种定长编码方式,大多数字符使用 2 个字节编码,但某些字符需要使用额外的 2 个字节来表示(称为代理对)。
- 字节顺序:UTF-16 编码方式需要考虑字节顺序(大端序或小端序)。
- 适用范围:UTF-16 可以很好地表示大多数常用字符,但对于罕见的字符或辅助平面字符(如 emoji),可能需要使用代理对表示,因此在某些情况下可能会占用更多的空间。
-
UTF-32:
- 编码单位:UTF-32 使用 32 位(4 字节)为一个编码单位。
- 定长编码:UTF-32 是一种定长编码方式,每个字符都使用固定长度的 4 个字节来表示。
- 占用空间:UTF-32 在存储和传输文本数据时通常会占用更多的空间,因为每个字符都使用固定长度的 4 个字节。
字符编码边界
现在我们确定了字符编码,但我们如何确定一个字符的边界呢?
假设现在我们有字符串 ”Go语言“,它们的 Unicode 编码如下:
|
|
如果我们直接把他组合起来:
1000111110111110001011111011011000101000000000
这样如何能知道多少个字节应该解析成一个字符呢?显然,直接组合的方式是不可取的。
定长编码
解决方案一:每个字符采用固定长度的编码(统一按最长的来),位数不够,高位补零。
比如固定为2个字节,前面字符串的编码如下:
|
|
每次解析两个字节为一个字符,这样就解决了字符编码边界的问题。
但这样也存在浪费内存空间的问题,而且随着字符集收录符号的增加,编号跨度增大,定长编码造成的浪费就越显著。
变长编码
小编号少占字节,大编号多占字节。
在UTF-8编码中,每个Unicode字符使用1至4个字节表示,字节的排列方式遵循一定的规则,以便解码器可以正确地识别每个字符的边界。UTF-8的字符编码边界是通过字节的高位(最高有效位)的值来确定的。
UTF-8采用了一种前缀码的方式,具体规则如下:
-
对于单字节的字符,即Unicode范围为U+0000到U+007F的字符,UTF-8编码为0xxxxxxx,其中x表示字符的码位。(和 ASCII 字符集相同)
-
对于多字节的字符,UTF-8使用了多个字节来表示,其中第一个字节的高位位数表示了字符需要使用的总字节数,以及字符的高位信息。具体规则如下:
- 2字节字符:Unicode范围为U+0080到U+07FF,UTF-8编码为110xxxxx 10xxxxxx。
- 3字节字符:Unicode范围为U+0800到U+FFFF,UTF-8编码为1110xxxx 10xxxxxx 10xxxxxx。
- 4字节字符:Unicode范围为U+10000到U+10FFFF,UTF-8编码为11110xxx 10xxxxxx 10xxxxxx 10xxxxxx。
在UTF-8中,每个字节的最高有效位(MSB)用于指示字节是否为字符的一部分以及字符的长度。如果最高位为0,则表示该字节是字符的一部分;如果最高位为1,则表示该字节是多字节字符的开始字节,并且有多少个连续的1可以告诉你字符的总字节数。
这种设计使得UTF-8解码器能够准确地识别每个字符的边界,从而正确地解析UTF-8编码的文本数据。
字符串结尾标识
字符串的终止有两种方式,一种是 C 语言中的隐式申明,以字符“\0”作为终止符。
这样的缺点是在字符串中不能出现字符“\0”。
一种是 Go语言中的显式声明。Go 语言运行时字符串 string 的表示结构如下。
|
|
其中,str
字段是一个指向存储字符串数据的字节数组的指针,len
字段表示字符串的长度。
这里的 len 表示的是字节长度,而不是字符长度。
底层数据结构
在 Go 语言中,字符串类型 string
的底层数据结构是一个不可变的字节切片(byte slice)。这个字节切片中存储着字符串的 UTF-8 编码的字节序列。由于字符串是不可变的,因此一旦创建,其内容就不能被修改。
在 Go 语言的源码中,string
类型的定义如下:
|
|
其中,str
字段是一个指向存储字符串数据的字节数组的指针,len
字段表示字符串的长度。由于字符串的不可变性,因此使用指针的方式可以确保字符串的内容在创建后不会被修改。
由于字符串是不可变的,因此可以在内存中共享相同的底层数据。这意味着,如果两个字符串具有相同的内容,那么它们可能会共享相同的底层数据,从而节省内存。
在 Go 语言中使用符文(rune)类型来表示和区分字符串中的“字符”,rune 其实是 int32 的别称。 当用 range 轮询字符串时,轮询的不再是单字节,而是具体的 rune。如下所示,对字符串 b 进行轮询,其第一个参数 index 代表每个 rune 的字节偏移量,而runeValue 为 int32,代表符文数。
|
|
fmt.Printf
有一个特殊的格式化符#U
可用于打印符文数十六进制的Unicode编码方式及字符形状。如上例打印出:
|
|
修改字符串中的内容
在 Go 语言中,底层的字节切片是不可变的,这意味着你不能直接修改它。如果你尝试修改底层的字节切片,Go 语言的运行时系统会导致程序崩溃或者出现未定义的行为。这是因为底层的字节切片可能被多个字符串共享,修改其中一个字符串的底层字节切片可能会影响到其他字符串,破坏了字符串的不可变性和共享性。
通常情况下,如果你需要修改字符串中的内容,你应该创建一个新的字符串来代替原来的字符串。你可以使用 []byte
类型来修改字符串的内容,因为 []byte
是可变的字节切片,但是需要注意的是,这样做会创建一个新的字节数组,而不是修改原始字符串的底层数据。
以下是一个示例说明:
|
|
在这个示例中,我们通过将字符串转换为 []byte
类型来修改字符串的内容,然后再将修改后的字节切片转换回字符串。这样做是安全的,因为我们创建了一个新的字节数组来存储修改后的字符串内容,而不是直接修改原始字符串的底层数据。
【注意】正因为 string 和 []byte 间的转换非常方便,在某些高频场景中往往会成为性能的瓶颈,比如数据库访问、HTTP请求处理等。
byte 切片转换为 string 的场景有很多,出于性能的考虑,有时候只是应用在临时需要字符串的场景下。byte 切片转换成 string 时并不会拷贝内存,而是直接返回一个 string,这个 string 的指针(string.str)指向切片的内存。
比如,编译器会识别如下临时场景:
- 使用m[string(b)]来查找map(map 是 string 为 key,临时把切片 b 转成 string );
- 字符串拼接,如"<" + “string(b)” + “>";
- 字符串比较:string(b) == “foo”
由于只是临时把 byte 切片转换成 string,也就避免了因 byte 切片内容修改而导致 string 数据变化的问题,所以此时可以不必拷贝内存。
为什么不允许修改
像 C++语言中的 string,其本身拥有内存空间,修改 string 是支持的。但在 Go 的实现中 string 不包含内存空间,只有一个内存的指针,这样做的好处是 string 变得非常轻量,可以很方便地进行传递而不用担心内存拷贝。
因为 string 通常指向字符串字面量,而字符串字面量存储的位置是只读段,而不是堆或栈上,所以才有了 string 不可修改的约定。
string类型的空间占用
当查看string类型的变量所占的空间大小时,会发现是16字节(64位机器)。
|
|
也许你会好奇,为什么是16字节,它的底层存储模型是什么样子的。
在src/runtime/string.go
中,定义了string
的结构:
|
|
string
底层结构是一个结构体,它有两个字段:
str unsafe.Pointer
:该字段指向了 string 底层的 byte 数组(切片)。
len int
:该字段确定了 string 字符串的长度。
unsafe.Pointer
和int
类型的字段分别占用 8 个字节空间,所以string
类型的变量占用 16 字节(内存对齐后)空间。