《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语言精进之路》读书笔记 | 了解切片实现原理并高效使用

《Go语言精进之路》读书笔记 | 了解Go语言控制语句惯用法及使用注意事项

《Go语言精进之路》读书笔记 | 汇总

《Go语言精进之路》读书笔记 | 选择适当的Go语言版本

《Go语言精进之路,从新手到高手的编程思想方法和技巧1》读书笔记和分享