字符串String

字符串的底层表示

type StringHeader struct {
	Data uintptr
	Len int
}
  • Data 指向底层的字符数组
  • Len 表示字符串的长度

Go语言中,所有的文件都采用UTF-8编码,字符常量也是用的是UTF-8字符编码集。UTF-8 是可变长的编码方式,比如ASCII码 就用一个字节表示,中文就是3个字节表示。我记得ASCII的UTF-8编码是最高位为0。

使用range 轮询字符串,所出来的是utf-8编码的符文rune。比如"Hello你好",它轮询的时候,就是挨个输出H e l l o 你 好。

字符串与字节数组的转换

a := "hello world"
b := []byte(a)
c := string(b)

当a通过[]byte转换为b后,可能会觉得Go会把StringHeader.Data指向的字符数组地址直接给到b,但不是,因为string是不可修改的,所以StringHeader.Data指向的字符数组是不能直接拿出来修改的。所以这里就牵涉到了复制,当字符串长度大于32字节,还需要申请堆内存,所以牵涉到大字符串转换的时候,要考虑一下对内存的影响,比如日志的函数内部就不可能直接将字符串转换为[]byte数组。所以要考虑如何用指针就能将其弄出来呢?需要熟悉unsafe.Pointer 和 uintptr转换,可以看这篇文章Go指针的使用限制以及unsafe.Pointer的突破之路

这里,我们先把Slice的底层结构给出来

type SliceHeader struct {
	Data uintptr
	Len int
	Cap int
}

type StringHeader struct {
	Data uintptr
	Len int
}

 // 在了解Slice和String的底层结构表示后,也可以使用反射类型来构造
func string2bytes(s string) []byte {
    stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))
    bh := reflect.SliceHeader{
        Data: stringHeader.Data,
        Len: stringHeader.Len,
        Cap: stringHeader.Len,
    }
    return *(*[]byte)(unsafe.Pointer(&bh))
}

func bytes2string(b []byte) string {
    sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&b))
    sh := reflect.StringHeader{
        Data: sliceHeader.Data,
        Len:  sliceHeader.Len,
    }
    return *(*string)(unsafe.Pointer(&sh))
}

 func str2bytes(s string) []byte {
 	// 首先呢,获取string的指针,然后呢将其转换为unsafe.Pointer后,再将
 	// 其转换为uintptr的2元素数组,为啥是2呢,因为StringHeader的结构
 	// 就是两个字段。因此呢,拿着StringHeader的Data和Len去构造[]byte
 	// 的SliceHeader即可,可以看到SliceHeader就是多了一个Cap。因此呢,
 	// 将Len作为Cap就行,也就是底下的x[1]。
     x := (*[2]uintptr)(unsafe.Pointer(&s))
     h := [3]uintptr{x[0], x[1], x[1]}
     return *(*[]byte)(unsafe.Pointer(&h))
 }

 func bytes2str(b []byte) string {
     return *(*string)(unsafe.Pointer(&b))
 }

以上方法,都可以直接零拷贝完成字符串和字节数组的转换。

数组Array

数组中的复制都是值拷贝,它的类型是[n]T。
比如

package main

import (
	"fmt"
)

func main() {
	a := [3]int{1, 2, 3}
	b := a
	afunc(a)
	fmt.Printf("a:%p, b: %p, a的类型: %T", &a, &b, a)
}

func afunc(c [3]int) {
    fmt.Printf("c: %p", &c)
}
// 输出c: 0xc000016060, a: 0xc000016020, b: 0xc000016040, a的类型: [3]int

你会发现a b c的地址都不一样,因为他是一个值拷贝,所以这一点是需要注意的,因此尽量不要进行大数组的拷贝。

底层结构

type Array struct {
	Elem *Type
	Bound int64
}

编译时,还会对数组进行优化,比如数组长度小于4时,会采用挨个赋值的方式,并在栈上进行初始化。数组长度大于4,会在静态区初始化数据。

切片Slice

切片的底层结构是

type SliceHeader struct {
	Data uintptr
	Len int
	Cap int
}
  • Data 是指向底层数组的指针
  • Len 是数据的长度
  • Cap 是数组的长度,也是Slice的容量

切片的复制

数组是值拷贝,切片也是值拷贝。但与数组不同的是,切片的值拷贝是拷贝SliceHeader。那么其指向底层的数组Data指针就同样被拷贝了,所以在将Slice传入函数后,在函数内对Slice的数据所做修改将会同样影响函数外部的Slice。我们看以下的例子,来解释说明一下。


func main() {
	var a []int
	a = append(a, 1, 2, 3, 4)
	fmt.Printf("before func: %v\n", a)
	afunc(a)
	fmt.Printf("after func: %v\n", a)
}

func afunc(b []int) {
	b[0] = 0
	b[1] = 1
	b[2] = 2
	b[3] = 3
	b = append(b, 4)
	b = append(b, 5)
	fmt.Printf("in func: %v\n", b)
}

// output: 
// before func: [1 2 3 4]
// in func: [0 1 2 3 4 5]
// after func: [0 1 2 3]

如果在函数内对Slice进行了扩容,比如新增数据了,那么在函数内部Len值就会增大,如果新增的数据比较多,那么还有可能导致Cap也变化。此时呢,Slice的Data指针就可能有变化。但是函数外部的Slice并不能感知到Data和Len以及Cap的变化,因此此时的修改就会导致函数内外的Slice不一致了。这也是我们在使用append时,通常会将结果再赋值到原切片的原因。

切片的扩容

那么在切片新增数据的时候,在超过容量Cap的时候,就需要扩容。扩容时,就是对Data所指向的数组进行扩容。扩容的策略,按照以下方式来。

如果 新申请容量 > 2倍的旧容量
结果是新申请容量
否则
如果 旧切片的长度 < 1024
结果就是旧容量的2倍
否则
结果就是旧容量+=25% x 旧容量,直到最终容量大于或等于新申请的容量。

如果 最终申请的容量 < 0
则最终容量就是新申请的容量。 这种情况是以上的操作可能溢出了int的最大值。

Slice的复制

可以通过copy来复制,copy内部使用memmove来实现深拷贝。

a1 := []int{1, 2, 3, 5}
a2 := make([]int, len(a1), cap(a1))
// 将a1的内容深拷贝到a2中
copy(a2, a1)

逃逸

slice字面量初始化会以数组的形式存储于静态区。在使用make函数初始化时,如果大小超过64KB,这个切片就会逃逸到堆上。小于64KB,就存储到栈上。