Go语言(基础)——切片

1、基本介绍

  • Go 语言中的数组长度不可变,这在使用的过程中带来了不便,为了弥补这个缺陷,Go 语言提供了一种新的数据类型——切片,有人也把它称为动态数组,切片的长度是可变的,在往切片中添加元素时,可能会因为原有切片容量不足,而重新开辟一块空间存储数据。

2、切片的数据结构

  • 在 reflect 包中,我们可以看到 SliceHeader(切片头)数据结构,它就代表了切片的数据结构,在下面的代码中,Len 代表切片中目前存储的元素数目,Cap 代表切片底层数组的长度。每次我们往切片中添加元素时,切片的 Cap 如果尚且大于 Len 的话,则还在原内存空间中扩展,但是如果切片的 Cap 小于 Len 的话,则会重新在内存中开辟出一块空间供切片存储数据使用,切片扩容稍后介绍。
type SliceHeader struct {
	Data uintptr
	Len  int
	Cap  int
}

3、切片的声明与初始化

  • 切片的声明方式是:var slice []T,其中 T 是切片元素的类型,与数组不同的是,切片在声明的时候不要在方括号 [] 中写入切片的长度,如果在方括号 [] 中写入数字,则就不是切片,而是数组了。切片还可以基于现有的切片或数组生成,方式是 slice := array[n:m],其中 array 可能是切片,也可能是数组,并且 {n >= 0 && n < m && m <= len(array)}。
//方式一:声明和初始化两个步骤分开
var slice1 []byte
slice1 = make([]byte, 2, 4)
//方式二:声明和初始化两个步骤放一起
slice2 := []byte{'a', 'b', 'c', 'd'}
//方式三:基于现有的数组或切片创建切片
slice3 := slice2[:2] //slice3 -> {'a', 'b'}

4、对切片进行切片以及切片扩容的研究

4.1、对切片进行切片
  • 切片操作并不会直接复制原本切片指向的数据,它创建一个新的切片,然后这个切片复用原来切片的底层数组。因此在新切片没有扩容的前提下,修改新切片里的内容,也会对原有切片对应位置的元素进行修改。具体看下面代码:
var a = []int{1, 2, 3, 4}
b := a[:2] //b->{1, 2}
b[0] = 10 //b->{10, 2} and a->{10, 2, 3, 4}
//注意这里 a 的 Len 和 Cap 都是4,b 的 Len 和 Cap 分别是 2 和 4
//前面说过 Cap 代表切片底层数组的长度,从这里 b 的 Cap 和 a 的 Cap 一样,
//可以反映出新切片复用原来切片的底层数组
  • 但是假如我对新切片进行扩容了呢?注意,只是增加了新切片的 Len,而 Cap 没有增加,实际上不算是扩容。看下面代码,真正的扩容在下一节讲解。
var a = []int{1, 2, 3, 4}
b := a[:2] //b->{1, 2}
b = append(b, []int{5, 6}...) //b->{1, 2, 5, 6} and a->{1, 2, 5, 6}
//尽管 b 的 Len 变成了 4,但是 Cap 依然是 4,没有增加,所以没有扩容,
//也就是说 b 没有开辟新的空间去存储底层数组,那么这时对 b 进行修改,
//a 的底层数组对应位置数据也就被修改了
4.1.1、通过指针偏移获取元素
  • 虽然 Go 语言为了安全,对指针做了诸多限制,比如在 C 语言中可对指针进行运算,从而获取到其他内存位置存储的数据(这么做很危险,可能会破坏系统变量),但是在 Go 语言中,禁止了对指针进行运算这一操作。然而我们还是通过一些手段实现对指针的运算操作,只是步骤稍微麻烦了一些。大致可分为六个步骤,看下面代码。
//通过切片的方式,获取切片 b,然后在已知 b[1]的地址的前提下,
//通过对 b[1] 的地址进行“运算”,获取 a[2] 的值
var a []int= []int{1, 2, 3, 4}
var b []int= a[:2]
//第一步:获取 b[1] 的 Pointer 值(地址值)
//type Pointer *ArbitraryType | type ArbitraryType int
//所以 Pointer 本质上就是 *int
unsafe_b1_ptr := unsafe.Pointer(&b[1]) //unsafe_b1_ptr->0xc000010368
//第二步:由于不能对 *int 进行计算,将 unsafe_b1_ptr 转换成 uintptr 类型的变量
uintptr_b1_ptr := uintptr(unsafe_b1_ptr)
//第三步:获取本计算机下 int 数据类型占用内存大小,实际上是为了求偏移量,等会计算地址用
size := unsafe.Sizeof(int(1))
//第四步:对地址值 uitptr_b1_ptr 进行运算
uintptr_a2_ptr := uintptr_b1_ptr + size
//第五步:将 uintptr 类型的地址值 uintptr_a2_ptr 转换为 Pointer 类型
unsafe_a2_ptr := unsafe.Pointer(uintptr_a2_ptr)
//第六步:获取最终地址的值
var a2_ptr *int = (*int)(unsafe_a2_ptr)
fmt.Println(*a2_ptr) //输出 3
4.2、对切片进行扩容
  • Go 语言中的切片扩容机制,与切片的数据类型、原本切片的容量、所需要的容量都有关系,比较复杂。对于常见数据类型,在元素数量较少时,大致可以认为扩容是按照翻倍进行的,下面是 runtime 包里 slice.go 切片扩容的部分底层源码:
func growslice(et *_type, old slice, cap int) slice {
	//省略部分代码
	newcap := old.cap
	doublecap := newcap + newcap
	if cap > doublecap {
		newcap = cap
	} else {
		if old.len < 1024 { //当原本切片长度小于 1024 时,扩容时容量直接翻倍
			newcap = doublecap
		} else { //否则当原本切片长度大于 1024 时,容量会在不溢出且小于扩容目标情况下每次增加 25%
			// Check 0 < newcap to detect overflow 检查不会溢出
			// and prevent an infinite loop. 防止无限循环
			for 0 < newcap && newcap < cap {
				newcap += newcap / 4 //每次增加25%
			}
			// Set newcap to the requested cap when
			// the newcap calculation overflowed.
			if newcap <= 0 {
				newcap = cap
			}
		}
	}
	//省略部分代码
}
  • 切片扩容会开辟一块新的空间存储切片。
var a []int= []int{1, 2, 3, 4} //a.len=4, a.cap=4, &a=0xc0000044a0
fmt.Printf("cap:%d, len:%d, addr:%p\n", cap(a), len(a), &a)
a = append(a, []int{0, 4, 3, 2, 1}...) //a.len=9, a.cap=10, &a=0xc000096440
fmt.Printf("cap:%d, len:%d, addr:%p\n", cap(a), len(a), &a)
4.2.1、对切片进行扩容的一点建议
  • 前面我们说过,只有当 Cap 增加时,切片才算发生了扩容,但是在写代码时,我们有时并不知道切片有没有扩容,比如我们通过切片的方式得到的一个新切片,我们要在新切片上添加数据,如果往新切片中添加数据,并没有发生扩容,那么就会修改原本切片中存储的数据,为了避免这种意想不到的事情发生,对切片进行扩容时,建议使用 copy 方式,得到一个重新分配内存空间的切片后,再进行扩容。
var srcSlice = []int{1, 2, 3, 4}
var dstSlice = make([]int, 4, 8) //注意:这里设置的 len 必须大于或者等于srcSlice 的长度才能完整复制 srcSlice 到 dstSlice
res := copy(dstSlice, srcSlice)
4.2.3、copy 切片时的一点提示
  • 如果源切片的长度 length1 大于目的切片的长度 length2,则只会复制源切片前 length2 长度的内容到目的切片中,目的切片不会进行扩容。
srcSlice := []int{7, 8, 9, 0, 1}
dstSlice := make([]int, 2, 4)
copy(dstSlice, srcSlice) //dstSlice->[7 8]
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wuxy_Fansj_Forever

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值