前言

字符串作为文本处理的基础,在Go语言中扮演着举足轻重的角色。它提供了一种方便的方式来存储和操作文本数据。而切片,作为Go语言特有的动态数组实现,他不仅仅是数组的抽象,更是Go语言并发编程中不可或缺的组件。

本文旨在剖析字符串与切片的关键底层原理,不仅为面试准备提供有力支持,更关键的是,这些知识对于提升代码性能、优化内存分配策略以及编写出更加稳定可靠的代码具有不可估量的价值。

Go语言的string到底是什么

string的底层表示

想要探究string类型的底层到底是什么,我们从打印它的字节占用数开始实验,代码如下:

func main() {
    var a string = "好好学Golang"
    fmt.Println(unsafe.Sizeof(a))
}

我们惊奇地发现字符串的字节占用数居然是16。我们打开runtime/string.go,里面有一个stringStruct的结构体,它就是Go语言中字符串的底层表示。我们可以发现他由一个万能指针unsafe.Pointer和int的变量构成,由于我的机器是64位,所以两者的内存占用都是8字节,合起来正好是16。

type stringStruct struct {
    str unsafe.Pointer
    len int
}

既然我们已经探索了string的底层表示,下一步我们探究这个len表示的是数组长度还是字符的个数?runtime包中的数据结构是对外无法访问的,为了验证这个答案,我们必须把目光投放到反射包reflect/value.go中的StringHeader,每一个字符串在运行时都会使用这个结构体来表示。

type StringHeader struct {
	Data uintptr
	Len  int
}

由于string无法直接强转StringHeader,为我们先转为万能指针,然后再转到StringHeader,最后来打印Len的值,实验代码如下:

func main() {
    a := "好好学Golang"
    s := (*reflect.StringHeader)(unsafe.Pointer(&a))
    fmt.Println(s.Len) // 15
}

实验结果显示,长度是15。我们都知道一个汉字占3字节,这里是3个汉字,就是9字节,再加上6字节的英文,刚好是15字节。这说明Len表示的是数组占用的字节数。

编码问题

Go语言中所有字符均使用Unicode字符集,并使用utf-8可变长编码。那Golang是如何自动帮我们解析出来汉字或者英文的呢?

在探究这个问题之前,我们需要通过先讨论如何访问字符串。鉴于Go是可变长字符,我们无法通过遍历从i到len(str)来访问每个字符,实际上,我们需要通过for range来遍历,并用%c进行打印。

字符串被range遍历的时候,会被自动解码为rune类型的字符,可以被runtime/utf8.go这个包的算法自动解码。

func main() {
    a := "好好学Golang"
    for _, c := range a {
       fmt.Printf("%c\n", c)
    }
}

我们使用《好好学Go(一)》中查看Plan9汇编的命令go build -gcflags -S main.go,可以看到调用了decoderune

image.png

字符串切分

最后,我们讨论一下字符串切分的问题。当字符串需要切分时,我们需要先转为rune数组,然后切片,然后转为string。具体代码如下:

str = string([]rune(str)[:5])

如何理解Go语言的切片

切片的底层

切片的底层与字符串类似,有一个存放数据的指针,一个表示长度的Len,一个表示容量的Cap,它在runtime/slice.go下面。由于切片是一个动态数组,Len表示实际有数据的数组长度,Cap包含数组中空的部分,Cap>=Len。

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

切片的创建

我们一般会使用“根据字面量创建切片”以及“使用make创建切片”这两种方法。

// 根据字面量创建切片
slice := []int{1,2,3}
// 根据make创建切片
slice := make([]int,10)

我们分别探究这两种创建切片方式的底层实现区别。首先是根据字面量创建切片,它实际上底层先创建数组,然后创建slice结构体,放入三个值。我们查看汇编代码即可知晓,可以看出如果使用字面量的方式创建切片,大部分的工作都会在编译期间完成。

image.png

接着,我们探究make创建的底层实现。make创建切片,实际上是直接调用runtime.makeslice。当我们使用 make 关键字创建切片时,很多工作都需要运行时的参与。

image.png

切片的扩容

谈到切片就不能不谈起切片的扩容。切片扩容使用runtime.growslice函数。扩容原理如下:

  • 如果期望容量大于当前容量的两倍,就会直接使用期望容量。
  • 如果切片长度小于1024,容量会直接翻一倍。
  • 如果切片长度大于1024,每次会增加25%。

需要注意的是,切片扩容是并发不安全的,注意切片并发需要加锁

最后

本文仅仅是初探了Go语言中切片和字符串的部分原理,更多的底层原理会在后面的博文中继续探讨。