A simple for cycle will also step on the pit

Posted by The_Black_Knight on Mon, 03 Jan 2022 12:22:19 +0100

preface

When a business is implemented recently, the data needs to be read and then processed asynchronously; Naturally, it is relatively simple to implement in Go. The pseudo code is as follows:

    list := []*Demo{{"a"}, {"b"}}
    for _, v := range list {
        go func() {
            fmt.Println("name="+v.Name)
        }()
    }
    
    type Demo struct {
        Name string
    }

<!--more-->

It seems very simple, but a few lines of code are inconsistent with our expectations. After printing, the output is:

name=b
name=b

Not what we expected:

name=a
name=b

Kengyi

Because the qualification of writing go is still shallow and the Taoist profession is even shallower, I just found this bug for an hour or so; At first, I thought it was a data source problem. I experienced several rounds of self doubt. In short, let's see how to fix this problem.

First, the first method is to use temporary variables:

    list := []*Demo{{"a"}, {"b"}}
    for _, v := range list {
        temp:=v
        go func() {
            fmt.Println("name="+temp.Name)
        }()
    }

In this way, we can output correctly. In fact, we can also see the clue of the problem from this writing method.

When the first one does not use temporary variables, the main coroutine will run quickly, and the printed sub coroutine may not run yet; When it starts running, v here has been assigned by the last one.

So what is printed here is always the last variable.

The use of temporary variables will copy the current traversal value, so they will not affect each other.

Of course, in addition to temporary variables, closures can also be used.

    list := []*Demo{{"a"}, {"b"}}
    for _, v := range list {
        go func(temp *Demo) {
            fmt.Println("name="+temp.Name)
        }(v)
    }

When passing parameters through closures, each goroutine will store a copy of the parameters in its own stack, which can also be distinguished.

Pit II

Similarly, there is a second pit:

    list2 := []Demo{{"a"}, {"b"}}
    var alist []*Demo
    for _, test := range list2 {
        alist = append(alist, &test)
    }
    fmt.Println(alist[0].Name, alist[1].Name)

This code is not what we expected:

b b

But we can modify it slightly:

    list2 := []Demo{{"a"}, {"b"}}
    var alist []Demo
    for _, test := range list2 {
        fmt.Printf("addr=%p\n", &test)
        alist = append(alist, test)
    }
    fmt.Println(alist[0].Name, alist[1].Name)
addr=0xc000010240
addr=0xc000010240
a b

By the way, I printed the memory address. In fact, I can guess the reason from the result; The memory addresses printed each time are the same, so if we store pointers, essentially we store the contents of the same memory address, so the values are the same.

If we only store values without pointers, we will not have this problem.

But what if you want to use a pointer?

    list2 := []Demo{{"a"}, {"b"}}
    var alist []*Demo
    for _, test := range list2 {
        temp := test
        //fmt.Printf("addr=%p\n", &test)
        alist = append(alist, &temp)
    }
    fmt.Println(alist[0].Name, alist[1].Name)

It is also simple. You can use temporary variables in the same way.

From the official source code, we can know that for range is only a syntax sugar and is essentially a for loop; Because the same object is traversed and assigned every time, such "Oolong" will appear.

defer's pit

for loop + defer is also a combination pit (although it is not recommended). Let's take an example first:

// demo1
func main() {
    a := []int{1, 2, 3}
    for _, v := range a {
        defer fmt.Println(v)
    }
}

// demo2
func main() {
    a := []int{1, 2, 3}
    for _, v := range a {
        defer func() {
            fmt.Println(v)
        }()
    }
}

Output separately:

//demo1
3
2
1
//demo2
3
3
3

The result of demo1 is easy to understand. defer can be understood as putting the execution statement into the stack, so the result presented is first in and last out.

In demo2, because it is a closure and the closure holds a reference to the variable v, v has been assigned by the last value when the final execution is delayed, so it is the same when printed.

The solution is similar to the above. You can solve it by passing in parameters:

    for _, v := range a {
        defer func(v int) {
            fmt.Println(v)
        }(v)
    }

The daily development rate of such detailed problems can't be met. The most likely thing to encounter is the interview, so it's no harm to know more.

summary

Similar to the first case, when goroutine is called in the for loop, I think the IDE can remind you completely; For example, the IDEA includes most of the errors that may be sent, and looks forward to the subsequent updates of Golan.

But in fact, these mistakes have been reminded by the official blog.


https://github.com/golang/go/wiki/CommonMistakes#using-reference-to-loop-iterator-variable
It's just that most people probably haven't seen it. After this, I have to spend time reading it.

Topics: Go goroutine