Image by

Assignment, reassignment, and shadowing in Go

Most people in Go are familiar with the basics of creating a variable and updating its value, but in some cases, it can become confusing even for engineers that are not new to Go. Let’s start with the basics. There are a few ways to declare and assign a variable:

Let’s focus on the operator := since it’s the one that can be tricky. As we’ve seen, it’s used to create a variable and assign a value at the same time. This means you cannot use it on a variable that is already declared… or can you?

As expected, this code won’t compile and throw the following error: no new variables on left side of := , but let’s try something else:

In this example, everything compiles but might not produce the expected results. In this case, the output is value in scope 2, value in main 1 . The reason here is that := create a second variable v . This is called shadowing. In any scope, you can shadow a symbol from the parent scope. The parent scope won’t be affected by the change. It’s usually highly recommended to avoid shadowing variables to prevent unwanted side effects. I have seen people shadowing variables by mistake multiple times, and this can lead to a headache when debugging. This is why I recommend people to use a linter on their editor/IDE that will let them know when they shadow a variable. You can do so using go vet .

Now, let’s look at something a bit more confusing:

What should be the expected result here? We use := twice on n so it should fail to compile, Right? Well… no, this code actually works. This is a common pattern in Go, but many people don’t really know what is actually happening. Is the variable n shadowed? This is where := does a bit of magic. When you have multiple assignations at the same time, if at least one of the variables isn’t declared, Go will be smart enough to know if it needs to assign or declare:

This leads us to this example that confuses people. Look at the do function:

Here it can be very confusing about what close is actually getting. Especially because we’re using := everywhere. Now that we know how := works it does remove a bunch of questions. What do we know here?

  • We know that os.Open will not create a new variable but assign a value to err.
  • &err will send the address of err.
  • The firstmayFailOrNot() will also assign a value to err , so if it fails this is what close will get. We don’t need to provide a value to return because we’re using a named return value, so whatever is in err at the time we do return is what will be returned.
  • Now, what about the second mayFailOrNot()? err is being shadowed so the err we have is a different variable, which means close() should not get the value… but it does. Interesting.
  • We then shadow err again, so based on the previous scope we now that it’s going to be caught by close . Except it‘s not.

So why do the 2 scopes act differently even though they both shadow the same variable? The reason here is the return statements. Remember, err is declared in the signature of the method, which means that ultimately the final value of err is the value we return. So whatever error we put in the return is what will be assigned to err which is what will be caught by close .

Staff Engineer at Abstract, Splice. I love Go, and I love Git. https://melvin.la

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store