Go Slice 详解


使用

新建方法

变量方式

语法

a:=[]T{}

var c []T

T 指的是变量类型

demo

package main

import "fmt"

func main() {
	//声明一个 int 型切片
	a := []int{}
	//声明并初始化一个 int 型切片,长度为4,容量为4
	b := []int{1, 2, 3, 4}
	//声明一个未指定大小的数组来定义切片
	var c []int

	fmt.Println(a)
	fmt.Println(b)
	fmt.Println(c)
}

make

语法

sliceName:=make([]T,lenSlice,CapSlice)
package main

import "fmt"

func main() {
	//声明一个 int 型切片
	a:=make([]int,4,4)
	fmt.Println(a)
}

数组剪切

语法

slice := array[l,r,cap]

slice是一个基于array(或一个切片)的切片,从索引l开始,到索引r-1结束(即r不包含在切片中),并且其容量为cap。(左闭右开

cap 字段可以不填写,即 slice := array[l,r] 此时的 cap=len(slice)

demo

package main

import "fmt"

func main() {
	//声明一个 int 型切片
	A := [5]int{1, 2, 3, 4, 5}
	a := A[1:2:3]
	fmt.Println(a)
}

函数

len() && cap()

demo
--code
a := []int{1, 2, 3, 4}
fmt.Println(len(a))
fmt.Println(cap(a))
--result
4
4

Copy()

demo
func main() {
	a := []int{1, 2, 3, 4, 5}
	b := a
	c := make([]int, len(a))
	copy(c, a)
	fmt.Println(a) //[1 2 3 4 5]
	fmt.Println(b) //[1 2 3 4 5]
	fmt.Println(c) //[1 2 3 4 5]
	b[0] = 1000
	c[0] = 1001
	fmt.Println(a) //[1000 2 3 4 5]
	fmt.Println(b) //[1000 2 3 4 5]
	fmt.Println(c) //[1000 2 3 4 5]
}
[1 2 3 4 5]
[1 2 3 4 5]
[1 2 3 4 5]
[1000 2 3 4 5]
[1000 2 3 4 5]
[1001 2 3 4 5]

可以看到,a 与 b 的修改同步,说明两者共用一个底层数组,c 的修改没有影响其他两个,说明 c 是独立的底层数组

使用copy()内置函数拷贝两个切片时,会将源切片的数据逐个拷贝到目的切片指向的数组中,拷贝数量取两个切片长度的最小值。

例如长度为10的切片拷贝到长度为5的切片时,将会拷贝5个元素。

也就是说,copy过程中不会发生扩容。

append()

slice= append(slice,element1,element2,element3...)
demo
func main(){
	var s []int
	s = append(s, 1)        // [1]
	s = append(s, 2, 3, 4)  // [1 2 3 4]
	s2 := []int{5, 6, 7}  
	s = append(s, s2...)    // [1 2 3 4 5 6 7]
}

其中需要注意的是 s = append(s, s2...) 因为 s2 是一个元素集而不是单个元素,因此需要注意加上...

遍历

切片的遍历方式和数组是一致的,支持索引遍历和for range遍历。

func main() {
	s := []int{1, 3, 5}

	for i := 0; i < len(s); i++ {
		fmt.Println(i, s[i])
	}

	for index, value := range s {
		fmt.Println(index, value)
	}
}

删除元素

go 并没有为 slice 写单独的 delete 函数,而是通过组合切片来进行元素的删除的。

demo

func main() {
	// 从切片中删除元素
	a := []int{30, 31, 32, 33, 34, 35, 36, 37}
	// 要删除索引为2的元素
	a = append(a[:2], a[3:]...)
	fmt.Println(a) //[30 31 33 34 35 36 37]
}

利用左闭右开的特性就对了。

是否为空

结论

仅使用 len(s) == 0 作为判断标准。

验证

运行下列代码

package main

import "fmt"

func main() {
	a := []int{}
	fmt.Println("a:", a)
	fmt.Println("&a:", &a)
	fmt.Println("&a == nil:", &a == nil)
	fmt.Println("a == nil:", a == nil)
	fmt.Println("len(a) == 0:", len(a) == 0)
	var b []int
	fmt.Println("b:", b)
	fmt.Println("&b:", &b)
	fmt.Println("&b == nil:", &b == nil)
	fmt.Println("b == nil:", b == nil)
	fmt.Println("len(b) == 0:", len(b) == 0)

}

结果

a: []
&a: &[]
&a == nil: false
a == nil: false
len(a) == 0: true
b: []
&b: &[]
&b == nil: false
b == nil: true
len(b) == 0: true

a,b同样是空切片,但是两者在测试 == nil 的时候的结果却不相同

解释

切片是依赖于底层数组实现的,因此切片并不是没有地址的,即使是空切片也是指向一个空数组,而一个空数组是有地址的。

b 还不是一个切片,此时他只是一个被声明的结构体罢了,还没有存储数据,因此才会是 nil

记住:每个切片都指向一个底层数组。

跟官方的说法是:

节选自官方源码注释

// p = unsafe.Pointer(u + offset)

// Note that the pointer must point into an allocated object, so it may not be nil.

简要翻译一下就是,unsafe.Pointer 不可以是一个空指针

而 Slice 的结构是

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

因此 slice 不会为 nil

原理

结构

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}
  • array 用于指向一个底层数组
  • len 是目前切片存储的数据长度(个数)
  • cap 是底层数组所能容纳是数据长度(个数)

使用make创建Slice

使用make来创建Slice时,可以同时指定长度和容量,创建时底层会分配一个数组,数组的长度即容量。

例如,语句slice := make([]int, 5, 10)所创建的Slice,结构如下图所示:

Slice 扩容(cap 发挥作用的重点)

基本原理

使用 append 向 Slice 追加元素时,如果 Slice 空间不足,将会触发 Slice 扩容,扩容实际上重新一配一块更大的内存,将原Slice数据拷贝进新 Slice,然后返回新 Slice,扩容后再将数据追加进去。

例如,当向一个 capacity 为5,且 length 也为5的Slice再次追加1个元素时,就会发生扩容,如下图所示:

而这个扩容的操作其实是:

  1. 申请一个更大的底层数组
  2. 把原数据复制到新的数组中
  3. 把要添加的数据放到新数组中。

源码

//version go1.19.3
// growslice handles slice growth during append.
// growslice处理append期间的slice增长。
// It is passed the slice element type, the old slice, and the desired new minimum capacity,and it returns a new slice with at least that capacity, with the old data copied into it.
//它被传递slice元素类型、旧slice和所需的新最小容量,并且它返回一个至少具有该容量的新slice,旧数据被复制到其中。
// The new slice's length is set to the old slice's length,NOT to the new requested capacity.
//新切片的长度被设置为旧切片的长度,而不是新请求的容量。

// This is for codegen convenience. The old slice's length is used immediately
// to calculate where to write new values during an append.
// TODO: When the old backend is gone, reconsider this decision.
// The SSA backend might prefer the new length or to return only ptr/cap and save stack space.
func growslice(et *_type, old slice, cap int) slice {
// ... qian'm
//从这里开始是关键
	newcap := old.cap
	doublecap := newcap + newcap

	if cap > doublecap {
        //原本设计的容量足够容纳新slice
		newcap = cap
	} else {
		const threshold = 256
        //原本容量是否大于 256
		if old.cap < threshold {
			newcap = doublecap
		} else {
			// Check 0 < newcap to detect overflow
			// and prevent an infinite loop.
			for 0 < newcap && newcap < cap {
				// Transition from growing 2x for small slices
				// to growing 1.25x for large slices. This formula
				// gives a smooth-ish transition between the two.
				newcap += (newcap + 3*threshold) / 4
			}
			// Set newcap to the requested cap when
			// the newcap calculation overflowed.
			if newcap <= 0 {
				newcap = cap
			}
		}
	}
// ...
}

扩容规则

注意,每次的扩容规则都会根据版本有细微变化

Go-实战之 Go Slice 的坑

重点问题

此部分仅为个人复习用,请谨慎参考

  1. slice 为空的判定方式,为什么这么用?

    len(slice)==0 因为 nil 不能作为判断标准,详情参见上文。

  2. slice 扩容的方式?

    参见上文

  3. slice 的 cap 应该如何设定?

    尽量预估可能需要的开销,减少扩容造成的性能损失,但也不要占据过多的内存。

  4. 赋值拷贝和 copy 的区别?

    赋值拷贝共享一个底层数组,copy 函数是重新创建了一个底层数组

注意点 OR 编程Tips

  • 创建切片时可跟据实际需要预分配容量,尽量避免追加过程中扩容操作,有利于提升性能;
  • 切片拷贝时需要判断实际拷贝的元素个数
  • 谨慎使用多个切片操作同一个数组,以防读写冲突

参考资料


如果本文帮助到了你,帮我点个广告可以咩(o′┏▽┓`o)


评论
 上一篇
hexo-mermaid hexo-mermaid
啥都没写,别看
2023-07-22
下一篇 
Go-实战之 Go 并发中的坑 Go-实战之 Go 并发中的坑
本文探讨了 Go 并发中的一些常见陷阱。并发是 Go 中强大的特性,可以实现高效的并发任务执行。然而,如果处理不当,它可能会在代码中引入微妙的 bug 和意外行为。
2023-07-19
  目录