go-string源码阅读
GO string源码阅读及详解
在刷leetcode过程中碰到了使用string的题,发现自己还没专门看够Go中的string使用及实现等,故专门参考网络及MarsCode等产出了本篇文章
Q:如何在GO中定义string?
A_1:
采用双引号赋值字符串,其中转义字符会进行转义(\t \n等
1 | str := "hello\tstring\n" |
A_2:
采用反引号定义字符串,转义字符不会进行转义
1 | str := `hello\tstring\n` |
tips:使用反引号可创建多行字符串
1 | str := `hello |
Q:string底层?
基于Go SDK 1.23.0
在源码包中src/runtime/string.go
stringStruct定义了string的数据结构
1 | type stringStruct struct { |
在src/builtin/builtin.go
中有对string的注释
1 | // string is the set of all strings of 8-bit bytes, conventionally but not |
即:
- 从底层来讲,Go 语言里的 string 类型是由 8 位字节组成的序列。这里的 8 位字节意味着它可以存储任意的字节数据,不一定非得是常规的文本字符所对应的字节。
- 注释里提到“conventionally but not necessarily representing UTF - 8 - encoded text”,意思是在实际应用中,大多数时候 string 用来存储 UTF - 8 编码的文本,这是一种常见的做法。不过,string 也可以存储其他编码格式的文本,甚至是二进制数据,例如图片、音频等数据的字节序列。
- 注释指出“ A string may be empty, but not nil”,这表明 string 类型可以有一个空字符串值,也就是 “”,但它不能是 nil。在 Go 里,nil 一般用于表示指针、切片、映射、通道等类型的零值,而 string 不是这些类型,所以它不能被赋值为 nil。
- “Values of string type are immutable”表明 string 类型的值是不可变的(Java中也是类似的处理,String创建出来后就是作为一个常量存在常量池中,不可直接修改,在Java中只能另用stringbuilder/stringbuffer等修改)。一旦创建了一个 string,就不能修改它的内容。如果需要对字符串进行修改,实际上是创建了一个新的字符串。
Q:使用string注意事项?
- 通过string下标访问中文时,需转为rune序列,才能正确以UTF-8读取
- 使用
range
遍历string时会将其拆成一个字节序列,再遍历其包含的每个UTF-8编码值,即每个Unicode字符
Q:string创建过程?
1 | str := "hello string" |
会调用src/runtime/string.go
中的gostringnocopy
方法
1 | //go:nosplit |
接收一个byte指针作为参数,返回一个string类型值
unsafe.Pointer
用于将*byte
转换成unsafe.Pointer
,从而绕过Go的安全检查机制,直接访问内存中的数据
findnull()
则可以理解为一个保护措施,在 gostringnocopy
函数里,findnull
函数被用来确定传入的 *byte
指针所指向的 C 风格字符串的长度,然后用这个长度来创建一个 Go 字符串。
在 Go 语言里,字符串一般是用 UTF-8 编码的,并且有明确的长度信息。不过在和 C 语言交互或者处理一些以空字符结尾的字符串数据时,就需要找出空字符的位置来确定字符串的长度。findnull
函数就是为了实现这个目的而存在的。
1 | func findnull(s *byte) int { |
最后的s := *(*string)(unsafe.Pointer(&ss))
可理解为在告诉编译器如何理解ss
指向的内存布局,如何将其视为一个合法的string
对象
ss取地址 -> 视为unsafe.Pointer类型指针 -> 强转为*string指针 -> *操作取指针指向的对象的值(将其视为*string后
Q:为什么要使用stringStruct?
1 | type stringStruct struct { |
stringStruct作为byte序列和string的中间层,其包含两个信息
- 指向数据地址的指针
- 字符串长度
有这个中间层存在,在处理大字符串,如对其切片等操作时,不会再次复制整个字符串的数据,而是操作stringStruct
只调整指针指向地址以及字符串长度
当然,以上都为Go runtime内部实现,我们非必须了解其背后细节
Q:GO是否存在string常量池?
直接上代码跑跑就知道了
1 | str1 := "abc" |
即,Go虽然没有明确的常量池机制,但是Go编译器会对字符常量进行优化
Q:如何修改string?
string
类型初始化后,直接对其修改会报错
1 | str := "hello go" |
string与[]byte相互转换
可将string转为[]byte修改后,再转回string
1 | str := "hello go" |
转换过程中存在内存拷贝
Q:为什么不允许修改string?
Java同理
修改字符串太麻烦,不如直接重新创建
当字符串内容固定不变时,Go可以安全地共享相同的字符串实例,减少不必要的内存复制。例如子串提取和拼接可以通过简单的指针调整实现,而无需复制整个数据,显著提升了处理大字符串或频繁操作时的性能。
Q:字符串拼接常用方法
1.加号拼接
最理所当然想到的方式
1 | str1 := "hello" |
使用+
拼接时,底层会开辟一块新空间,将两个str内容进行复制,最终将复制的str合并在新空间中。
对于频繁的字符串拼接来说效率不高。
2.fmt.Sprintf
fmt.Sprintf
为格式化输出函数之一,接收一个格式化字符串和一系列参数,返回一个格式化后的字符串。
适合需要插入变量或控制输出格式的场合(典例手搓toString()
方法
1 | str1:="hello" |
由于它涉及到解析格式化字符串,性能上会有一定影响。特别是当使用%v
或%+v
等通用格式化动词时,它需要了解传递给它的值的实际类型以便正确地进行格式化。为了做到这一点,fmt包会使用到反射,对大量数据进行操作时,会对性能会有较大的影响。
3.strings.Builder
strings.Builder
是Go1.10
引入的一个高效可变字符串缓冲区,特别适合于需要进行多次追加操作的场景。
1 | str1:="hello" |
源码在src/strings/builder.go
1 | // A Builder is used to efficiently build a string using [Builder.Write] methods. |
addr
为一个指向Builder
结构体的指针,它的作用是检测Builde
是否被按值复制。当调用Builder
的方法时,会检查addr
是否等于当前实例的指针,如果不相等,就会触发panic,提示非法使用按值复制的Builder
,即同时存在两个Builder
向同一个buf
写值
buf
为一个字节切片,用于存储构建过程中的字符串数据
注释额外提醒不要直接访问此缓冲区:
- 这个切片在某个时刻会使用
unsafe
包的方法转换为字符串,直接访问可能会破坏内部实现。 buf
的长度len(buf)
和容量cap(buf)
的数据可能是未初始化的,直接访问可能会导致未定义行为。
与+和fmt.Sprintf相比,strings.Builder在处理大量字符串拼接时性能更好,因为它提取分配了buf内存,减少了内存分配次数。
4.strings.join
strings.Join
接受一个字符串切片和一个分隔符作为参数,然后将切片中的所有元素用分隔符连接成一个单一的字符串。
当我们有一个已经分割好的字符串列表并且想要用特定字符连接它们时,该方式是最理想选择。
1 | str1:="hello" |
源码在/src/strings/strings.go
中
1 | // Join concatenates the elements of its first argument to create a single string. The separator |
strings.Join
基于strings.builder
来实现的,并且可以自定义分隔符,能提前预分配buf的空间,减少了内存分配消耗的性能
5.bytes.Buffer
bytes.Buffer
是一个实现了io.Writer
接口的类型,它可以像strings.Builder
一样被用来构建字符串,但它实际上是为处理字节流设计的。
源码在src/bytes/buffer.go
中
1 | // A Buffer is a variable-sized buffer of bytes with [Buffer.Read] and [Buffer.Write] methods. |
在bytes.Buffer
也存在字节切片buf,通过WriteString
方法在底层的[]byte切片
中进行拼接。
1 | str1:="hello" |
但bytes.Buffer
的性能略逊于strings.Builder
,因为strings.Buider
对字符串拼接有专门优化。