15|同构复合类型:从定长数组到变长切片
15|同构复合类型:从定长数组到变长切片
讲述:Tony Bai
时长21:31大小19.65M
数组有哪些基本特性?
多维数组怎么解?
切片是怎么一回事?
Go 是如何实现切片类型的?
切片的动态扩容
小结
思考题
赞 35
提建议
精选留言(41)
- Darren2021-11-15课后题测试代码如下: var sl1 []int var sl2 = []int{} fmt.Print("========基本区别=========\n") fmt.Printf("%v,len:%d,cap:%d,addr:%p\n", sl1, len(sl1), cap(sl1), &sl1) fmt.Printf("%v,len:%d,cap:%d,addr:%p\n", sl2, len(sl2), cap(sl2), &sl2) fmt.Printf("sl1==nil:%v\n", sl1 == nil) fmt.Printf("sl2==nil:%v\n", sl2 == nil) a1 := *(*[3]int)(unsafe.Pointer(&sl1)) a2 := *(*[3]int)(unsafe.Pointer(&sl2)) fmt.Print("========底层区别=========\n") fmt.Println(a1) fmt.Println(a2) type SliceDemo struct { Values []int } var s1 = SliceDemo{} var s2 = SliceDemo{[]int{}} bs1, _ := json.Marshal(s1) bs2, _ := json.Marshal(s2) fmt.Print("========序列化区别=========\n") fmt.Println(a1) fmt.Println(string(bs1)) fmt.Println(string(bs2)) ========基本区别========= [],len:0,cap:0,addr:0xc0000a6018 [],len:0,cap:0,addr:0xc0000a6030 sl1==nil:true sl2==nil:false ========底层区别========= [0 0 0] [18601168 0 0] ========序列化区别========= [0 0 0] {"Values":null} {"Values":[]} 可以看到,日常的使用基本是没有区别的,只不过与nil的比较,以及底层数据结构和序列化还是有一定的区别的。 同时go官方推荐使用 var sl1 []int The former declares a nil slice value, while the latter is non-nil but zero-length. They are functionally equivalent—their len and cap are both zero—but the nil slice is the preferred style. 在goland开发时,第二种声明方式会出现黄色下划线,提示需要改动。展开
作者回复: 思考题完成的很细致,很全面。手动点赞!
共 5 条评论65 - 在下宝龙、2021-11-15var sl1 []int var sl2 = []int{} s1是声明,还没初始化,是nil值,底层没有分配内存空间。 s2初始化了,不是nil值,底层分配了内存空间,有地址。 我是这么理解的。展开
作者回复: 正确✅
37 - trietree2021-11-15sl1未初始化,值为nil,和nil比较返回true sl2初始化为empty slice,和nil比较返回false
作者回复: 正确✅
14 - 风铃2022-02-23个人感觉,在初始化切片的时候,最好的分配项目的需求,分配一定的容量(cap),要不在切片里面的数据多了,每次进行扩容,会消耗大量的内存性能
作者回复: 👍。
6 - 松2022-01-28有个疑问,切片的底层数组,在切片发生自动扩容后,在物理空间上还是连续的吗?
作者回复: 扩容是新分配一段连续的大点的内存,原先的内存块不要了。所以依旧是连续的。
共 4 条评论6 - 罗杰2021-11-15数组在传参的时候是作为一个整体的,这个我倒是我没注意到。不过切片坑是真的多,需要先好好了解底层原理,不然很容易被坑。可以再看看饶大切片相关文章补充一下:https://qcrao.com/2019/04/02/dive-into-go-slice/共 4 条评论3
- 码狐2022-03-07var sl1 []int 不显示初始化,所以 sl1 对应 slice 的零值 nil,并且此时没有 ptr、len 和 cap var sl2 = []int{} 显示初始化,sl2 对应 [] 空数组,ptr 指定空数组的地址,len 和 cap 都是 0
作者回复: ✅
2 - Calvin2021-11-15var sl1 []int var sl2 = []int{} ------------- 1)sl1 是 nil slice,len 和 cap 是 0,array 是 nil,sl1 == nil 为 true; 2)sl2 是 empty slice,len 和 cap 是 0,array 是 zerobase uintptr (base address for all 0-byte allocations, 见 runtime/malloc.go),sl2 == nil 为 false。展开
作者回复: ✅
共 4 条评论2 - 布凡2021-11-15var sl1 []int var sl2 = []int{} s1是声明,还没初始化,是nil值,底层没有分配内存空间。这就意味这针对sl1做操作的时候同时初始化 ,例如sl1 = append(sl1, 1),这个语句的操作就是先初始化一个长度为1的空间,然后把 “1”填入这个空间中。 s2初始化了,不是nil值,底层分配了内存空间,有地址。例如,sl2 = append(sl2, 2),这个语句就是直接将“2”这个值填入到已初始化的空间中。展开
作者回复: ✅
2 - liaomars2021-11-15文章末的思考题回答: var sl1 []int ,这种方式是nil切片,长度和容量都是0,跟nil比较结果是为true var sl2 = []int{},这种方式是空切片,空切片的数据指针都是指向同一个地址,跟nil比较结果是false
作者回复: ”空切片的数据指针都是指向同一个地址“ -- 这个值得商榷哦。 var sl1 = []int{} var sl2 = []int{} println((&sl1) == (&sl2)) // false
共 2 条评论2 - 布凡2021-11-15老师,请问下为什么go没有class 这个类型呢?是因为想要开发者多用组合少用继承的设计理念吗?还是有其它原因呢?
作者回复: class是面向对象语言的专有名词。go定位就不是oo语言,所以没有class。
共 3 条评论2 - 笑忘日月星辰2022-06-25老师好,关于扩容,麻烦解惑下 问题:扩容当小于1024时候,是扩容为当前的2倍;当大于1024小于1280时候扩容为1.25倍,当大于1280小于1696时候,扩容为1.325倍吗?这个扩容的规则是什么? func main() { var s []int for i := 0; i < 2048; i++ { s = append(s, i) fmt.Println(len(s), cap(s)) } } 打印结果 --------------------------------------------------- 512 512 513 1024 ... 1024 1024 1025 1280 ... 1280 1280 1281 1696 ... 1696 1696 1697 2304 ...展开
作者回复: 你用的是什么版本的go编译器,go 1.18,如果是go 1.18,那么可以看https://mp.weixin.qq.com/s/4wYrwBwnuylSvTaxBMXUgg ,go 1.18对slice的扩容算法了调整。
1 - 程旭阳2021-11-15``` package main import "fmt" func main() { var sl1 []int var sl2 = []int{} fmt.Printf("%T, %v, %p\n", sl1, sl1, sl1) // []int, [], 0x0 fmt.Printf("%T, %v, %p\n", sl2, sl2, sl2) // []int, [], 某个地址值 fmt.Println(sl1 == nil) // true fmt.Println(sl2 == nil) // false fmt.Println(len(sl1), cap(sl1)) // 0, 0 fmt.Println(len(sl2), cap(sl2)) // 0, 0 // fmt.Println(sl1[0]) 下标越界 panic // fmt.Println(sl2[0]) 下标越界 panic sl1 = append(sl1, 1) // 可以 append 操作 sl2 = append(sl2, 1) // 可以 append 操作 } ```展开
作者回复: 正确✅
1 - Unknown element2022-12-05 来自广东在评论区追问好像不会被回复。。那我只能再问一下了 func main() { arr := [3]int{1, 2, 3} sl := arr[1:2] fmt.Printf("%v, addr: %p, len: %d, cap: %d\n", sl, &sl, len(sl), cap(sl)) sl = append(sl, 222) fmt.Printf("%v, addr: %p, len: %d, cap: %d\n", sl, &sl, len(sl), cap(sl)) sl = append(sl, 333) fmt.Printf("%v, addr: %p, len: %d, cap: %d\n", sl, &sl, len(sl), cap(sl)) sl[1] = 111 fmt.Println(arr, sl) } 输出: [2], addr: 0xc000004078, len: 1, cap: 2 [2 222], addr: 0xc000004078, len: 2, cap: 2 [2 222 333], addr: 0xc000004078, len: 3, cap: 4 [1 2 222] [2 111 333] 从这段程序的输出可以看到最后一次append扩容了(cap翻倍了)但是为什么地址没有变呢 我的go版本是1.17.3 (老师您上次的回答没答到点上,所以允许我再问一次。。。)展开
作者回复: 追问的评论在后台的确看不到,这个应该是平台没实现的feature。 扩容是slice底层数组扩容,slice的地址不变。
- Unknown element2022-12-03 来自辽宁老师我想请教两个问题: 1. 下面这段代码的输出我理解不了: func main() { arr := [3]int{1, 2, 3} sl := arr[1:2] fmt.Printf("%v, addr: %p, len: %d, cap: %d\n", sl, &sl, len(sl), cap(sl)) sl = append(sl, 222) fmt.Printf("%v, addr: %p, len: %d, cap: %d\n", sl, &sl, len(sl), cap(sl)) sl = append(sl, 333) fmt.Printf("%v, addr: %p, len: %d, cap: %d\n", sl, &sl, len(sl), cap(sl)) sl[1] = 111 fmt.Println(arr, sl) } 输出: [2], addr: 0xc000004078, len: 1, cap: 2 [2 222], addr: 0xc000004078, len: 2, cap: 2 [2 222 333], addr: 0xc000004078, len: 3, cap: 4 [1 2 222] [2 111 333] 在 sl 第二次 append 时可以看到 cap 翻倍了,那么应该是扩容了,因此 sl 不再指向 arr 的第二个元素,最后 sl[1] = 111 可以佐证这一点,因为假设 sl 指向 arr[1] 那么 sl[1] 就是 arr[2],会被这里的赋值操作修改为 111,但是输出的结果中 arr[2] 依然是 222; 然而 sl 的地址在 append 过程中始终没变,看起来又好像没有扩容; 上面两个现象感觉很矛盾,希望老师能解答一下,我本地的 go 版本是1.17.3 2. 我想问下怎么看一个预定义标识符的运行时表示是一个结构体还是指针,比如第 16 节的文章中有明确说 map 的运行时表示是 *runtime.hmap,所以是指针;第 13 节的文章中说 string 的运行时表示是 stringHeader,所以是结构体;这一节的切片的运行时表示是 slice 这个结构体本身还是一个指向 slice 的指针呢? 希望老师抽空解答一下 谢谢老师!展开
作者回复: 问题1: 第二次append没有扩容啊。看第一次的输出结果,len:1, cap:2,所以sl有空间容纳第二次append的222。第三次才是扩容。 问题2:这个没有什么标准,go一共就那么几个预定义标识符,记住了就行了。slice就是一个slice结构体,不是指针。
共 3 条评论 - 撕影2022-11-20 来自北京课后题测试 var sl1 []int var sl2 = []int{} 我是这么理解的 第一个是声明,第二个是声明且实例化,(凡带=号的都有实例化的意思),就好像c语言 char* p 跟 char* p =null展开
作者回复: 也没错。也可以再看看其他人的答案,从多角度理解一下。
- Sunrise2022-11-16 来自北京append 之后为什么要重新赋给原先的切片这块有点疑问,望老师抽空解答 u := [5]int{1, 2} s1 := u[0:1] s2 := append(s1, 3) fmt.Printf("%p %p %v %v", s1, s2, s1, s2) // 0xc000100000 0xc000100000 [1] [1 3] 这里 s1 和 s2 的地址一样,为什么 s1 和 s2 的值不一样,它们的底层数组不都是同一块吗展开
作者回复: 切片是有长度的啊。s1的长度是1,所以输出1一个元素:1。s2是在s1的基础上append了一个元素,它的长度是2,所以输出s2时输出[1 3]。这个3把原数组u中的第二个元素2给覆盖了。
- 不明真相的群众2022-10-06 来自北京有一个疑问,切片是基于数组创建的,数组的存储空间是连续的,那么如果对切片进行扩容,扩容出来的 和已经存在的原始数据 存储空间也是连续的吗?
作者回复: 扩容后,切片的存储空间也是连续的,但这个存储空间与原数组已经不是一个存储空间了,彻底分离了。
共 2 条评论 - undefined.2022-08-29 来自辽宁假设切片apped超过cap后,重新分配空间,新切片修改数据不会改变原来的数组,那不是很容易混淆或者引发bug ? 请问这个在实际生产上是怎么解决的呢
作者回复: 文中仅仅是为了演示构造的一个例子。通常我们不会去基于数组构建切片,而是直接用make或[]T{}来构建切片变量。全程使用切片变量不需要开发者关心底层存储数据的数组的变化,即便扩容也是自动的,对开发者无感,开发者只需要操作切片变量即可。
共 2 条评论 - 左卫康2022-08-05 来自北京思考题:var sl1 []int 仅声未初始化,值为对应类型的零值,nil var sl2 = []int{} 声明并初始化,是个空切片
作者回复: ✅
1