美文网首页
切片传递与指针传递到底有啥区别

切片传递与指针传递到底有啥区别

作者: 机器铃砍菜刀s | 来源:发表于2020-11-27 13:44 被阅读0次

提出疑问

在Go的源码库或者其他开源项目中,会发现有些函数在需要用到切片入参时,它采用是指向切片类型的指针,而非切片类型。这里未免会产生疑问:切片底层不就是指针指向底层数组数据吗,为何不直接传递切片,两者有什么区别

例如,在源码log包中,Logger对象上绑定了formatHeader方法,它的入参对象buf,其类型是*[]byte,而非[]byte

func (l *Logger) formatHeader(buf *[]byte, t time.Time, file string, line int) {}

有以下例子

func modifySlice(innerSlice []string) {
    innerSlice[0] = "b"
    innerSlice[1] = "b"
    fmt.Println(innerSlice)
}

func main() {
    outerSlice := []string{"a", "a"}
    modifySlice(outerSlice)
    fmt.Print(outerSlice)
}

// 输出如下
[b b]
[b b]

我们将modifySlice函数的入参类型改为指向切片的指针

func modifySlice(innerSlice *[]string) {
    (*innerSlice)[0] = "b"
    (*innerSlice)[1] = "b"
    fmt.Println(*innerSlice)
}

func main() {
    outerSlice := []string{"a", "a"}
    modifySlice(&outerSlice)
    fmt.Print(outerSlice)
}

// 输出如下
[b b]
[b b]

很好,在上面的例子中,两种函数传参类型得到的结果都一样,似乎没发现有什么区别。通过指针传递它看起来毫无用处,而且无论如何切片都是通过引用传递的,在两种情况下切片内容都得到了修改。

这印证了我们一贯的认知:函数内对切片的修改,将会影响到函数外的切片。但,真的是如此吗?

考证与解释

在《你真的懂string与[]byte的转换了吗》一文中,我们讲过切片的底层结构如下所示。

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

array是底层数组的指针,len表示长度,cap表示容量。

我们对上文中的例子,做以下细微的改动。

func modifySlice(innerSlice []string) {
    innerSlice = append(innerSlice, "a")
    innerSlice[0] = "b"
    innerSlice[1] = "b"
    fmt.Println(innerSlice)
}

func main() {
    outerSlice := []string{"a", "a"}
    modifySlice(outerSlice)
    fmt.Print(outerSlice)
}

// 输出如下
[b b a]
[a a]

神奇的事情发生了,函数内对切片的修改竟然没能对外部切片造成影响?

为了清晰地明白发生了什么,将打印添加更多细节。

func modifySlice(innerSlice []string) {
    fmt.Printf("%p %v   %p\n", &innerSlice, innerSlice, &innerSlice[0])
    innerSlice = append(innerSlice, "a")
    innerSlice[0] = "b"
    innerSlice[1] = "b"
    fmt.Printf("%p %v %p\n", &innerSlice, innerSlice, &innerSlice[0])
}

func main() {
    outerSlice := []string{"a", "a"}
    fmt.Printf("%p %v   %p\n", &outerSlice, outerSlice, &outerSlice[0])
    modifySlice(outerSlice)
    fmt.Printf("%p %v   %p\n", &outerSlice, outerSlice, &outerSlice[0])
}

// 输出如下
0xc00000c060 [a a]   0xc00000c080
0xc00000c0c0 [a a]   0xc00000c080
0xc00000c0c0 [b b a] 0xc000022080
0xc00000c060 [a a]   0xc00000c080

在Go函数中,函数的参数传递均是值传递。那么,将切片通过参数传递给函数,其实质是复制了slice结构体对象,两个slice结构体的字段值均相等。正常情况下,由于函数内slice结构体的array和函数外slice结构体的array指向的是同一底层数组,所以当对底层数组中的数据做修改时,两者均会受到影响。

但是存在这样的问题:如果指向底层数组的指针被覆盖或者修改(copy、重分配、append触发扩容),此时函数内部对数据的修改将不再影响到外部的切片,代表长度的len和容量cap也均不会被修改。

为了让读者更清晰的认识到这一点,将上述过程可视化如下。

1.png 2.png 3.png 4.png

可以看到,当切片的长度和容量相等时,发生append,就会触发切片的扩容。扩容时,会新建一个底层数组,将原有数组中的数据拷贝至新数组,追加的数据也会被置于新数组中。切片的array指针指向新底层数组。所以,函数内切片与函数外切片的关联已经彻底斩断,它的改变对函数外切片已经没有任何影响了。

注意,切片扩容并不总是等倍扩容。为了避免读者产生误解,这里对切片扩容原则简单说明一下(源码位于src/runtime/slice.go 中的 growslice 函数):

切片扩容时,当需要的容量超过原切片容量的两倍时,会直接使用需要的容量作为新容量。否则,当原切片长度小于1024时,新切片的容量会直接翻倍。而当原切片的容量大于等于1024时,会反复地增加25%,直到新容量超过所需要的容量。

到此,我们终于知道为什么有些函数在用到切片入参时,它需要采用指向切片类型的指针,而非切片类型。

func modifySlice(innerSlice *[]string) {
    *innerSlice = append(*innerSlice, "a")
    (*innerSlice)[0] = "b"
    (*innerSlice)[1] = "b"
    fmt.Println(*innerSlice)
}

func main() {
    outerSlice := []string{"a", "a"}
    modifySlice(&outerSlice)
    fmt.Print(outerSlice)
}

// 输出如下
[b b a]
[b b a]

请记住,如果你只想修改切片中元素的值,而不会更改切片的容量与指向,则可以按值传递切片,否则你应该考虑按指针传递。

例题巩固

为了判断读者是否已经真正理解上述问题,我将上面的例子做了两个变体,读者朋友们可以自测。

测试一

func modifySlice(innerSlice []string) {
    innerSlice[0] = "b"
  innerSlice = append(innerSlice, "a")
    innerSlice[1] = "b"
    fmt.Println(innerSlice)
}

func main() {
    outerSlice := []string{"a", "a"}
    modifySlice(outerSlice)
    fmt.Println(outerSlice)
}

测试二

func modifySlice(innerSlice []string) {
    innerSlice = append(innerSlice, "a")
    innerSlice[0] = "b"
    innerSlice[1] = "b"
    fmt.Println(innerSlice)
}

func main() {
    outerSlice:= make([]string, 0, 3)
    outerSlice = append(outerSlice, "a", "a")
    modifySlice(outerSlice)
    fmt.Println(outerSlice)
}

测试一答案

[b b a]
[b a]

测试二答案

[b b a]
[b b]

你做对了吗?

相关文章

  • 切片传递与指针传递到底有啥区别

    提出疑问 在Go的源码库或者其他开源项目中,会发现有些函数在需要用到切片入参时,它采用是指向切片类型的指针,而非切...

  • C++基础

    C++ 值传递、指针传递、引用传递详解C++中引用传递与指针传递区别 引用传递和指针传递的区别 引用的规则:(1)...

  • leetcode的Combination Sum题补充切片的传递

    Combination Sum以go 切片的传递 内容:回溯法。切片切为形参,为值传递。如果需要引用传递,要加指针...

  • golang值传递、指针传递

    函数参数传递过程中,数组是值传递的,切片是指针传递。 直接上代码:

  • golang学习笔记(七)复合类型

    复合类型 类型名称作为函数参数pointer指针值传递array数组值传递slice切片引用传递map字典引用传递...

  • golang中数组、切片以及映射(map)的特点

    数组 数组和切片的创建方式不同 数组是值传递,除非声明为指针传递 数组是切片和映射的基石 切片 切片是围绕动态数组...

  • 数组,切片

    值传递:数组,结构体指针(地址)传递:切片,结构体方法 数组 1.元素交换 切片 内存扩容,在内存大小小于1024...

  • 数组切片总结

    一句话总结:切片是动态数组。数组需要明确指定大小,切片不需要。数组是值传递,切片是地址传递。 区别 初始化 数组需...

  • 传递值与传递指针

    假如有一个变量: 这时要通过一个函数set改变a的值: 在main函数中调用: 最终a的结果还是0。这是因为传递到...

  • 读书笔记17.06.02【stack】【vector】

    C++中参数传递:按值传递,指针传递和引用传递按值传递:形参是实参的拷贝。指针传递:拷贝指针,被调用函数对指针指向...

网友评论

      本文标题:切片传递与指针传递到底有啥区别

      本文链接:https://www.haomeiwen.com/subject/tadxwktx.html