Go语言的切片实现可以看runtime的slice.go文件,切片是Go语言内置的数据结构,编译器识别到切片语法操作时会自动调用runtime对应底层实现,所以用起来就非常方便,这也是语言级的实现比库实现的优势。
可以通过一个例子来看切片的内存模型:
// go version: 1.7
package main
import (
"fmt"
"reflect"
"unsafe"
)
// GDB时,避免局部变量可能会被优化,全局变量方便GDB查看进程对应内存
// 编译时加调试信息并去掉优化: go build -gcflags "-N -l" hi.go
var (
a = [...]int{1, 2, 3, 4, 5} // 数组
b = a[2:4] // 切片 --> 通过数组+[:]语法生成切片
c = &a // 指针指向数组
d = []int{1, 2, 3} // 切片 --> 直接初始化切片[]Type{data..}语法
)
// runtime: 拷贝自slice.go的私有结构体, 64位操作系统就是24个字节大小
type slice struct {
array unsafe.Pointer
len int
cap int
}
func main() {
fmt.Println("&a[2]", &a[2]) // &a[2] 0xf8270
fmt.Println("len", len(b), "cap", cap(b)) // len 2 cap 3
fmt.Println(reflect.TypeOf(a), unsafe.Sizeof(a)) // [5]int 40
fmt.Println(reflect.TypeOf(b), unsafe.Sizeof(b)) // []int 24
fmt.Println(reflect.TypeOf(c), unsafe.Sizeof(c)) // *[5]int 8
fmt.Printf("%+v\n", *(*slice)(unsafe.Pointer(&b))) // {array:0xf8270 len:2 cap:3}
fmt.Printf("%+v\n", *(*slice)(unsafe.Pointer(&d))) // {array:0xf8090 len:3 cap:3}
// append 出发自动扩容,生成一个新的切片实例
fmt.Println(unsafe.Pointer(&d)) // 0xfa100
d = append(d, 4)
fmt.Printf("%+v\n", *(*slice)(unsafe.Pointer(&d))) // {array:0xc4200121e0 len:4 cap:6}
fmt.Println(unsafe.Pointer(&d)) // 0xfa100
}
内置函数 append 会出发切片的扩容,这个跟C++ vector实现原理有点类似,申请个新的底层数组,拷贝原有数据过去。
// 切片的append操作,返回一个新切片
slice = append(slice, elem1, elem2)
slice = append(slice, anotherSlice...)
// runtime src: slice.go, @fun: growslice
// 扩容策略: 小于1024,每次扩容*2; 大于1024,每次扩容*1.25
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
for newcap < cap {
newcap += newcap / 4
}
}
}
切片使用起来非常的方便,这都得益于go语言语法级对切片的支持,很容用[:]之类的写法来生成切片实例。还有go的协程也是类似,而库级别的实现远远没有语法级别的支持用起来方便,这都是go的优势所在。