Why do these two for loop variations give me diffe

2020-02-01 16:51发布

问题:

I'm seeing different behavior in my program that's tied to this particular loop in my program but I'm not sure I understand why it's behaving the way it is.

//global variable
var cmds = []string {
    "create",
    "delete",
    "update",
}

func loop1() {

    actions := make(map[string]func())

    for _, cmd := range cmds {
        actions[cmd] = func() {
            fmt.Println(cmd)
        }
    }
    for _, action := range actions {
        action()
    }
}
func loop2() {

    actions := make(map[string]func())

    for i, cmd := range cmds {
        command := cmds[i]
        actions[cmd] = func() {
            fmt.Println(command)
        }
    }
    for _, action := range actions {
        action()
    }
}

The output for loop1() is

update
update
update

The output for loop2() is

delete
update
create

I went looking on the internet and read the following

When ranging over a slice, two values are returned for each iteration. The first is the index, and the second is a copy of the element at that index

It says a copy, so does that mean it returns a copy of the string but it's really a pointer to variable cmd? In which case any references to cmd will by the end of the loop all actually reference the last element in the array, e.g. update? Does this mean that elements of an array should always be referenced by their index when using the range method, and what's the use case for using the element it returns since it's always updating the pointer?

回答1:

The problem with loop1() is that you store a function literal in the actions map that references the loop variable cmd. There is only one instance of this loop variable, so when after the loop you call the functions stored in the actions map, all will refer to this single loop variable (which is kept because the functions / closures still have a reference to it), but its value at the time of execution will be the last value set by the for loop, which is the last value in the cmds slice (that is, "update", so you'll see "update" printed 3 times).

An easy workaround is to make a copy of this loop variable, so each iteration, each function literal will have its own copy, which is "detached" from the loop variable:

func loop1() {
    actions := make(map[string]func())

    for _, cmd := range cmds {
        cmd2 := cmd
        actions[cmd] = func() {
            fmt.Println(cmd2) // Refer to the detached, copy variable!
        }
    }
    for _, action := range actions {
        action()
    }
}

With this, output of loop1() (try it on the Go Playground):

update
create
delete

This it's not an issue of the for ... range, it's because the closures refer to the same variable, and you don't use the value of the variable right away, only after the loop. And when you print the value of this variable, all print the same, last value of it.

Also see this possible duplicate: Golang: Register multiple routes using range for loop slices/map