GoLang切片相关问题梳理讲解
来源:脚本之家
时间:2022-12-30 19:25:36 379浏览 收藏
本篇文章给大家分享《GoLang切片相关问题梳理讲解》,覆盖了Golang的常见基础知识,其实一个语言的全部知识点一篇文章是不可能说完的,但希望通过这些问题,让读者对自己的掌握程度有一定的认识(B 数),从而弥补自己的不足,更好的掌握它。
1.数组和切片有什么区别
Go语言中数组是固定长度的,不能动态扩容,在编译期就会确定大小,声明方式如下:
var buffer [255]int buffer := [255]int{0}
切片是对数组的抽象,因为数组的长度是不可变的,在某些场景下使用起来就不是很方便,所以Go语言提供了一种灵活,功能强悍的内置类型切片(“动态数组”),与数组相比切片的长度是不固定的,可以追加元素。切片是一种数据结构,切片不是数组,切片描述的是一块数组,切片结构如下:
我们可以直接声明一个未指定大小的数组来定义切片,也可以使用make()函数来创建切片,声明方式如下:
var slice []int // 直接声明 slice := []int{1,2,3,4,5} // 字面量方式 slice := make([]int, 5, 10) // make创建 slice := array[1:5] // 截取下标的方式 slice := *new([]int) // new一个
切片可以使用append追加元素,当cap不足时进行动态扩容。
2.拷贝大切片一定比拷贝小切片代价大吗
这道题本质是考察对切片本质的理解,Go语言中只有值传递,所以我们以传递切片为例子:
func main() { param1 := make([]int, 100) param2 := make([]int, 100000000) smallSlice(param1) largeSlice(param2) } func smallSlice(params []int) { // .... } func largeSlice(params []int) { // .... }
切片param2要比param1大1000000个数量级,在进行值拷贝的时候,是否需要更昂贵的操作呢?
实际上不会,因为切片本质内部结构如下:
type SliceHeader struct { Data uintptr Len int Cap int }
切片中的第一个字是指向切片底层数组的指针,这是切片的存储空间,第二个字段是切片的长度,第三个字段是容量。将一个切片变量分配给另一个变量只会复制三个机器字,大切片跟小切片的区别无非就是 Len 和 Cap的值比小切片的这两个值大一些,如果发生拷贝,本质上就是拷贝上面的三个字段。
3.切片的深浅拷贝
深浅拷贝都是进行复制,区别在于复制出来的新对象与原来的对象在它们发生改变时,是否会相互影响,本质区别就是复制出来的对象与原对象是否会指向同一个地址。在Go语言,切片拷贝有三种方式:
1.使用=操作符拷贝切片,这种就是浅拷贝
2.使用[:]下标的方式复制切片,这种也是浅拷贝
3.使用Go语言的内置函数copy()进行切片拷贝,这种就是深拷贝
4.零切片 空切片 nil切片是什么
4.1零切片
我们把切片内部数组的元素都是零值或者底层数组的内容就全是 nil的切片叫做零切片,使用make创建的、长度、容量都不为0的切片就是零值切片:
slice := make([]int,5) // 0 0 0 0 0 slice := make([]*int,5) // nil nil nil nil nil
4.2nil切片
nil切片的长度和容量都为0,并且和nil比较的结果为true,采用直接创建切片的方式、new创建切片的方式都可以创建nil切片:
var slice []int var slice = *new([]int)
4.3空切片
空切片的长度和容量也都为0,但是和nil的比较结果为false,因为所有的空切片的数据指针都指向同一个地址 0xc42003bda0;使用字面量、make可以创建空切片:
var slice = []int{} var slice = make([]int, 0)
空切片指向的 zerobase 内存地址是一个神奇的地址,从 Go 语言的源代码中可以看到它的定义:
// base address for all 0-byte allocations var zerobase uintptr // 分配对象内存 func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer { ... if size == 0 { return unsafe.Pointer(&zerobase) } ... }
5.切片的扩容策略
这个问题是一个高频考点,我们通过源码来解析一下切片的扩容策略,切片的扩容都是调用growslice方法,截取部分重要源代码:
1.17之前
代码的扩容策略可以简述为以下三个规则:
1.当期望容量 > 两倍的旧容量时,直接使用期望容量作为新切片的容量
2.如果旧容量
3.如果旧容量 > 1024,那么会进入一个循环,每次增加25%直到大于期望容量
可以看到,原来的go对于切片扩容后的容量判断有一个明显的magic number:1024,在1024之前,增长的系数是2,而1024之后则变为1.25。关于为什么会这么设计,社区的相关讨论1给出了几点理由:
1.如果只选择翻倍的扩容策略,那么对于较大的切片来说,现有的方法可以更好的节省内存。
2.如果只选择每次系数为1.25的扩容策略,那么对于较小的切片来说扩容会很低效。
3.之所以选择一个小于2的系数,在扩容时被释放的内存块会在下一次扩容时更容易被重新利用
// runtime/slice.go // et:表示slice的一个元素;old:表示旧的slice;cap:表示新切片需要的容量; func growslice(et *_type, old slice, cap int) slice { if cap doublecap { newcap = cap } else { // 原 slice 容量小于 1024 的时候,新 slice 容量按2倍扩容 if old.cap maxAlloc newcap = int(capmem) case et.size == sys.PtrSize: lenmem = uintptr(old.len) * sys.PtrSize newlenmem = uintptr(cap) * sys.PtrSize capmem = roundupsize(uintptr(newcap) * sys.PtrSize) overflow = uintptr(newcap) > maxAlloc/sys.PtrSize newcap = int(capmem / sys.PtrSize) case isPowerOfTwo(et.size): var shift uintptr if sys.PtrSize == 8 { // Mask shift for better code generation. shift = uintptr(sys.Ctz64(uint64(et.size))) & 63 } else { shift = uintptr(sys.Ctz32(uint32(et.size))) & 31 } lenmem = uintptr(old.len) (maxAlloc >> shift) newcap = int(capmem >> shift) default: lenmem = uintptr(old.len) * et.size newlenmem = uintptr(cap) * et.size capmem, overflow = math.MulUintptr(et.size, uintptr(newcap)) capmem = roundupsize(capmem) newcap = int(capmem / et.size) } }
1.18之后
到了Go1.18时,又改成不和1024比较了,而是和256比较;并且扩容的增量也有所变化,不再是每次扩容1/4,如下代码所示:
//1.18 newcap := old.cap doublecap := newcap + newcap if cap > doublecap { newcap = cap } else { const threshold = 256 if old.cap在1.18中,优化了切片扩容的策略2,让底层数组大小的增长更加平滑:
通过减小阈值并固定增加一个常数,使得优化后的扩容的系数在阈值前后不再会出现从2到1.25的突变,该commit作者给出了几种原始容量下对应的“扩容系数”:
6. 参数传递切片和切片指针有什么区别
我们都知道切片底层就是一个结构体,里面有三个元素:
分别表示切片底层数据的地址,切片长度,切片容量。
type SliceHeader struct { Data uintptr Len int Cap int }当切片作为参数传递时,其实就是一个结构体的传递,因为Go语言参数传递只有值传递,传递一个切片就会浅拷贝原切片,但因为底层数据的地址没有变,所以在函数内对切片的修改,也将会影响到函数外的切片,举例:
func modifySlice(s []string) { s[0] = "song" s[1] = "Golang" fmt.Println("out slice: ", s) } func main() { s := []string{"asong", "Golang梦工厂"} modifySlice(s) fmt.Println("inner slice: ", s) }// 运行结果
out slice: [song Golang]
inner slice: [song Golang]不过这也有一个特例,先看一个例子:
func appendSlice(s []string) { s = append(s, "快关注!!") fmt.Println("out slice: ", s) } func main() { s := []string{"asong", "Golang梦工厂"} appendSlice(s) fmt.Println("inner slice: ", s) }// 运行结果
out slice: [asong Golang梦工厂 快关注!!]
inner slice: [asong Golang梦工厂]因为切片发生了扩容,函数外的切片指向了一个新的底层数组,所以函数内外不会相互影响,因此可以得出一个结论,当参数直接传递切片时,如果指向底层数组的指针被覆盖或者修改(重分配、append触发扩容),此时函数内部对数据的修改将不再影响到外部的切片,代表长度的len和容量cap也均不会被修改
参数传递切片指针就很容易理解了,如果你想修改切片中元素的值,并且更改切片的容量和底层数组,则应该按指针传递。
7.range遍历切片有什么要注意的
Go语言提供了range关键字用于for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素,有两种使用方式:
for k,v := range _ { } for k := range _ { }第一种是遍历下标和对应值,第二种是只遍历下标,使用range遍历切片时会先拷贝一份,然后在遍历拷贝数据:
s := []int{1, 2} for k, v := range s { } 会被编译器认为是 for_temp := s len_temp := len(for_temp) for index_temp := 0; index_temp不知道这个知识点的情况下很容易踩坑,例如下面这个例子:
package main import ( "fmt" ) type user struct { name string age uint64 } func main() { u := []user{ {"asong",23}, {"song",19}, {"asong2020",18}, } for _,v := range u{ if v.age != 18{ v.age = 20 } } fmt.Println(u) }// 运行结果
[{asong 23} {song 19} {asong2020 18}]因为使用range遍历切片u,变量v是拷贝切片中的数据,修改拷贝数据不会对原切片有影响。
以上就是本文的全部内容了,是否有顺利帮助你解决问题?若是能给你带来学习上的帮助,请大家多多支持golang学习网!更多关于Golang的相关知识,也可关注golang学习网公众号。
-
473 收藏
-
377 收藏
-
384 收藏
-
246 收藏
-
110 收藏
-
210 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 542次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 507次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 497次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 484次学习