Go 语言切片(Slices)

19 阅读6分钟

一、切片的定义和赋值

在 Go 语言中,切片是对数组的一种引用,它本身并不存储数据。切片的定义形式如s := []int{1, 2, 3}。切片在内存中的结构包含一个指向底层数组的指针、切片的长度(Len)和切片的容量(Cap)。例如,对于上述切片s,它有一个指针指向存储123的底层数组,长度为 3,容量至少为 3。

可以通过赋值语句将一个切片赋给另一个切片变量,例如:s1 := s,此时s1s将共享底层数组。

二、切片的多种初始化方式

1. 使用字面量

s := []int{1, 2, 3}

2. 从数组切片

arr := [5]int{1, 2, 3, 4, 5}
s := arr[1:3]

3. 使用make函数

s := make([]int, 3, 5)

创建长度为 3、容量为 5 的切片。

三、切片的数据访问

可以通过索引来访问切片中的元素,索引从 0 开始。例如,对于切片s := []int{1, 2, 3}s[0]表示访问第一个元素,s[2]表示访问最后一个元素。如果访问超出切片长度范围的索引会导致程序出现运行时错误(panic)。

四、通过省略号添加多个元素到切片

可以使用...语法将一个数组或切片的元素添加到另一个切片中。例如:

arr := [3]int{4, 5, 6}
s := []int{1, 2, 3}
s = append(s, arr...)

这里将数组arr的元素添加到切片s中。

五、切片的元素删除和拷贝

1. 元素删除

可以通过重新切片的方式来删除切片中的元素。例如,要删除切片s := []int{1, 2, 3, 4, 5}中的第三个元素,可以这样做:s = append(s[:2], s[3:]...),现在s变为[1, 2, 4, 5]

2. 元素拷贝

可以使用copy函数进行切片的拷贝。例如:

s1 := []int{1, 2, 3}
s2 := make([]int, len(s1))
copy(s2, s1)

现在s2s1的一个副本,修改s2中的元素不会影响s1

六、为什么要懂切片的原理

  1. 避免意外的数据修改:了解切片的底层存储原理可以帮助我们避免因为多个切片共享底层数组而导致的意外数据修改。
  2. 优化性能:理解切片的扩容机制可以让我们在创建切片时合理地预估容量大小,减少频繁扩容带来的性能开销。
  3. 正确处理内存分配:知道切片的内存布局有助于我们更好地管理内存,避免内存泄漏和不必要的内存分配。

七、切片的底层存储原理

切片的底层数据结构包含三个部分:一个指向底层数组的指针、切片的长度(Len)和切片的容量(Cap)。

当使用append函数向切片添加元素时,切片可能会发生扩容。如果容量足够,append操作会直接在底层数组的剩余空间中添加新元素,然后更新切片的长度。如果容量不够,Go 语言会创建一个新的底层数组,将原来底层数组中的元素复制到新数组中,再把新元素添加到新数组中,最后更新切片的指针、长度和容量,使切片指向新的底层数组。

多个切片可以共享同一个底层数组,这意味着对一个切片的修改可能会影响到其他共享底层数组的切片。


通过对 Go 语言切片的深入理解,我们可以更好地利用这一强大的数据结构,提高代码的效率和可读性。

Go语言中切片(Slice)作为函数参数的传递方式

在Go语言中,切片作为函数参数是值传递,但由于切片本身的结构特点,其行为表现出类似引用传递的效果。

一、切片的数据结构

切片在Go语言中是一种引用类型,它包含三个部分:一个指向底层数组的指针、切片的长度(Len)和切片的容量(Cap)。

当切片作为函数参数传递时,实际上是将切片这三个部分的值进行复制,传递给函数中的参数变量。这个过程就像是值传递一样,函数内部的参数变量和外部的切片变量是两个不同的变量。

二、函数内部操作对切片的影响

  1. 修改切片元素的情况
    • 因为函数内部的切片参数和外部切片共享底层数组,所以当在函数内部修改切片元素时,实际上是通过指针修改了共享的底层数组中的元素。
    • 例如:
    func modifySlice(slice []int) {
        slice[0] = 100
    }
    func main() {
        slice := []int{1, 2, 3}
        modifySlice(slice)
        // 此时slice变为[100, 2, 3]
    }
    
    • 在这个例子中,modifySlice函数接收一个切片参数slice,在函数内部修改了slice[0]的值。由于函数内部的切片和外部的切片共享底层数组,所以外部的切片也会看到这个修改。
  2. 重新切片或扩容的情况
    • 如果在函数内部对切片进行重新切片或者扩容操作,情况会有所不同。重新切片只是改变了切片的长度和指针指向底层数组的位置,而扩容操作可能会导致切片指向一个全新的底层数组。
    • 例如,在函数内部进行扩容操作:
    func resizeSlice(slice []int) {
        newSlice := append(slice, 4)
        // 此时newSlice和slice可能指向不同的底层数组
    }
    
    • append操作触发扩容时,newSlice会指向一个新的底层数组,而外部的slice仍然指向原来的底层数组。所以在这种情况下,外部的切片不会受到函数内部扩容操作的直接影响,除非函数通过返回值将新的切片返回给外部。

三、总结

虽然切片作为函数参数是值传递(传递切片的三个部分的值),但由于切片包含了指向底层数组的指针,使得在函数内部可以通过这个指针修改共享的底层数组中的元素,从而在行为上表现出类似于引用传递的效果。这种特性使得切片在函数之间传递和共享数据时非常方便,但也需要注意在函数内部对切片的操作可能会对外部切片产生的影响,特别是在涉及重新切片和扩容等操作时。