6

golang中slice作为参数会怎么样

 3 years ago
source link: https://studygolang.com/articles/33009
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

golang参数传递其实只有一种就是值拷贝,那么slice作为参数传递的时候有什么特别的地方吗?

修改值

我们先看一个小示例

package main

import "fmt"

func changeValue(s []int) {
    fmt.Printf("inner: %v \t%p\n", s, s)
    s[0] = 0
    fmt.Printf("inner: %v \t%p\n", s, s)
}
func main() {
    s1 := []int{1, 2, 3}
    fmt.Printf("outer: %v \t%p\n", s1, s1)
    changeValue(s1)
    fmt.Printf("outer: %v \t%p\n", s1, s1)
}

它输出会是什么样的呢?

outer: [1 2 3]  0xc0000a2140
inner: [1 2 3]  0xc0000a2140
inner: [0 2 3]  0xc0000a2140
outer: [0 2 3]  0xc0000a2140

可以看到 我们通过slice作为参数的函数改变了原slice中的值,而且函数内外slice的内存地址都是一样的。但是golang是值拷贝,它是怎么通过函数内部改变外部的值呢,我们看一下slice的源码实现

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

可以看到,slice是一个结构体,它有三个属性,底层数组指针、长度、容量,因此,虽然值拷贝了一个副本,但是副本中有底层的数组指针,修改的是同一个内存地址,所以在函数内部改变了传入的slice参数,外部也会相应改变。

容量不够,长度增加

我们再看一个demo

package main

import "fmt"

func showAttribute(s []int) {
    fmt.Printf("addr: %p  len: %d  cap: %d  %v\n", s, len(s), cap(s), s)
}
func changeLength(s []int) {
    fmt.Printf("inner: ")
    showAttibute(s)
    s = append(s, 4)
    fmt.Printf("inner: ")
    showAttibute(s)
}

func main() {
    s1 := []int{1, 2, 3}
    fmt.Printf("outer: ")
    showAttibute(s1)
    // changeValue(s1)
    changeLength(s1)
    fmt.Printf("outer: ")
    showAttibute(s1)
}

它输出如下:

outer: addr: 0xc00000a400  len: 3  cap: 3  [1 2 3]
inner: addr: 0xc00000a400  len: 3  cap: 3  [1 2 3]
inner: addr: 0xc00000c330  len: 4  cap: 6  [1 2 3 4]
outer: addr: 0xc00000a400  len: 3  cap: 3  [1 2 3]

可以看到函数内部slice地址发生了变化,这是因为 s1 := []int{1, 2, 3} 建了一个len=3,cap=3的slice,当在后面追加一个值4的时候,slice副本的底层数组重新分配,cap加倍,len加一,因此地址改变。但是副本的变化不会影响函数外部slice的属性。

容量够,长度增加

我们再看另一种情况

func main() {
    s1 := []int{1, 2, 3}
    // fmt.Printf("outer: ")
    // showAttibute(s1)
    // // changeValue(s1)
    // changeLength(s1)
    // fmt.Printf("outer: ")
    // showAttibute(s1)

    s2 := make([]int, 0, 10)
    s2 = append(s2, s1...)
    fmt.Printf("outer: ")
    showAttibute(s2)
    // changeValue(s1)
    changeLength(s2)
    fmt.Printf("outer: ")
    showAttibute(s2)
}

输出如下

outer: addr: 0xc00000e230  len: 3  cap: 10  [1 2 3]
inner: addr: 0xc00000e230  len: 3  cap: 10  [1 2 3]
inner: addr: 0xc00000e230  len: 4  cap: 10  [1 2 3 4]
outer: addr: 0xc00000e230  len: 3  cap: 10  [1 2 3]

与上面不同的是 slice的地址没有改变。这是因为 s2 := make([]int, 0, 10) 建立了一个容量为10的slice,我们给他添加3个数据后,长度变为3,传递到函数 changeLength() 中后,追加了一个4,但是容量>=长度,底层数组没有重新分配,地址不改变,cap不改变,len加一。同样函数内部副本改变的长度信息改变不会影响函数外部slice,因为函数外部slice还是3个值。

range与slice

我们再看一个例子

package main

import "fmt"

func main() {
    arr := [5]int{1, 2, 3, 4, 5}
    sli := []int{1, 2, 3, 4, 5}

    for i, v := range arr { // range只在使用时求值一次,对于数组会拷贝一次,迭代的v来自数组副本
        if i == 0 {
            arr[i+2] = 33 // 在遍历i=0时,修改i=2时的arr值
            fmt.Println(arr)
        }
        arr[i] = v + 100 // 在遍历到i=2时,v是arr副本中的值,之前修改的arr[2]不会影响i=2时的v
    }
    fmt.Println(arr)
    for i, v := range sli { //slice拷贝副本中有底层数组指针,因此修改sli会影响v
        if i == 0 {
            sli[i+2] = 33 // 在遍历i=0时,修改i=2时的sli值
            fmt.Println(sli)
        }
        sli[i] = v + 100 // 遍历到i=2时,v就是sli[2]的值,前面的修改发生作用了
    }
    fmt.Println(sli)
}

其实看了前面的slice传参,这里就很好理解了,range就是个函数,arr和sli都是传进去的参数,虽然都是值拷贝,但是数组拷贝了的数据副本改变不会影响原数组,而slice副本中有底层数组指针,指向同一块内存,修改副本值就是修改原值。它的输出如下:

[1 2 33 4 5]
[101 102 103 104 105]
[1 2 33 4 5]
[101 102 133 104 105]

有疑问加站长微信联系(非本文作者)

eUjI7rn.png!mobile

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK