Golang scope issue

A feature bug: Shadow Variables.

When you work long enough with languages like PHP or JavaScript, you get used to their very unusual behaviors. These are languages that are interpreted just in time. They have to be more tolerant to user mistakes. They fix issues on the fly because by the time they are reading your code, the code is already in production.

For compiled languages however, you get to find out about mistakes early on. When you hit the compile button, or run the command, your code fails in case of error. This is a good thing when it works. But compiled languages are not perfect either. They have to compromise and make some obscure rules, that may sometimes come back to bite you.

In the past 6 months, I've been working exclusively with Golang. Coming from PHP, I've pushed aside my own belief on what a programming language should or shouldn't do and so far it is working. I am writing production grade code everyday. But, until recently I had never encountered such an annoying bug/feature of Go.

The Problem

Here is a some code that I wrote, and for the life of me, I couldn't figure out why it wasn't working.

func doComplex() (string, error) {
    return "Success", nil
}

func main() {
    var val string
    num := 32

    switch num {
    case 4:
    // do nothing
    case 16:
    // do nothing
    case 32:
        val, err := doComplex()
        if err != nil {
            panic(err)
        }
        if val == "" {
            // do something else
        }
    case 64:
        // do nothing
    }
    fmt.Println(val)
}

By looking at this code, can you guess what the value of the string val is? Of course you can. It's Success right? If you had guess that, you'd be wrong. If you had guessed empty string? That's because you are like, so smart.

After simplifying the doComplex() function to just return a string and nil, it was still not working. So, I changed the return signature of doComplex() to only return a string, and it magically worked:

func doComplex() string {
    return "Success"
}

func main() {
    var val string
    num := 32

    switch num {
    case 4:
    // do nothing
    case 16:
    // do nothing
    case 32:
        val = doComplex()
        if val == "" {
            // do something else
        }
    case 64:
        // do nothing
    }
    fmt.Println(val)
}

Scopes & Shadows

What the hell is the problem? If you already know the answer, well good for you. For the rest of us, the problem is scopes and shadow variables. Let's only look at case 32 in the original code.

func main() {
    var val string
    num := 32

    switch num {
    case 32:
        val, err := doComplex()
        if err != nil {
            panic(err)
        }
        if val == "" {
            // do something else
        }
    }
    fmt.Println(val)
}

This code has 2 different scopes. The first scope is created in the main function. The second one is the switch statement. Scopes 2 is the child of scopes 1, meaning variables created in scopes 1 are available in scopes 2, but not the other way around.

golang scopes

In case 32, we created a brand new variable err. In go, to create a variable you use the colon equal sign :=. This creates the variable and assigns it a value. val is already defined, go is smart enough to know that only err is a new variable here. Except, this is a new scope. Go creates two new variables val, err in this new scope.

So when we write the statement val, err := doComplex(), this new variable also named val is assigned the string Success. But its life is short lived. When the switch statement is evaluated, val is thrown away since its job is done in the scope. Then when we get to fmt.Println(val) this is the original val we created in the beginning and it contains no value. So we print an empty string.

Note that when I changed the signature of the doComplex() function, I also removed the := sign. Which fixed the code, but it no longer returns an error.

The fix

Obviously this is not the intended behavior. My goal was to have val change value based on the specific case it lands on. We can do this by removing the := sign and declaring the error separately.

func main() {
    var val string
    num := 32

    switch num {
    case 32:
        var err error
        val, err = doComplex() // colon removed
        if err != nil {
            panic(err)
        }
        if val == "" {
            // do something else
        }
    }
    fmt.Println(val)
}

Now, only err is created in the switch scope. val is the original variable declared in the beginning of the function. It is correctly assigned the returned value of doComplex() and it prints the word Success successfully.

Knowing that Go creates different scopes and accesses local scopes first should help you and me avoid bugs like these in the future. Every time you see code between curly brackets {}, consider that a new scope is created.


Comments(2)

Jason :

This issue is common enough that it's part of the built in vetting/linter tool: https://golang.org/cmd/vet/#hdr-Shadowed_variables

You should consider setting up a linter/vet as part of your workflow to prevent mistakes like these and many more.

ibrahim :

Thanks Jason, the vetting tool is really useful. But it also good for developers to be aware of the issue the vetting tool is solving.

Let's hear your thoughts

For my eyes only