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)
}
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)
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)
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)
}
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)
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)
}
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.