Go最全Golang 中的 slice 详解_golang slice,2024年最新一文带你搞懂Golang多线程Handler

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

在这里插入图片描述

func main() {
	// 数组的地址可以通过数组名来获取
	// 数组的第一个元素的地址就是数组的地址
	// 数组的各个元素的间隔是依据数组的类型决定的,比如int64间隔8个,int32间隔4个
	// 输出的地址是16进制
	intArr := [3]int{1, 2, 3}
	fmt.Printf("intArr的地址=%p\n", &intArr)
	fmt.Printf("intArr[0]地址=%p\n", &intArr[0])
	fmt.Printf("intArr[1]地址=%p\n", &intArr[1])
	fmt.Printf("intArr[2]地址=%p\n", &intArr[2])
}

在这里插入图片描述

二维数组

在这里插入图片描述

那么二维数组在内存的空间地址是连续的么?

func twoArray() {
	intArr := [2][3]int{{1, 2, 3}, {4, 5, 6}}
	fmt.Printf("intArr的地址=%p\n", &intArr)
	fmt.Printf("intArr[0]地址=%p\n", &intArr[0])
	fmt.Printf("intArr[0][0]地址=%p\n", &intArr[0][0])
	fmt.Printf("intArr[0][1]地址=%p\n", &intArr[0][1])
	fmt.Printf("intArr[0][2]地址=%p\n", &intArr[0][2])
	fmt.Printf("intArr[1]地址=%p\n", &intArr[1])
	fmt.Printf("intArr[1][0]地址=%p\n", &intArr[1][0])
	fmt.Printf("intArr[1][1]地址=%p\n", &intArr[1][1])
	fmt.Printf("intArr[1][2]地址=%p\n", &intArr[1][2])
}

在这里插入图片描述

注意地址为16进制,可以看出二维数组在内存的空间地址是连续的。

简单介绍一下内存地址, 0xc0000c8030 与 0xc0000c8038 相差 8,就是 8 个字节,因为这是一个 int 型的数组(64位系统默认是 int64),所以两个相邻数组元素地址差 8 个字节。0xc0000c8038 与 0xc0000c8040 也是差了 8 个字节,在16进制里 8 + 8 = 0 ,0 就是16。

在这里插入图片描述

2、go 中的切片(slice)

Go语言中,数组在传递的时候,传递的是原数组的拷贝,对大数组来说,内存代价会非常大,影响性能。

传递数组指针可以解决这个问题,但是数组指针也有一个弊端:原数组的指针指向改变了,那函数里面的指针指向也会跟着改变,某些情况下,可能会产生意想不到的bug。slice的出现,便是为了解决这个问题。

在这里插入图片描述
  上图中,ptr 是指向底层数组的指针,len 是指slice 的长度,cap 是指 slice 的容量。

slice 是对数组的封装,是对数组中一个连续片段的引用,是一个引用类型。slice 本身并不是动态数组或者数组指针,它的内部实现是通过指针引用底层数组,设置相关的属性,将数据的读写操作限定在指定的区域内,修改的是底层数组,而不是slice本身。

3、数组和切片的区别

  • 相同点:

(1)都是只能存储一组相同类型的数据结构;
(2)下标都是从0开始的,可以通过下标来访问单个元素;
(3)有容量、长度,长度通过 len 获取,容量通过 cap 获取。

  • 不同点:

(1)数组是定长的,长度定义好之后,不能再更改,长度是类型的一部分,访问和复制不能超过数组定义的长度,否则就会下标越界。切片长度和容量可以自动扩容,切片的类型和长度无关。

在 Go 中,数组是不常见的,因为其长度是类型的一部分,限制了它的表达能力,比如 [3]int 和 [4]int 就是不同的类型。

(2)数组是值类型,切片是引用类型,每个切片都引用了一个底层数组,切片本身不能存储任何数据,都是底层数组存储数据,修改切片的时候修改的是底层数组中的数据,切片一旦扩容,会指向一个新的底层数组,内存地址也就随之改变。

二、切片(slice)的底层实现

源码位于 src\runtime\slice.go 中。
  golang 中 slice 实际上是一个结构体,包含三个字段:长度、容量、底层数组。

type slice struct {
	array unsafe.Pointer // 指向底层数组的指针
	len   int // 长度 
	cap   int // 容量
}

注意,底层数组是可以被多个 slice 同时指向的,因此对一个 slice 的元素进行操作是有可能影响到其他 slice 的。

看个经典的例子:

package main
import "fmt"
func main() {
	slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
	s1 := slice[2:5]
	fmt.Println(s1) // [2 3 4]
	s2 := s1[2:6:7] // 长度 2:6,容量 2:7
	fmt.Println(s2) // [4 5 6 7]
	s2 = append(s2, 100)
	s2 = append(s2, 200)
	s1[2] = 20
	fmt.Println(s1)    // [2 3 20]
	fmt.Println(s2)    // [4 5 6 7 100 200]
	fmt.Println(slice) // [0 1 2 3 20 5 6 7 100 9]
}

s1 从 slice 索引2(闭区间)到索引5(开区间,元素真正取到索引4),长度为3,容量默认到数组结尾,为8。s2 从 s1 的索引2(闭区间)到索引6(开区间,元素真正取到索引5),容量到索引7(开区间,真正到索引6),为5。

在这里插入图片描述

接着,向 s2 尾部追加一个元素 100,s2 容量刚好够,直接追加。不过,这会修改原始数组对应位置的元素。这一改动,数组和 s1 都可以看得到。

在这里插入图片描述

再次向 s2 追加元素200,这时,s2 的容量不够用,该扩容了。于是,s2 另起炉灶,将原来的元素复制新的位置,扩大自己的容量。并且为了应对未来可能的 append 带来的再一次扩容,s2 会在此次扩容的时候多留一些 buffer,将新的容量将扩大为原始容量的2倍,也就是10了。这一改动,不会影响数组和 s1 。

在这里插入图片描述

最后,修改 s1 索引为2位置的元素,这次只会影响原始数组相应位置的元素,它影响不到 s2 了,人家已经远走高飞了。

在这里插入图片描述
  再提一点,打印 s1 的时候,只会打印出 s1 长度以内的元素。所以,只会打印出3个元素,虽然它的底层数组不止3个元素。

当 slice 作为函数参数时,是值传递,函数内部对 slice 的作用并不会改变外层的 slice ,要想真的改变外层 slice,只有将返回的新的 slice 赋值到原始 slice,或者向函数传递一个指向 slice 的指针。slice 结构体自身不会被改变,指针指向的底层数组的地址也不会被改变,改变的是数组中的数据。

package main
func main() {
    s := []int{1, 1, 1}
    f(s)
    fmt.Println(s) // [2 2 2]
}
func f(s []int) {
    // i只是一个副本,不能改变s中元素的值
    /\*for \_, i := range s {
 i++
 }
 \*/
    for i := range s {
        s[i] += 1
    }
}

三、切片(slice)的扩容

slice可以理解为动态数组,既然是动态数组,那必然需要进行扩容。

1、触发扩容的时机

向 slice 追加元素,如果底层数组的容量不够(即便底层数组并未填满),就会触发扩容。追加元素调用的是 append 函数。

2、扩容规则

Go <= 1.17

1、首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)。

2、否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的 2 倍

3、否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)就是旧容量(old.cap)按照 1.25 倍循环递增,也就是每次加上 cap / 4。

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

…(img-dSox0UgG-1715504084205)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

  • 17
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值