DEV Community

Chig Beef
Chig Beef

Posted on

I Hate Variable Shadowing

Intro

I know, it's been a while, I've actually written over 3,000 lines of code so far this month, and the game I'm working on, CompNerdSim. I love Golang, and I would rather be coding in this language than any other I know, however, variable shadowing is something that is hurting me deeply.

What Is Variable Shadowing

Here a Wiki on it, but I'll explain it here.
Let's say I define a variable in a certain scope, doesn't matter whether that's global, in a function, or in an if statement. Now, within that scope, I might have a smaller scope, such as a nested if, loop, etc. If I create a new variable in that smaller scope with the same name, it will mask the variable in the outer scope.
Here's an example of this.

package main

import "fmt"

func main() {
    x := 10
    {
        x := 5
        fmt.Println(x)
    }
    fmt.Println(x)
}

Enter fullscreen mode Exit fullscreen mode

Now what do you expect the result to be? If you're looking closely you'll see the := we use both times, so these are definitely different variables. So this outputs 5 and then 10, just as we would expect.
Also, just a side note, I know I kind of pulled out those squirlies without an if or a loop or anything, you can do that, it simulates scope, but you could have an if statement instead, just in case you're confused. It's basically identical to.

x := 10
if true {
    x := 5
    fmt.Println(x)
}
fmt.Println(x)
Enter fullscreen mode Exit fullscreen mode

One Character

So where is the first issue? Well it happens when we miss the : in :=. Let's see that code now.

x := 10
{
    x = 5
    fmt.Println(x)
}
fmt.Println(x)
Enter fullscreen mode Exit fullscreen mode

And our result is the number 5 twice. You expected this right? Good, because that still makes sense. But that doesn't mean it feels right so far, only one character makes this difference. This is a very simple example, but this issue that doesn't raise any compiler error could bring a lot of trouble if you don't realize, especially in bigger projects (such as mine is becoming).

Multi-Variable Assignment

This is where I start hating everything. I want you to look at this code.

func num() int {
    return 7
}

func some_num() (int, error) {
    return 3, nil
}

func main() {
    x := num()
    {
        x, err := some_num()
        if err != nil {
            log.Fatal(err.Error())
        }
        fmt.Println(x)
    }
    fmt.Println(x)
}
Enter fullscreen mode Exit fullscreen mode

So what do we have here? We have 2 functions, num and some_num. sum only returns a number, but some_num could error (in our simple case, it only returns nil because the error isn't too important. So can you guess the output? If you guessed 3 then 7 you're right! But dang it, we want the x in the inner scope to be the one in the outer, so we should just get rid of the : and that fixes it right? Nope, the compiler has a cry because err hasn't been defined, so it forces us to use :=, a very easy mistake.

How To Fix This Issue

Here's the code, but working as intended.

var err error
x := num()
{
    x, err = some_num()
    if err != nil {
        log.Fatal(err.Error())
    }
    fmt.Println(x)
}
fmt.Println(x)
Enter fullscreen mode Exit fullscreen mode

All we have to do is pre-define err. We don't have to do this outside of the inner scope, we could've done it right before x, err = some_num(), I just thought it was nicer up there.

The Issue In My Code

In CompNerdSim, you have to write code to make money, and well, this means there's an interpreter in the game. Here's an example of what a simplified lexer might look like (if you don't know what a lexer is, you can head straight to part 4 of Pogo).

func (l *Lexer) lex() {
    var token Token

    switch l.curChar {
    case '=':
        token = create_slither_token("ASSIGN", "=")
        do_some(token)
    case ',':
        token = create_slither_token("SEP", ",")
        do_some(token)
    case '(':
        token := create_slither_token("L_BRACE", "(")
        do_some(token)
    case ')':
        token = create_slither_token("R_BRACE", ")")
        do_some(token)
    case '[':
        token = create_slither_token("L_BLOCK", "[")
        do_some(token)
    case ']':
        token = create_slither_token("R_BLOCK", "]")
        do_some(token)
    case '{':
        token = create_slither_token("L_SQUIRLY", "{")
        do_some(token)
    case '}':
        token = create_slither_token("R_SQUIRLY", "}")
        do_some(token)
    case ';':
        token = create_slither_token("SEMICOLON", ";")
        do_some(token)
    }

    fmt.Println(token)
}
Enter fullscreen mode Exit fullscreen mode

Did you catch it? You're looking out for it so you probably saw that when we handled the left bracket, we used := instead of =, and with so many cases, some more complicated, how are we meant to notice and fix that? Suddenly, we've got an uninitialized token heading towards our parser, and where is it coming from? The compiler is giving no error, because nothing on a language level is wrong.
I don't want to act like my case is too terrible, it's just a game, nothing to get mad over, but if you're writing code that expected an initialized struct, and you just don't handle the case where it isn't, your code blows up. Now imagine 10x the lines of code, 10x the developers, and 100x the consequences of downtime, and you can see where you really need to be careful about these sorts of things.

What Do You Do?

For your average hobby programmer, however, just keep this in mind the next time your structs aren't initialized, that sneaky :=, although great shorthand, could really bite you in the butt.

Top comments (3)

Collapse
 
masterkind profile image
GO

Image description

My IDE does this - very helpful in such cases...

Collapse
 
chigbeef_77 profile image
Chig Beef

That's very cool, and so simple, what IDE do you use (I won't judge lol)?

Collapse
 
masterkind profile image
GO

Jetbrains...