本文是听过Go进阶训练营《再探Golang的slice、map、channel》之后一些思考。
一、array
数组的定义:
var buffer [256]byte
array的存在是必要的,它可以很好的表示矩阵信息,但是在Go中数组的存在,更重要的意义在于作为slice的源数据存储介质。
二、slice
切片的定义:
var slice []byte = buffer[100:150]
那么slice这个变量在这里究竟是什么呢?我们采用下面的表述形式来理解slice:
type sliceHeader struct {
Length int
ZerothElement *byte
}
slice := sliceHeader{
Length: 50,
ZerothElement: &buffer[100],
}
其中主要包含了长度和一个指向数组某个元素的指针。Go程序员们经常谈论“slice header”,因为“slice header”是slice变量实际存储的内容。
三、slice作函数入参
slice是个对象,包含了一个指针和一个长度值。它并不是一个指向对象的指针。
这一点非常重要。
举例如下:
func AddOneToEachElement(slice []byte) {
for i := range slice {
slice[i]++
}
}
slice作为参数传到函数中时,实际是传递的slice header的复制。尽管复制了一份slice header,但是不论是原始slice header,还是复制的slice header,它们指向的同一个数组。所以上述函数,会把slice范围内的数组的元素都进行加一操作。
我们可以通过传递slice header改变数组中的值,但是由于传递的是slice header的复制,无法改变slice header的值。如果想要改变slice header的值,需要把新的slice header作为值返回。
四、 slice的Capacity
Capacity记录了数组元素的个数。当试图让slice的右边界超过Capacity时,会触发数组越界的panic。
此时sliceHeader的定义多了一个Capacity的字段:
type sliceHeader struct {
Length int
Capacity int
ZerothElement *byte
}
这也符合了go源码中对于slice的定义:
type slice struct {
array unsafe.Pointer
len int
cap int
}
五、make
我们无法让扩展slice超过它的Capacity,但是可以通过make指令新申请一个数组,把数据拷贝过去,让slice在新的数组上进行表述。例如:
slice := make([]int, 10, 15)
newSlice := make([]int, len(slice), 2*cap(slice))
copy(newSlice, slice)
slice = newSlice
六、append
append,有两点需要注意:
- slice扩容,当cap < 1024时,每次2;当cap >= 1024 时,每次1.25
- 由于slice header经常变动,所以append必须返回slice header的值,编译器也不允许调用append时,不对新的slice header进行保存
- 预先分配内存可以提升性能
- 直接使用index赋值而非append可以提升性能
理解了上述逻辑,slice作为入参传入函数时,如果slice未扩容,那么变更的数组还是在原来的数组上。如果slice扩容了,数组是新创建的,变更就不再原数组上了。
七、nil
举例:
func main() {
var s[]int
b, _ := json.Marshal(s)
fmt.Println(string(b))
}
上述程序会打印null,说明底层数组还没有创建。
func main {
s := []int{}
b, _ := json.Marshal(s)
fmt.Println(string(b))
}
上述程序会打印[],说明这种方式底层的数组已经创建了。
这是一个坑点:
- 使用[]Type{}或者make([]Type)初始化后,slice不为nil
- 使用var x[]Type后,slice为nil
八、string
string是元素类型为byte的只读的slice。
因此可以进行如下操作:
slash := "/usr/ken"[0] // yields the byte value '/'.
或者
usr := "/usr/ken"[0:4] // yields the string "/usr"
可以对string和slice进行强制类型转换
str := string(slice)
slice := []byte(usr)
创建string的切片就开销很小,只需要创建string header就可以了,由于string是只读的slice,所以不同的string header可以安全地共享同一个数组。
九、一个技巧
下面的两个函数,哪个效率更高?
func normal(s []int) {
i := 0
i += s[0]
i += s[1]
i += s[2]
i += s[3]
println(i)
}
func bce(s []int) {
_ = s[3]
i := 0
i += s[0]
i += s[1]
i += s[2]
i += s[3]
println(i)
}
bce的汇编会被编译器优化,减少slice越界的检查次数,所以bce函数的效率更高
这里的经验是,如果能确定访问的slice长度,可以先执行一次让编译器去做优化。
十、总结
明确slice header的概念,对理解slice的各种行为非常有帮助,希望对读者有所帮助。