《Go语言精进之路》读书笔记 | 了解string实现原理并高效使用
Posted COCOgsta
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《Go语言精进之路》读书笔记 | 了解string实现原理并高效使用相关的知识,希望对你有一定的参考价值。
书籍来源:《Go语言精进之路:从新手到高手的编程思想、方法和技巧》
一边学习一边整理读书笔记,并与大家分享,侵权即删,谢谢支持!
附上汇总贴:《Go语言精进之路》读书笔记 | 汇总_COCOgsta的博客-CSDN博客
15.1 Go语言的字符串类型
在Go语言中,无论是字符串常量、字符串变量还是代码中出现的字符串字面量,它们的类型都被统一设置为string:
// chapter3/sources/string_type.go
const (
s = "string constant"
)
func main()
var s1 string = "string variable"
fmt.Printf("%T\\n", s) // string
fmt.Printf("%T\\n", s1) // string
fmt.Printf("%T\\n", "temporary string literal") // string
复制代码
Go的string类型具有如下功能特点。
(1)string类型的数据是不可变的
一旦声明了一个string类型的标识符,该标识符所指代的数据在整个程序的生命周期内便无法更改。
(2)零值可用
Go string类型支持“零值可用”的理念。其零值为"",长度为0。
var s string
fmt.Println(s) // s = ""
fmt.Println(len(s)) // 0
复制代码
(3)获取长度的时间复杂度是O(1)级别
Go将string的长度作为一个字段存储在运行时的string类型的内部表示结构中。这样获取string长度的操作,即len(s)是一个代价极低的O(1)操作。
(4)支持通过+/+=操作符进行字符串连接
通过+/+=操作符进行的字符串连接是体验最好的字符串连接操作,Go语言支持这种操作:
s := "Rob Pike, "
s = s + "Robert Griesemer, "
s += " Ken Thompson"
fmt.Println(s) // Rob Pike, Robert Griesemer, Ken Thompson
复制代码
(5)支持各种比较关系操作符:==、!= 、>=、<=、>和<
// chapter3/sources/string_compare.go
func main()
// ==
s1 := "世界和平"
s2 := "世界" + "和平"
fmt.Println(s1 == s2) // true
// !=
s1 = "Go"
s2 = "C"
fmt.Println(s1 != s2) // true
// < 和 <=
s1 = "12345"
s2 = "23456"
fmt.Println(s1 < s2) // true
fmt.Println(s1 <= s2) // true
// > 和 >=
s1 = "12345"
s2 = "123"
fmt.Println(s1 > s2) // true
fmt.Println(s1 >= s2) // true
复制代码
如果两个字符串的长度不相同,无须比较即可断定。如果长度相同,则要判断指针是否指向同一块底层存储数据。如果相同,则两个字符串是等价的,否则还需比对实际的数据内容。
(6)对非ASCII字符提供原生支持
Go字符串的每个字符都是一个Unicode字符,并且这些Unicode字符是以UTF-8编码格式存储在内存当中的。
(7)原生支持多行字符串
Go语言提供了通过反引号构造“所见即所得”的多行字符串的方法:
// chapter3/sources/string_multilines.go
const s = `好雨知时节,当春乃发生。
随风潜入夜,润物细无声。
野径云俱黑,江船火独明。
晓看红湿处,花重锦官城。`
func main()
fmt.Println(s)
$go run string_multilines.go
好雨知时节,当春乃发生。
随风潜入夜,润物细无声。
野径云俱黑,江船火独明。
晓看红湿处,花重锦官城。
复制代码
15.2 字符串的内部表示
Go string在运行时表示为下面的结构:
// $GOROOT/src/runtime/string.go
type stringStruct struct
str unsafe.Pointer
len int
复制代码
string类型是一个描述符,它本身并不真正存储数据,而仅是由一个指向底层存储的指针和字符串的长度字段组成。下面是runtime包中实例化一个字符串对应的函数:
// $GOROOT/src/runtime/string.go
func rawstring(size int) (s string, b []byte)
p := mallocgc(uintptr(size), nil, false)
stringStructOf(&s).str = p
stringStructOf(&s).len = size
*(*slice)(unsafe.Pointer(&b)) = slicep, size, size
return
复制代码
用图15-1来表示函数rawstring调用后的一个string实例的状态。
图15-1 string类型在运行时的表示
经过rawstring实例化后,stringStruct中的str指针指向真正存储字符串数据的底层内存区域,len字段存储的是字符串的长度(这里是5);rawstring同时还创建了一个临时slice,该slice的array指针也指向存储字符串数据的底层内存区域。
直接将string类型通过函数/方法参数传入也不会有太多的损耗,因为传入的仅仅是一个“描述符”,而不是真正的字符串数据。
15.3 字符串的高效构造
Go还提供了其他一些构造字符串的方法,比如:
使用fmt.Sprintf;使用strings.Join;使用strings.Builder;使用bytes.Buffer。
在这些方法中做了预初始化的strings.Builder连接构建字符串效率最高,带有预初始化的bytes.Buffer和strings.Join这两种方法效率其次,未做预初始化的strings.Builder、bytes.Buffer和操作符连接在第三档次; fmt.Sprintf性能最差,排在末尾。
strings.Join连接构建字符串的平均性能最稳定,使用操作符连接的方式最直观、最自然,如果是由多种不同类型变量来构建特定格式的字符串,那么fmt.Sprintf是最适合的。
15.4 字符串相关的高效转换
string到[]rune以及string到[]byte的转换,这两个转换也是可逆的。下面就是从[]rune或[]byte反向转换为string的例子:
// chapter3/sources/string_slice_to_string.go
func main()
rs := []rune
0x4E2D,
0x56FD,
0x6B22,
0x8FCE,
0x60A8,
s := string(rs)
fmt.Println(s)
sl := []byte
0xE4, 0xB8, 0xAD,
0xE5, 0x9B, 0xBD,
0xE6, 0xAC, 0xA2,
0xE8, 0xBF, 0x8E,
0xE6, 0x82, 0xA8,
s = string(sl)
fmt.Println(s)
$go run string_slice_to_string.go
中国欢迎您
中国欢迎您
复制代码
转换是要付出代价的,这些代价的根源在于string是不可变的,运行时要为转换后的类型分配新内存。
想要更高效地进行转换,唯一的方法就是减少甚至避免额外的内存分配操作。
在日常Go编码中,我们会经常遇到将slice临时转换为string的情况。Go编译器为这样的场景提供了优化(slicebytetostringtmp函数):
// $GOROOT/src/runtime/string.go
func slicebytetostringtmp(b []byte) string
if raceenabled && len(b) > 0
racereadrangepc(unsafe.Pointer(&b[0]),
uintptr(len(b)),
getcallerpc(),
funcPC(slicebytetostringtmp))
if msanenabled && len(b) > 0
msanread(unsafe.Pointer(&b[0]), uintptr(len(b)))
return *(*string)(unsafe.Pointer(&b))
复制代码
该函数的“秘诀”就在于不为string新开辟一块内存,而是直接使用slice的底层存储。当然使用这个函数的前提是:在原slice被修改后,这个string不能再被使用了。因此这样的优化是针对以下几个特定场景的。
(1)string(b)用在map类型的key中
b := []byte'k', 'e', 'y'
m := make(map[string]string)
m[string(b)] = "value"
m[[3]stringstring(b), "key1", "key2"] = "value1"
复制代码
(2)string(b)用在字符串连接语句中
b := []byte't', 'o', 'n', 'y'
s := "hello " + string(b) + "!"
复制代码
(3)string(b)用在字符串比较中
s := "tom"
b := []byte't', 'o', 'n', 'y'
if s < string(b)
...
复制代码
Go编译器对用在for-range循环中的string到[]byte的转换也有优化处理,它不会为[]byte进行额外的内存分配,而是直接使用string的底层数据。
在如今强大的硬件算力面前,少数几次string和slice的转换代价可能微不足道。但能充分理解Go编译器对string和slice互转在特定场景下的优化依然是大有裨益的。在性能敏感的领域,这些优化也许能起到大作用。
以上是关于《Go语言精进之路》读书笔记 | 了解string实现原理并高效使用的主要内容,如果未能解决你的问题,请参考以下文章
《Go语言精进之路》读书笔记 | 了解map实现原理并高效使用
《Go语言精进之路》读书笔记 | 了解切片实现原理并高效使用