golang中的slice非常强大,让数组操作非常方便高效。在开发中不定长度表示的数组全部都是slice。但是很多同学对slice的模糊认识,造成认为golang中的数组是引用类型,结果就是在实际开发中碰到很多坑,以至于出现一些莫名奇妙的问题,数组中的数据丢失了。
下面我们就开始详细理解下slice,理解后会对开发出高效的程序非常有帮助。
这个是slice的数据结构,它很简单,一个指向真实array地址的指针ptr,slice的长度len和容量cap。
其中len和cap就是我们在调用len(slice)和cap(slice)返回的值。
我们来按照slice的数据结构定义来解析出ptr,len,cap
// 按照上图定义的数据结构
type Slice struct {
ptr unsafe.Pointer // Array pointer
len int // slice length
cap int // slice capacity
}
下面写一个完整的程序,尝试把golang中slice的内存区域转换成我们定义的Slice进行解析
package main
import (
"fmt"
"unsafe"
)
// 按照上图定义的数据结构
struct {
ptr unsafe.Pointer // Array pointer
len int // slice length
cap int // slice capacity
}
// 因为需要指针计算,所以需要获取int的长度
// 32位 int length = 4
// 64位 int length = 8
var intLen = int(unsafe.Sizeof(int(0)))
func main() {
s := make([]int,10,20)
// 利用指针读取 slice memory 的数据
if intLen == 4 { // 32位
m := *(*[4 + 4*2]byte)(unsafe.Pointer(&s))
fmt.Println("slice memory:",m)
} else { // 64 位
m := *(*[8 + 8*2]// 把slice转换成自定义的 Slice struct
slice := (*Slice)(unsafe.Pointer(&s))
fmt.Println("slice struct:",slice)
fmt.Printf("ptr:%v len:%v cap:%v \n",slice.ptr,slice.len,255)">cap)
fmt.Printf("golang slice len:%v cap:%v \n",len(s),255)">cap(s))
s[0] = 0
s[1] = 1
s[2] = 2
// 转成数组输出
arr := *(*[3]int)(unsafe.Pointer(slice.ptr))
fmt.Println("array values:",arr)
// 修改 slice 的 len
slice.len = 15
fmt.Println("Slice len: ",255)">len)
fmt.Println("golang slice len: ",255)">len(s))
}
运行一下查看结果
$ go run slice.go
slice memory: [0 64 6 32 200 0 0 0 10 0 0 0 0 0 0 0 20 0 0 0 0 0 0 0]
slice struct: &{0xc820064000 10 20}
ptr:0xc820064000 len:10 cap:20
golang slice cap:20
array values: [0 1 2]
Slice len: 15
golang slice len: 15
看到了,golang slice的memory内容,和自定义的Slice的值,还有按照slice中的指针指向的内存,就是实际Array数据。当修改了slice中的len,len(s)也变了。
接下来结合几个例子,了解下slice一些用法
声明一个Array通常使用make,可以传入2个参数,也可传入3个参数,第一个是数据类型,第二个是len,第三个是cap。如果不穿入第三个参数,则cap=len,append可以用来向数组末尾追加数据。
这是一个append的测试
// 每次cap改变,指向array的ptr就会变化一次
s := 1)
fmt.Printf("len:%d cap: %d array ptr: %v \n",255)">cap(s),*(*unsafe.Pointer)(unsafe.Pointer(&s)))
for i := 0; i < 5; i++ {
s = append(s,i)
fmt.Printf("Array:",s)
运行结果
len:1 cap: 1 array ptr: 0xc8200640f0
len:2 cap: 2 array ptr: 0xc820064110
len:3 cap: 4 array ptr: 0xc8200680c0
len:4 len:5 cap: 8 array ptr: 0xc82006c080
len:6 Array: [0 0 1 2 3 4]
看出来了吧,每次cap改变的时候指向array内存的指针都在变化。当在使用append的时候,如果cap==len了这个时候就会新开辟一块更大内存,然后把之前的数据复制过去。
实际go在append的时候放大cap是有规律的。在cap小于1024的情况下是每次扩大到2 * cap,当大于1024之后就每次扩大到1.25 * cap。所以上面的测试中cap变化是 1,2,4,8
在实际使用中,我们最好事先预期好一个cap,这样在使用append的时候可以避免反复重新分配内存复制之前的数据,减少不必要的性能消耗。
创建切片
s := []int{1,2,3,4,5}
fmt.Printf(s),cap(s)))
fmt.Println(s)
s1 := s[1:3]
fmt.Printf("Array",s1)
运行结果
cap: 5 array ptr: 0xc820012210
Array: [1 2 3 4 5]
ptr: 0xc820012218
Array [2 3]
在一个切片基础上创建新的切片s1,新切片的ptr指向的就是s1[0]数据的内存地址。可以看到指针地址0xc820012210与0xc820012218相差8byte正好是一个int类型长度,cap也相应的变为4
就写到这里了,总结一下,切片的结构是指向数据的指针,长度和容量。复制切片,或者在切片上创建新切片,切片中的指针都指向相同的数据内存区域。
知道了切片原理就可以在开发中避免出现错误了,希望这篇博客可以给大家带来帮助。
参考:https://blog.golang.org/go-slices-usage-and-internals
附上 go 源码中slice的数据结构定义
type slice struct {
array unsafe.Pointer
int
cap int
}