<Go语言学习笔记>【数组与切片】

时间:2022-07-27
本文章向大家介绍<Go语言学习笔记>【数组与切片】,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。
*本文为极客时间《Go语言核心36讲》的学习笔记,梳理了索引相关的知识点。

Go 语言的切片类型属于引用类型,同属引用类型的还有字典类型通道类型函数类型等;而 Go 语言的数组类型则属于值类型,同属值类型的有基础数据类型以及结构体类型

两者区别

简单的说,数组类型的长度是固定的,而切片类型是可变长的。数组的容量永远等于其长度,都是不可变的。

切片

切片的结构:

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

切片的结构体由3部分构成,Pointer 是指向一个数组的指针,len 代表当前切片的长度,cap 是当前切片的容量。cap 总是大于等于 len 的,可以参考下面这段源码。

func makeslice(et *_type, len, cap int) unsafe.Pointer {
	mem, overflow := math.MulUintptr(et.size, uintptr(cap))
	if overflow || mem > maxAlloc || len < 0 || len > cap {
		// NOTE: Produce a 'len out of range' error instead of a
		// 'cap out of range' error when someone does make([]T, bignumber).
		// 'cap out of range' is true too, but since the cap is only being
		// supplied implicitly, saying len is clearer.
		// See golang.org/issue/4085.
		mem, overflow := math.MulUintptr(et.size, uintptr(len))
		if overflow || mem > maxAlloc || len < 0 {
			panicmakeslicelen()
		}
		panicmakeslicecap()
	}

	return mallocgc(mem, et, true)
}

切片的扩容和缩容

切片的扩容

假设原有切片的容量是A,当切片需要扩容的时候:

第一,会先判定需要扩容的容量X,如果X大于两倍的A,那么直接扩容超过X。也就是说新的切片的容量将会是大于等于X。

第二,如果X小等于两倍的A,此时需要进行再一次的判断,旧容量A如果小于1024,直接扩容两倍的A,此时新的切片容量是2A(理论上)。

第三,如果X小等于两倍的A,且旧容量A大于1024,会循环扩容1.25倍,直到大于X为止。此时的新切片的容量是(n 1.25 A)

以上的计算是理论上的值,实际上的值可能会稍微大一些。

具体代码:/runtime/slice.go 中的 growslice方法。

newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
	newcap = cap
} else {
	if old.len < 1024 {
		newcap = doublecap
	} else {
		// Check 0 < newcap to detect overflow
		// and prevent an infinite loop.
		for 0 < newcap && newcap < cap {
			newcap += newcap / 4 //等价于 newcap = 1.25*newcap
		}
		// Set newcap to the requested cap when
		// the newcap calculation overflowed.
		if newcap <= 0 {
			newcap = cap
		}
	}
}

切片的缩容

Go 本身没有切片缩容后,底层数组不会被释放掉。缩容次数多了,会占用很多内存。

可以用copy的方法,创建新的切片和底层数组。并把原来的切片置nil。

切片的底层数组什么时候会替换

准确的说,一个切片不存在底层数组被替换的情况。当一个切片容量不够时,会给他创建一个新的切片,这个切片有自己的底层数组,自己的结构,自己的内存地址。

我们看到某个切片变量被扩容了,实际上是这个变量内容发生了变化。具体而言,该变量的内存地址不变,但是地址里的东西发生了变化。

举一个形象的例子:假设第一次拜访黄河南路1号别墅的时候,这里住了一家三口人。后来发生了一些变化(对应扩容了),我们再一次去拜访这个地址,发现地址没有变化,房子也没有变化,但是变成一家五口人(其中三口人你认识,另外两人你不认识)但这一家人已经不是以前那一家人了。(某些科幻片的设定)

真正会导致底层数据发生变化的只有扩容的时候。因为数组不能被扩容这个缘故,需要重新创建一个新的底层数组,并创建一个新的切片信息。缩容并不会。

a := []int{1,2,3,4,5,6,}
println(a)
//a = a[:3] //裁剪三个
//println(a) //地址不会变化,容量不会变化,长度会变化
a = append(a,make([]int, 5)...) //增加5个元素
println(a) //地址 容量,长度 都会变

一些拓展问题

关于append的之后,切片的具体变化情况。

如果append时,引发了切片扩容,那么新的切片内容会发生变化,包括底层数组,长度。如果没有触发扩容,那么只有长度会发生变化。具体可以看代码:

s6 := make([]int, 0)
//s6 := make([]int, 1,10)
println(s6)
fmt.Printf("The capacity of s6: %dn", cap(s6))
for i := 1; i <= 5; i++ {
	s6 = append(s6, i)
	println(s6) //一旦触发扩容,地址信息会变
	fmt.Printf("s6(%d): len: %d, cap: %dn", i, len(s6), cap(s6)) //长度在稳定增加,但是容量会跳着增加
}
fmt.Println()

range 循环时切片的具体变化

切片在range 循环时,value的赋值一样是值传递,本身的地址不变,内容会变。并且循环内对value 的修改,不会影响原来的切片内容。

var b = [6]int{1,2,3,4,5,6,}
for key, value := range b {
	fmt.Printf("value的值:%d,value的地址:%x,切片该元素的地址:%xn", value, &value, &b[key])
	//Value的值不会变,value 的地址不变
	//println(reflect.TypeOf(value))
	value += 1 //验证下会不会改变原
}
fmt.Printf("value的值:%d",b) //值没有加1 

空切片 与nil切片

var a []int //nil切片,只定义了类型,slice.array内容指向nil。
println(a) //[0/0]0x0

b := make( []int , 0 ) //空切片,有类型,有地址
b := []int{ }
println(b) // [0/0]0xc00003e778
  1. nil切片被用在很多标准库和内置函数中,返回一个不存在的切片。
  2. 空切片是一个定义好的,分配好内存的切片,只是切片里面没有任何元素。

make 和 new 的区别

Make 是专门用来创建 slicemapchannel 的值的。它返回的是被创建的,并且立即可用。

New 是申请一小块内存并标记它是用来存放某个值的。它返回的是指向这块内存的指针,而且这块内存并不会被初始化。或者说,对于一个引用类型的值,那块内存虽然已经有了,但还没法用(因为里面没有针对那个值的数据结构)。

所以,对于引用类型的值,不要用 new,能用 make 就用 make,不能用 make 就用复合字面量来创建。