深入了解Go语言切片的传递方式和底层结构

切片slice几乎是每个Go开发者和项目中都会经常使用的一种数据类型。在Go语言中,切片的知识十分广泛,因此我个人认为对切片要进行深入了解。

至今,网络上仍存在很多关于切片传递方式的错误观念,比如错误地宣称切片作为函数参数传递的是引用。实际上,无论是官方说明还是实际操作,切片作为函数参数传递的是值,就像数组一样。

接下来我们通过具体的例子来加深对切片传递方式的理解。

切片作为函数参数传递的是值的例子:

func main() {
    mSlice := []int{1, 2, 3}
    fmt.Printf("main-1: %p \n", &mSlice) // 0x140000b2000
    mAppend(mSlice)
    fmt.Printf("main-2: %p \n", &mSlice) // 0x140000b2000
}

func mAppend(slice []int) {
    fmt.Printf("append func: %p \n", &slice) // 0x140000b2018 与外部的不同
}

通过以上例子,我们可以清楚地看到切片作为函数参数传递是值的情况。

而以下是一个错误的例子,也是目前经常用来误导切片作为函数参数传递是引用的错误文章中经常出现的情况:

func main() {
    mSlice := []int{1, 2, 3}
    fmt.Printf("main-1: %v \n", mSlice) // [1,2,3]
    mAppend(mSlice)
    fmt.Printf("main-2: %v \n", mSlice) // [1,9,3],这里2被修改了,但不是引用传递导致的
}

func mAppend(slice []int) {
    slice[2] = 9 // 修改
}

在Go语言中,切片的本质是一个结构体。作为函数参数传递时,遵循结构体的性质。结构体中包含指针和长度信息,当长度大于容量时会触发数组的扩容。

在实际应用中,需要特别注意函数内切片append操作引起的扩容对原切片的影响。

func main() {
    mSlice := []int{1, 2, 3}
    fmt.Printf("main-1: %v \n", mSlice) // [1,2,3]
    mAppend(mSlice)
    fmt.Printf("main-2: %v \n", mSlice) // [1,2,3] 未生效
}

func mAppend(slice []int) {
    // slice[2] = 9 // 修改
    slice = append(slice, 4)
    fmt.Printf("append: %v \n", slice) // [1,2,3,4]
}

上述例子展示了切片扩容对函数内切片修改的影响。需要注意,当切片长度小于容量时,进行append操作引起的扩容会导致新切片的形成,旧切片并不会被修改。

除此之外,还有一个情况,即不引起切片底层数组扩容的情况下验证切片是否指向新数组的例子:

func main() {
    mSlice := make([]int, 3, 4) // len = 3, cap = 4, cap > len
    fmt.Printf("main-1: %v, 数组地址: %p \n", mSlice, mSlice) // [0,0,0], 0x14000120000
    mAppend(mSlice)
    fmt.Printf("main-2: %v, 数组地址: %p \n", mSlice, mSlice) // [0,0,0], 0x14000120000 
}

func mAppend(slice []int) {
    slice = append(slice, 4)
    fmt.Printf("append: %v, 数组地址: %p \n", slice, slice) // [0,0,0,4], 0x14000120000
}

在这个例子中,我们可以看到切片的底层数组地址并没有改变,这是因为切片在传递给函数时仍然是值传递,因此函数内的修改并不会影响原切片。

通过以上示例我们可以得出,要让切片在函数内的修改生效,最好的方式就是使用指针传参。下面是相应的例子:

func main() {
    mSlice := []int{1, 2, 3}
    fmt.Printf("main-1: %v, 数组地址: %p \n", mSlice, mSlice) // [1,2,3], 0x1400001a0a8
    mAppend(&mSlice)
    fmt.Printf("main-2: %v, 数组地址: %p \n", mSlice, mSlice) // [1,2,3,4], 0x1400001a0a8
}

func mAppend(slice *[]int) {
    *slice = append(*slice, 4)
    fmt.Printf("append: %v, 数组地址: %p \n", *slice, slice) // [1,2,3,4], 0x140000181b0
}

在这个例子中,我们成功地在函数内使用append修改了切片,并且也能看到切片数组地址的变化,这是因为进行了扩容操作。但底层数组的指针并没有改变,因此扩容后仍然指向原数组。

最后,关于切片扩容的相关信息:

  • 在Go 1.18之前,切片扩容的临界值为1024。长度小于1024时,切片先以两倍长度进行扩容。当长度大于1024时,每次增加25%的容量,直到新容量大于期望容量。
  • 而在Go 1.18及之后,切片扩容的临界值为256。长度小于256时,仍然以两倍长度进行扩容。而长度大于256时,采用新的算法,并且直到新容量大于期望容量。

未经允许不得转载:大白鲨游戏网 » 深入了解Go语言切片的传递方式和底层结构