Hidden trap in GO for-loop

今天开发过程中发现了 go 的一个小 trick,在 for-loop 的执行过程中,根据 slice 的数据类型,有不同的表现。 先看出问题的代码:

func LoopWithBug(list []Obj) ObjList {
    newList := make([]*Obj, 0, len(list))
    for _, o := range list {
        newList = append(newList, &o)
    }
    return ObjList{
        Objs: newList,
    }
}

初看大家看出问题来了吗? 一开始出问题的时候我也没看出来问题所在,于是写了个测试代码来复现问题。测试代码如下:

package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    print("starting code...\n")

    // Set up the test list
    ll := []Obj{
        {Val: 1},
        {Val: 2},
        {Val: 3},
    }

    // call the loop with bug
    resWithBug := LoopWithBug(ll)

    // print out the result
    fmt.Println(resWithBug)
}

func LoopWithBug(list []Obj) ObjList {
    newList := make([]*Obj, 0, len(list))
    for _, o := range list {
        newList = append(newList, &o)
    }
    return ObjList{
        Objs: newList,
    }
}


// 测试用数据结构
type Obj struct {
    Val int
}

func (o *Obj) String() string {
    b, _ := json.Marshal(o)
    return string(b)
}

type ObjList struct {
    Objs []*Obj
}

func (ol *ObjList) String() string {
    b, _ := json.Marshal(ol)
    return string(b)
}

代码的运行结果是:

❯ go run main.go

starting code...
{[{"Val":3} {"Val":3} {"Val":3}]}

可以发现本来预期打印出来的应该是 1,2,3,但结果输出了 3,3,3。 于是更进一步排查,我决定把 for-loop 里面的变量值与变量的内存地址(指针值)也一起打印出来,看看问题所在。

func LoopWithBug(list []Obj) ObjList {
    newList := make([]*Obj, 0, len(list))
    for _, o := range list {
        fmt.Printf("Before append o=%v, o_pointer=%p, newList=%v\n", o, &o, newList)
        newList = append(newList, &o)
        fmt.Printf("After append newList=%v\n", newList)
    }
    return ObjList{
        Objs: newList,
    }
}
❯ go run main.go

starting code...
Before append o={1}, o_pointer=0x140000b4008, newList=[]
After append newList=[{"Val":1}]
Before append o={2}, o_pointer=0x140000b4008, newList=[{"Val":2}]
After append newList=[{"Val":2} {"Val":2}]
Before append o={3}, o_pointer=0x140000b4008, newList=[{"Val":3} {"Val":3}]
After append newList=[{"Val":3} {"Val":3} {"Val":3}]
{[{"Val":3} {"Val":3} {"Val":3}]}

从打印出来的日志看到,虽然 o 的值一直在变,但 o 的内存地址一直是同一个值。而我们通过 newList = append(newList, &o) 加入进去 newList 的值,其实是重复添加了同一个 o 的内存地址。

我们试着改一下赋值方法

func LoopWithBug(list []Obj) ObjList {
    newList := make([]*Obj, 0, len(list))
    for _, o := range list {
        localO := o
        fmt.Printf("Before append o=%v, o_pointer=%p, localO_pointer=%p, newList=%v\n", o, &o, &localO, newList)
        newList = append(newList, &localO)
        fmt.Printf("After append newList=%v\n", newList)
    }
    return ObjList{
        Objs: newList,
    }
}
❯ go run main.go

starting code...
Before append o={1}, o_pointer=0x14000124008, localO_pointer=0x14000124030, newList=[]
After append newList=[{"Val":1}]
Before append o={2}, o_pointer=0x14000124008, localO_pointer=0x14000124098, newList=[{"Val":1}]
After append newList=[{"Val":1} {"Val":2}]
Before append o={3}, o_pointer=0x14000124008, localO_pointer=0x14000124130, newList=[{"Val":1} {"Val":2}]
After append newList=[{"Val":1} {"Val":2} {"Val":3}]
{[{"Val":1} {"Val":2} {"Val":3}]}

经过修改,每次 append 进 list 的指针是新的指针,并且和原始 list 中的每个对象的指针是不同的,是在不同的内存地址存储的。于是这样就避免了 for-loop 中复用局部变量而导致的 bug。

下面看一下另外一个代码,大家可以看看这个代码打印出来的值是不是如预期的呢?

func main() {
    print("starting code...\n")

    // Set up the test list
    ll := []*Obj{
        {Val: 1},
        {Val: 2},
        {Val: 3},
    }

    // call the loop with bug
    resWithBug := LoopWithBug(ll)

    // print out the result
    fmt.Println(resWithBug)
}

func LoopWithBug(list []*Obj) ObjList {
    newList := make([]*Obj, 0, len(list))
    for _, o := range list {
        fmt.Printf("Before append o=%v, o_pointer=%p, newList=%v\n", o, &o, newList)
        newList = append(newList, o)
        fmt.Printf("After append newList=%v\n", newList)
    }
    return ObjList{
        Objs: newList,
    }
}

本文作者

张育鑫(Taylor Zhang)

腾讯高级工程师,认证高级云架构工程师,认证高级云开发工程师

机器学习平台,MLOps,大数据,云原生,数据密集型分布式系统

LinkedIn | Blog | Github

Did you find this article valuable?

Support Yuxin(Taylor) Zhang by becoming a sponsor. Any amount is appreciated!