字符串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,就存储到栈上。