golang中数组与切片的Q&A

数组与切片 的 Q&A

切片作为函数参数

type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}
  1. slice其实是一个结构体,包含三个成员,len、map、array,分别表示长度、容量和底层数组的地址
  2. 当slice作为函数参数时,就是一个普通的结构体,其实很好理解,若直接传slice,在调用者看来,实参slice并不会
    被函数中的操作所改变,若传的是slice的指针,在调用者看来,是会被改变原slice的
  3. 值得注意的是,不管传递的是slice还是slice指针,如果改变了slice底层数组的数据,会反映到实参slice的底层数据
    为什么能改变底层数组的数据?
    很好理解,底层数组数据在结构体里是一个指针unsafe.Pointer类型,尽管slice传递是复制,但是它们指向底层数组的指针是一样的
  4. 通过slice的array字段就可以拿到数组的地址,在代码里直接通过s[i] = 10这种操作改变slice底层数组元素的值
  5. 另外值得注意的是,go语言的函数参数传递,只有值传递,没有引用传递
func main() {
	s := []int{1, 1, 1}
	fmt.Println(s)

	f(s)
	fmt.Println(s)
}

func f(s []int) {
	// range会拷贝一份(copy方法),不会对原切片造成影响
	//for _, v := range s{
	//	v++
	//}

	for i, _ := range s{
		s[i] += 1  // 修改的是底层数组的值
	}
}
  1. 果真改变了原始slice的底层数据,这里传递的是一个slice副本,在f函数中,s只是main函数中s的一个拷贝,在f函数内部对s的作用
    并不会改变外层main函数中的s
  2. 要想真的改变外层slice,只有将返回新的slice赋值到原始slice,或者向函数传递一个指向slice的指针,下面的例子
func myAppend(s []int) []int {
	// append方法导致了切片的扩容,底层数组复制,所以不会影响到外部切片
	s = append(s, 100)
	return s
}

func myAppendPtr(s *[]int) {
	// append方法导致了切片的扩容,底层数组复制,但是扩容也好,复制也巴都是针对外部切片的,因为传递进来的是指针
	*s = append(*s, 1000)
}

func main() {
	s := []int{1, 2, 3}
	newS := myAppend(s)
	fmt.Println(s)
	fmt.Println(newS)

	s = newS
	fmt.Println(s)

	myAppendPtr(&s)
	fmt.Println(s)

}
  1. myAppend函数里虽然改变了s,但它只是一个值传递,并不会影响最外层的s,因此第一行打印[1 2 3]
    而newS是一个新的slice,是基于s得到的,因此它打印的是追加了100之后的结果,[1,2,3,100]
    最后newS赋值给了s,s这是才真正变成了一个新的slice,在给myAppendPtr传入一个s的指针,这回它真的改变了[1,2,3,100,1000]

切片的容量是怎样增长的?

  1. 一般都是向slice追加了元素,才会引起扩容,追加元素调用的是append函数,先来看看append函数的原型
    func append(slice []Type, elems ...Type) []Type
  2. append函数的参数长度可变,因此可以追加多个值到slice中,也可以追加一个切片...
slice = append(slice, elem1, elem2)
slice = append(slice, anotherSlice...)
func main() {
	s1 := []int{1, 2, 3}
	// 数组的指针就是数组中第一个元素的指针
	// 切片的地址  切片的值  切片指向底层数组的地址 == 切片指向底层数组第一个元素的地址
	fmt.Printf("%p:%v:%p:%p\n", &s1, s1, s1, &(s1[0]))

	// 切片扩容,底层数组复制,切片的地址不变,但是底层数组的地址改变了
	s1 = append(s1, []int{4, 5}...)

	fmt.Printf("%p:%v:%p:%p\n", &s1, s1, s1, &(s1[0]))
}

输出结果

0xc000004078:[1 2 3]:0xc000010180:0xc000010180
0xc000004078:[1 2 3 4 5]:0xc00000c360:0xc00000c360
  1. append()函数的返回值是一个新的slice, go编译器不允许调用了append函数后不使用返回值
append(slice, elem1, elem2)
append(slice, anotherSlice...)

所以上面这种写法是错的,编译器不能通过
4. 使用append可以向slice添加元素,实际上是向底层数组添加元素,但是底层数组的长度是固定的,如果索引
len-1所指向的元素已经是底层数组的最后一个元素,就没办法添加了
5. 切片扩容的规则
1. 当原slice的容量小于1024的时候,新slice的容量是原slice容量的2倍
2. 当原slice的容量大于1024的时候,新slice的容量是原slice容量的1.25倍
6. 其实上面说法是有误的,源码中
func growslice(et *_type, old slice, cap int) slice {
源码上半部分确实如上所说进行扩容,但是后面会进行内存对齐,这个和内存分配策略相关,内存对齐后,
新slice的容量要大于等于就slice容量的2倍或1.25倍
7. 经典例子1

func main() {
	s := []int{5}  // s长度1,容量1
	s = append(s, 7)  // s长度2,容量2
	s = append(s, 9)  // s长度3, 容量4
	x := append(s, 11)  // x长度4,容量4,s长度3,容量4  此处没有发生扩容
	y := append(s, 12)  // y长度4,容量4,s长度3,容量4  此处也没有发生扩容

	fmt.Println(len(s), cap(s))  // 3 4
	fmt.Println(len(x), cap(x))  // 4 4
	fmt.Println(len(y), cap(y))  // 4 4
	fmt.Println(s, x, y)  // [5 7 9] [5 7 9 12] [5 7 9 12]
} 

上面案例代码 切片对应状态
s := []int{5} s 只有一个元素,[5]
s = append(s, 7) s 扩容,容量变为2,[5, 7]
s = append(s, 9) s 扩容,容量变为4,[5, 7, 9]。注意,这时 s 长度是3,只有3个元素
x := append(s, 11) 由于 s 的底层数组仍然有空间,因此并不会扩容。这样,底层数组就变成了 [5, 7, 9, 11]。注意,此时 s = [5, 7, 9],容量为4;x = [5, 7, 9, 11],容量为4。这里 s 不变
y := append(s, 12) 这里还是在 s 元素的尾部追加元素,由于 s 的长度为3,容量为4,所以直接在底层数组索引为3的地方填上12。结果:s = [5, 7, 9],y = [5, 7, 9, 12],x = [5, 7, 9, 12],x,y 的长度均为4,容量也均为4
8. 经典例子2
在看一个例子,跟上面的有一点不太一样

func main() {
	s := []int{5}  // s长度1,容量1
	s = append(s, 7)  // s长度2,容量2
	s = append(s, 9)  // s长度3, 容量4
	x := append(s, 11)  // x长度4,容量4,s长度3,容量4
	y := append(x, 12)  // y长度5,容量8,s长度3,容量4

	fmt.Println(len(s), cap(s))  // 3 4
	fmt.Println(len(x), cap(x))  // 4 4
	fmt.Println(len(y), cap(y))  // 5 8
	fmt.Println(s, x, y)  // [5 7 9] [5 7 9 11] [5 7 9 11 12]
}
  • 总结:这里需要注意的是,append函数返回的是全新的slice, 并且对传入的slice不会有影响
  1. 再看一个例子:
func main() {
	s := []int{11, 22}
	s = append(s, 33, 44, 55)
	fmt.Println(s, len(s), cap(s))  // [11 22 33 44 55] 5 6
} 

看一段源码

// go 1.9.5 src/runtime/slice.go:82
func growslice(et *_type, old slice, cap int) slice {
    // ……
    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
        newcap = cap
    } else {
        // ……
    }
    // ……
    capmem = roundupsize(uintptr(newcap) * ptrSize)
    newcap = int(capmem / ptrSize)
}

我们可以看到,如果求的的新的容量cap>2倍的旧容量,那么newcap直接等于新求的容量cap
由于后面有内存对齐相关的源码,最终容量变成了6,:
参考文档: https://www.bookstack.cn/read/qcrao-Go-Questions/数组和切片-切片的容量是怎样增长的.md
10. 向一个nil的slice添加元素会发生什么,为什么?
其实nil slice或者empty slice都可以通过append函数实现底层数组的扩容,最终都是调用mallocgc向go的内存管理器申请一块内存,
然后赋值给原来的nil slice或emtpy slice,然后摇身一变,变成了真正的"slice"了

数组和切片有什么异同

  1. slice的底层数据是数组,slice是对底层数组的封装,它描述一个数组的片段,两者都可以通过下标访问单个元素
  2. 数组是定长的,长度定义好之后,不能在更改,在go中,数组是不常见的,因为它的长度是类型的一部分,[3]int和[4]int是不同的类型
  3. 而切片非常的灵活,他可以动态扩容,而且它的类型和长度无关
  4. 数组就是一片连续的内存,slice实际上是一个结构体,包含三个字段array数组指针,len长度,cap容量
// runtime/slice.go
type slice struct {
    array unsafe.Pointer // 元素指针
    len   int // 长度 
    cap   int // 容量
} 
  • 一定要知道,数组是一块连续的内存
  1. 注意, 底层数组可以被多个slice同时指向的,因此对一个slice的元素进行操作,有可能影响到其它slice
    【引申1】[3]int和[4]int是同一个类型吗
    答:不是,因为数组的长度也是类型的一部分,这一点跟切片不太一样
  2. 对切片进行[:],和append元素的经典案例:
func main() {
	slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
	s1 := slice[2:5]  // s1=[2, 3, 4] len=3 cap=8
	s2 := s1[2:6:7]  // s2=[4, 5, 6, 7] len=4 cap=5
	// 下面代码会改变底层数组的值:8->100,因为s1和slice也引用的底层数组,所以它俩都会受到影响
	// 此时slice = [0, 1, 2, 3, 4, 5, 6, 7, 100, 9], 由于s1的长度为3,所以它虽然底层数组收到了影响,但是它没变化
	s2 = append(s2, 100)  // s2=[4, 5, 6, 7, 100] len=5 cap=5
	// s2发生了扩容,底层数组复制,增加的200元素不会对s1和slice的底层数组产生影响
	s2 = append(s2, 200)  // s2=[4, 5, 6, 7, 100, 200] len=6 cap=10
	s1[2] = 20  // s1=[2, 3, 20] len=3 cap=8, slice的4->20  slice=[0, 1, 2, 3, 20, 5, 6, 7, 100, 9]
	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]
}
posted @ 2022-03-14 14:48  专职  阅读(71)  评论(0编辑  收藏  举报