Assigning slice vs redeclaring slice in Go

Viewed 297

I'm trying to use slices as a queue data structure and I came up with this implementation, that results in an infinite loop. This is because queue slice doesn't get updated with sub-slice queue[1:].

func badQueue() {
    queue := []int{0,1,2,3,4,5}
    for len(queue) > 0 {
        current, queue := queue[0], queue[1:]
        fmt.Println(current, queue)
    }
}
0 [1 2 3 4 5]
0 [1 2 3 4 5]
0 [1 2 3 4 5]
0 [1 2 3 4 5]
0 [1 2 3 4 5]
...

I have figured out that the issue is related to the fact that I am re-declaring current and queue (with :=) instead of assigning the value, which fixes the issue:

func goodQueue() {
    queue := []int{0,1,2,3,4,5}
    var current int
    for len(queue) > 0 {
        current, queue = queue[0], queue[1:]
        fmt.Println(current, queue)
    }
}
0 [1 2 3 4 5]
1 [2 3 4 5]
2 [3 4 5]
3 [4 5]
4 [5]
5 []

I know what is causing the issue, but I don't fully understand why re-declaration operation in this case is not working in the same way as assigning. Why queue is not re-declared with sub-slice of queue (queue[1:])?

Thanks!

3 Answers

Because you can have multiple variables with the same name, as long as they have different scope. The one in the inner scope will shadow the variable in the outer scope.

So if we break down your example

func badQueue() {
    // queue from outer scope, lets call it A
    queue := []int{0,1,2,3,4,5}
    // the only visible queue here is A, so len(queue) will always refer to A
    for len(queue) > 0 {
        // same thing here, the only visible queue is A, so queue[0] and queue[1:]
        // both refer to A
        // We are also declaring new variables, queue and current
        // This queue is now shadowing the outer queue, let's call this one B
        current, queue := queue[0], queue[1:]

        // Here queue will refer to B
        fmt.Println(current, queue)

        // When the current iteration of the loop ends current and queue B is destroyed
        // because they go out of scope and the loop start over with A unchanged
    }
}

The variable queue in the short-declaration case, is scoped only for the body of the loop, i.e. destroyed and created for every new iteration. As quoted in Short variable declarations

Unlike regular variable declarations, a short variable declaration may redeclare variables provided they were originally declared earlier in the same block (or the parameter lists if the block is the function body) with the same type, and at least one of the non-blank variables is new. As a consequence, redeclaration can only appear in a multi-variable short declaration. Redeclaration does not introduce a new variable; it just assigns a new value to the original.

There're two points required to understand how this works in Go:

  1. Scope

    Go uses block scope, each brace bracket pair will create a new scope and identifier in inner scope will shadow the one in outer scope if they have the same declaration name.

     func main() {
         var name string = "Golang"
         fmt.Printf("Outer Scope: %s\n", name) // Print "Golang"
         {
             var name string = "Java"
             fmt.Printf("Inner Scope: %s\n", name)   // Print "Java"
         }   
         fmt.Printf("Outer Scope: %s\n", name)  // Print "Golang" again
      }
    
  2. Short variable declaration

    Operator := is a composite operation which will do several things in one statment: declaration, type inference and assignment, basically you can treat it as a syntax sugar. The following code sample of S1 and S2 are equivalent:

    func main() {
        // S1
        var name string = "Golang" 
    
        // S2
        name := "Golang"
    }
    

With above two points in mind, your code will be the following after translation:

func badQueue() {
    queue := []int{0,1,2,3,4,5}
    for len(queue) > 0 {
        var current int
        var queue []int
        current, queue = queue[0], queue[1:]
        fmt.Println(current, queue)
    }
}

It's clear to see the outer queue is not affected inside for loop.

BTW, for each variable on the left hand side of :=, the compiler will lookup the current block scope to try to resolve the identifier, if it's already declared before, the compiler will reuse it rather than creating a new one. But if all lhs variables are declared before, the compiler will report an error with message "no new variables on left side of :=". Check out the following code:

func main() {
    var name string
    name, age := "Golang", 10 // reuse the above 'name' and create a new variable 'age'

    var tom, jerry string
    tom, jerry := "Tom", "Jerry" // no new variables on left side of :=
}

The new compiler implementation is here, for those who're interested in the details.

Related