Go切片内容丢失问题


title: "Go切片内容丢失问题"
date: 2017-10-24
draft: false
categories: ["Golang"]
tags: [""]


感觉这篇文章有点标题党的嫌疑, 文中要说的其实还是Go的值传递的问题, 不是发现了Go的某些惊天大bug,误入的大神可以不用看下面的内容啦 !

起因是在编写公司一个项目的过程中, 发现如果在结构体的不同方法之间调用其包含的切片,会出现切片内容不一致的情况.代码类似这样:

type T struct {
    slice []int
}

func (t T) Append() {
    t.slice = append(t.slice, 1)
    fmt.Println("#1 slice:",t.slice)
}

func (t T) Show() {
    fmt.Println("#2 slice:", t.slice)
}

func main() {
    t := T{}
    t.slice = make([]int, 0, 1)
    t.Append()
    t.Show()
}

执行上面的代码会输出:

#1 slice: [1]
#2 slice: []

按照正常的思维, 切片应该都是输出[1]才对,但其实因为Go的值传递模型, 导致了结果与预料的出现了偏差.之前以为自己对这个问题彻底搞懂了,没想到还是会纠结了一下, 下面记录下解决问题的过程,希望自己彻底理解Go的值传递模型,也希望能对你有帮助.

为上面的代码添加一些打印输出:

type T struct {
    slice []int
}

func (t T) Append() {
    fmt.Printf("Append: %p\n", &t)
    t.slice = append(t.slice, 1)
}

func (t T) Show() {
        fmt.Printf("Show: %p\n", &t)
}

func main() {
    t := T{}
    fmt.Printf("main: %p\n", &t)
    t.slice = make([]int, 0, 1)
    t.Append()
    t.Show()
}

输出为

main: 0x1040a0b0
Append: 0x1040a0c0
Show: 0x1040a0d0

可以看到, 三个变量t的地址都不相同, 所以在调用某个类型的方法的时候, Go是会拷贝一个新的值,确实是使用值传递.

接下来打印slice的地址出来看看(我在这里犯了一个错误)

type T struct {
    slice []int
}

func (t T) Append() {
    t.slice = append(t.slice, 1)
    fmt.Printf("Append slice: %p\n", t.slice)
}

func (t T) Show() {
         fmt.Printf("Show slice: %p\n", t.slice)
}

func main() {
    t := T{}
    t.slice = make([]int, 0, 1)
    fmt.Printf("main slice: %p\n", t.slice)
    t.Append()
    t.Show()
}

输出为:

main slice: 0x10410020
Append slice: 0x10410020
Show slice: 0x10410020

看到这里输出的地址相同, 我误以为调用类型方法的时候,对切片没有使用值传递.但其实不然

Go的切片本质是也是一个结构体, 在源码中可以看到它的定义:

// src/runtime/slice.go
type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

我们上面输出的3个地址都是这个结构体的地址.但不是说Go是值传递的吗,为什么这里又传了引用?
其实Go在这里还是使用了值传递, 但对于slice, map, channel这三种应用类型, Go又不会做全部的复制处理, 如果这样做显然开销太大.
Go默默得为我们新建了一个变量, 然后将这个变量的值指向了切片.说起来有点绕, 用一张图来表示会更直白:

Go切片内容丢失问题_第1张图片
image.png

我们打印看下图中 t ' 对应的slice的地址看看:

type T struct {
    slice []int
}

func (t T) Append() {
    t.slice = append(t.slice, 1)
    fmt.Printf("Append slice: %p\n", &t.slice)
}

func (t T) Show() {
         fmt.Printf("Show slice: %p\n", &t.slice)
}

func main() {
    t := T{}
    t.slice = make([]int, 0, 1)
    fmt.Printf("main slice: %p\n", &t.slice)
    t.Append()
    t.Show()
}

输出的3个地址确实都不同:

main slice: 0x1040a0b0
Append slice: 0x1040a0c0
Show slice: 0x1040a0d0

总结:
Go里面的参数传递其实都是按照值传递来进行的, 理解这点可以解决很多开发中遇到的问题.至于如何解决上面代码中切片元素不一致的问题 ? 很简单, 把类型方法中的T改为指针类型就可以了.

你可能感兴趣的:(Go切片内容丢失问题)