博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
golang slice 切片原理
阅读量:6548 次
发布时间:2019-06-24

本文共 3788 字,大约阅读时间需要 12 分钟。

golang 中的 slice 非常强大,让数组操作非常方便高效。在开发中不定长度表示的数组全部都是 slice 。但是很多同学对 slice 的模糊认识,造成认为golang中的数组是引用类型,结果就是在实际开发中碰到很多坑,以至于出现一些莫名奇妙的问题,数组中的数据丢失了。

下面我们就开始详细理解下 slice ,理解后会对开发出高效的程序非常有帮助。

这个是 slice 的数据结构,它很简单,一个指向真实 array 地址的指针 ptrslice 的长度 len 和容量 cap

496176-20160514133733937-1151272381.png

496176-20160514133752702-1527122175.png

其中 lencap 就是我们在调用 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 mainimport (    "fmt"    "unsafe")// 按照上图定义的数据结构type Slice struct {    ptr unsafe.Pointer // Array pointer    len int            // slice length    cap int            // slice capacity}// 因为需要指针计算,所以需要获取int的长度// 32位 int length = 4// 64位 int length = 8var 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]byte)(unsafe.Pointer(&s))        fmt.Println("slice memory:", m)    }    // 把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, slice.cap)    fmt.Printf("golang slice len:%v cap:%v \n", len(s), 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: ", slice.len)    fmt.Println("golang slice len: ", len(s))}

运行一下查看结果

$ go run slice.goslice 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:20golang slice len:10 cap:20array values: [0 1 2]Slice len:  15golang slice len:  15

看到了,golang slice 的memory内容,和自定义的 Slice 的值,还有按照 slice 中的指针指向的内存,就是实际 Array 数据。当修改了 slice 中的len, len(s) 也变了。

接下来结合几个例子,了解下slice一些用法

声明一个Array通常使用 make ,可以传入2个参数,也可传入3个参数,第一个是数据类型,第二个是 len ,第三个是 cap 。如果不穿入第三个参数,则 cap=lenappend 可以用来向数组末尾追加数据。

这是一个 append 的测试

// 每次cap改变,指向array的ptr就会变化一次s := make([]int, 1)fmt.Printf("len:%d cap: %d array ptr: %v \n", len(s), cap(s), *(*unsafe.Pointer)(unsafe.Pointer(&s)))for i := 0; i < 5; i++ {    s = append(s, i)    fmt.Printf("len:%d cap: %d array ptr: %v \n", len(s), cap(s), *(*unsafe.Pointer)(unsafe.Pointer(&s)))}fmt.Println("Array:", s)

运行结果

len:1 cap: 1 array ptr: 0xc8200640f0len:2 cap: 2 array ptr: 0xc820064110len:3 cap: 4 array ptr: 0xc8200680c0len:4 cap: 4 array ptr: 0xc8200680c0len:5 cap: 8 array ptr: 0xc82006c080len:6 cap: 8 array ptr: 0xc82006c080Array: [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("len:%d cap: %d array ptr: %v \n", len(s), cap(s), *(*unsafe.Pointer)(unsafe.Pointer(&s)))fmt.Println("Array:", s)s1 := s[1:3]fmt.Printf("len:%d cap: %d array ptr: %v \n", len(s1), cap(s1), *(*unsafe.Pointer)(unsafe.Pointer(&s1)))fmt.Println("Array", s1)

运行结果

len:5 cap: 5 array ptr: 0xc820012210Array: [1 2 3 4 5]len:2 cap: 4 array ptr: 0xc820012218Array [2 3]

在一个切片基础上创建新的切片 s1 ,新切片的 ptr 指向的就是 s1[0] 数据的内存地址。可以看到指针地址 0xc8200122100xc820012218 相差 8byte 正好是一个int类型长度,cap也相应的变为4

就写到这里了,总结一下,切片的结构是指向数据的指针,长度和容量。复制切片,或者在切片上创建新切片,切片中的指针都指向相同的数据内存区域。

知道了切片原理就可以在开发中避免出现错误了,希望这篇博客可以给大家带来帮助。

参考:

附上 go 源码中 slice 的数据结构定义

type slice struct {    array unsafe.Pointer    len   int    cap   int}

转载于:https://www.cnblogs.com/hutusheng/p/5492418.html

你可能感兴趣的文章
Bootstrap vs Foundation如何选择靠谱前端框架
查看>>
与、或、异或、取反、左移和右移
查看>>
vue常用的指令
查看>>
matlab练习程序(随机游走图像)
查看>>
Linux命令行下运行java.class文件
查看>>
input文本框实现宽度自适应代码实例
查看>>
行为型设计模式之命令模式(Command)
查看>>
减少死锁的几个常用方法
查看>>
HDFS 核心原理
查看>>
正确配置jstl的maven依赖,jar包冲突的问题终于解决啦
查看>>
安装 MariaDB
查看>>
java 为啥变量名前要加个m?
查看>>
探索Android中的Parcel机制(上)
查看>>
C#开发微信门户及应用(5)--用户分组信息管理
查看>>
怎样实现前端裁剪上传图片功能
查看>>
ffmpeg+SDL2实现的视频播放器「退出、暂停、播放」
查看>>
2011/7/3 第二次评审
查看>>
tar解压
查看>>
Apriori 关联算法学习
查看>>
JSLint的使用
查看>>