已复制
全屏展示
复制代码

Go语言之slice总结


· 6 min read

一. slice的结构

slice 翻译成中文就是切片,它和数组(array)很类似,可以用下标的方式进行访问,如果越界,就会产生 panic。但是它比数组更灵活,可以自动地进行扩容。

// runtime/slice.go
type slice struct {
	array unsafe.Pointer // 元素指针
	len   int            // 长度 
	cap   int            // 容量
}

slice 共有三个属性:

  • 指针,指向底层数组;
  • 长度,表示切片可用元素的个数,也就是说使用下标对 slice 的元素进行访问时,下标不能超过 slice 的长度;
  • 容量,底层数组的元素个数,容量 >= 长度。在底层数组不进行扩容的情况下,容量也是 slice 可以扩张的最大限度。

slice 和数组的区别:

  • slice 的底层数据是数组,slice 是对数组的封装,它描述一个数组的片段。两者都可以通过下标来访问单个元素。
  • 数组是定长的,不能再次更改。在 Go 中,其长度是类型的一部分,比如 [3]int 和 [4]int 是不同的类型。
  • 而切片则非常灵活,它可以动态地扩容。切片的类型和长度无关。

不可见的slice元素:

s1 := [5]int{100, 200, 300, 400, 500}
s2 := s1[0:1]
fmt.Println(s2, len(s2), cap(s2))
fmt.Println(s2[:cap(s2)])

[100] 1 5
[100 200 300 400 500]

二. slice的创建

2.1 直接声明

// 如下两种方式等同:声明 nil 切片
var s1 []int
var s2 = *new([]int)
fmt.Println(s1 == nil)
fmt.Println(s2 == nil)

true
true


// 如下两种方式等同:声明 empty 切片
var s3 = []int{}
var s4 = make([]int, 0)
fmt.Println(s3 == nil)
fmt.Println(s4 == nil)

false
false
  • 直接声明创建出来的 slice 其实是一个 nil slice。它的长度和容量都为0。和nil比较的结果为true。
  • empty slice,它的长度和容量也都为0,但是它的数据指针都指向同一个地址。空切片和 nil 比较的结果为false。

2.2 初始化表达式创建

s1 := []int{0, 1, 2, 3, 8: 100}
fmt.Println(s1, len(s1), cap(s1))

[0 1 2 3 0 0 0 0 100] 9 9

唯一值得注意的是上面的代码例子中使用了索引号,直接赋值,这样,其他未注明的元素则为默认值(int为0,string为"")。

2.3 make创建

make函数需要传入三个参数:切片类型,长度,容量。当然,容量可以不传,默认和长度相等。

slice := make([]int, 5, 10) // 长度为5,容量为10
slice[2] = 2                // 索引为2的元素赋值为2
fmt.Println(slice)

[0 0 2 0 0]

2.4 截取创建

  • 可以从数组或者 slice 直接截取,当然需要指定起止索引位置。
  • 基于已有 slice 创建新 slice 对象,被称为 reslice。新 slice 和老 slice 共用底层数组,新老 slice 对底层数组的更改都会影响到彼此。
  • 基于数组创建的新 slice 对象也是同样的效果:对数组或 slice 元素作的更改都会影响到彼此。
  • 互相影响的前提是两者共用底层数组,如果因为执行 append 操作使得新 slice 底层数组扩容,移动到了新的位置,两者就不会相互影响了。所以,问题的关键在于两者是否会共用底层数组。
slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := slice[2:5] // [2, 3, 4] 长度为3,容量为8(容量默认到数组结尾)
fmt.Println(s1, len(s1), cap(s1))

三. slice的append原理

append可以在slice后面追加元素,可以一次追加一个或者多个。每次append,都会检查容底层数组容量是否足够,不够的话,就会生成一个新的数组,生成新的数组以后就和以前的底层数组脱落关系了。

s1 := []int{100, 200, 300}
s1 = append(s1, 400)
s1 = append(s1, 500, 600)
fmt.Println(s1)
  • 使用 append 可以向 slice 追加元素,实际上是往底层数组添加元素。当添加满后就没法再添加了。
  • 这时,slice 会迁移到新的内存位置,并且新底层数组的长度也会增加,否则,每次添加元素的时候,都会发生迁移,成本太高。
  • 当原 slice 容量小于 1024 的时候,新 slice 容量变成原来的 2 倍;原 slice 容量超过 1024,新 slice 的容量是要 大于等于 老 slice 容量的 2倍或者1.25倍

四. slice的拷贝

  • 拷贝时先创建一个相同长度的slice,然后进行拷贝
array := []int{10, 20, 30, 40}
slice := make([]int, len(array))
n := copy(slice, array)

fmt.Println(n, slice)
fmt.Printf("%p, %p", &array[0], &slice[0])

4 [10 20 30 40]
0xc0000b6020, 0xc0000b6040
  • range遍历slice时,拿到的 Value 其实是切片里面的值拷贝。所以每次打印 Value 的地址都不变,由于 Value 是值拷贝的,并非引用传递,所以直接改 Value 是达不到更改原切片值的目的的,需要通过 &slice[index] 获取真实的地址。
slice := []int{10, 20, 30, 40}
for index, value := range slice {
    fmt.Printf("%d, %x, %x\n", value, &value, &slice[index])
}

五. 函数传参

  • Go 中数组赋值和函数传参都是值复制的。那这会导致什么问题呢?假想每次传参都用数组,那么每次数组都要被复制一遍。
func main() {
    arrayA := [2]int{100, 200}
    var arrayB [2]int
    arrayB = arrayA
    fmt.Printf("arrayA : %p , %v\n", &arrayA, arrayA)
    fmt.Printf("arrayB : %p , %v\n", &arrayB, arrayB)
    
    testArray(arrayA)
}

func testArray(x [2]int) {
    fmt.Printf("func Array : %p , %v\n", &x, x)
}

arrayA : 0xc00001c0b0 , [100 200]
arrayB : 0xc00001c0c0 , [100 200]
func Array : 0xc00001c100 , [100 200]
func main() {
    arrayA := []int{100, 200}
    testArrayPoint(&arrayA)   // 1.传数组指针
    fmt.Printf("arrayA : %p , %v\n", &arrayA, arrayA)
}

func testArrayPoint(x *[]int) {
    fmt.Printf("func Array : %p , %v\n", x, *x)
    (*x)[1] += 100
}

func Array : 0xc0000a6020 , [100 200]
arrayA : 0xc0000a6020 , [100 300]
  • 传切片,用切片传数组参数,既可以达到节约内存的目的,也可以达到合理处理好共享内存的问题。切片的指针和原来数组的指针是不同的。
func main() {
    arrayA := []int{100, 200}
    fmt.Printf("arrayA : %p , %v\n", &arrayA, arrayA)

    arrayB := arrayA[:]
    testArrayPoint(&arrayB)   // 2.传切片

    fmt.Printf("arrayA : %p , %v\n", &arrayA, arrayA)  // 切片的底层还是相同的数组,所以值变成了300
}

func testArrayPoint(x *[]int) {
    fmt.Printf("func Array : %p , %v\n", x, *x)
    (*x)[1] += 100
}

🔗

文章推荐