指针与切片

178 阅读3分钟

Go语言中的指针和切片(slice)


一、切片(Slice)的底层结构

Go语言的切片本质上是一个动态数组的抽象,其底层由三个关键部分组成:

type slice struct {
    array *[ ]T    // 指向底层数组的指针
    len   int      // 当前长度
    cap   int      // 容量(可扩展的空间)
}
  • 指针array指向底层数组的起始位置。
  • 长度len表示当前已使用的元素数量。
  • 容量cap表示底层数组的总长度(从起始位置到数组末尾)。

二、指针与切片的核心联系

1. 切片是引用类型

  • 切片本身并不直接存储数据,而是通过内部的指针引用底层数组
  • 赋值或传递切片时,只是复制切片的结构(指针、长度、容量),不复制底层数组
    s1 := []int{1, 2, 3}
    s2 := s1          // s2和s1共享同一个底层数组
    s2[0] = 100       // 修改s2会影响s1
    fmt.Println(s1)   // 输出 [100, 2, 3]
    

2. 函数参数传递

  • 当切片作为函数参数传递时,传递的是切片结构的值拷贝(包含指针的副本)。
  • 函数内部修改切片的元素会直接影响原始数据,但修改切片的长度或容量(如append)不会影响原切片。
    func modifySlice(s []int) {
        s[0] = 100     // 修改底层数组,影响原切片
        s = append(s, 4)  // 扩容可能导致新数组分配,不影响原切片
    }
    
    func main() {
        s := []int{1, 2, 3}
        modifySlice(s)
        fmt.Println(s)  // 输出 [100, 2, 3](未包含4)
    }
    

3. 切片扩容与指针变化

  • 当切片通过append操作超过容量时,会触发扩容,分配新数组并更新指针。
    s := make([]int, 2, 3)  // 容量为3
    s = append(s, 1)        // 容量足够,不扩容
    s = append(s, 2)        // 容量不足,扩容到6,指针指向新数组
    
  • 扩容后,新旧切片指向不同的底层数组,修改不再相互影响

4. 指针与切片的关系

  • 直接操作指针:通过指针可以直接修改底层数组。
    arr := [3]int{1, 2, 3}
    s := arr[:]
    p := &s[0]
    *p = 100       // 直接通过指针修改底层数组
    fmt.Println(arr)  // 输出 [100, 2, 3]
    
  • 切片指针的用途:如果需要在函数中修改切片的长度或容量,需传递切片的指针
    func appendToSlice(s *[]int, val int) {
        *s = append(*s, val)
    }
    
    func main() {
        s := []int{1, 2, 3}
        appendToSlice(&s, 4)
        fmt.Println(s)  // 输出 [1, 2, 3, 4]
    }
    

三、常见问题与最佳实践

1. 数据共享与竞态条件

  • 多个切片共享同一底层数组时,并发修改可能导致数据竞争。
    var wg sync.WaitGroup
    s := []int{1, 2, 3}
    
    wg.Add(2)
    go func() {
        s[0] = 100  // 并发修改
        wg.Done()
    }()
    go func() {
        s[0] = 200  // 并发修改
        wg.Done()
    }()
    wg.Wait()
    // 结果不确定!
    
    解决方案:使用互斥锁或避免共享可变数据。

2. 内存泄漏

  • 大切片保留对底层数组的引用时,即使只使用一小部分,也会阻止整个数组被GC回收。
    var bigData = make([]byte, 1<<30)  // 1GB数据
    smallSlice := bigData[:10]
    // bigData无法被回收,因为smallSlice仍引用它!
    
    解决方案:使用copy创建独立切片。

3. 性能优化

  • 避免频繁扩容:预分配足够容量(make([]T, 0, capacity))。
  • 减少切片拷贝:利用切片共享特性传递数据。

四、总结

场景指针与切片的关系
数据共享切片通过内部指针共享底层数组,修改元素会影响所有引用该数组的切片。
函数参数传递传递切片结构的值拷贝,函数内修改元素影响原数据,但扩容不影响原切片。
切片扩容扩容后指针指向新数组,与原切片解耦。
指针操作直接操作切片元素的指针可修改底层数组,需注意并发安全。

核心结论:切片通过内部指针实现高效的数据引用,但需明确其内存共享特性,避免意外副作用。在需要修改切片长度或容量时,需传递切片指针。