Short variable declarations, cont'd

August 17, 2023

Today we’ll look at one of the most interesting, and I would say, confusing, aspects of short variable declarations.

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. The non-blank variable names on the left side of := must be unique.

field1, offset := nextField(str, 0)
field2, offset := nextField(str, offset)  // redeclares offset
x, y, x := 1, 2, 3                        // illegal: x repeated on left side of :=

Honestly, I wish this variable re-declaration wasn’t a thing in Go. It has a number of drawbacks, which, in my view, make it more hassle than its worth. Let’s look at two of them.

  1. It makes it easy to accidentally shadow variables

    This one comes up on StackOverflow all the time. And it still bites me from time to time. Let’s look at a common example, inspired by the many StackOverflow posts on the topic

    package main
    
    var db *sql.DB
    
    func main() {
     	db, err := sql.Open( /* database connection arguments */)
     	if err != nil {
     		panic(err)
    	}
    	defer db.Close()
    	querySomething()
    }
    
    func querySomething() {
    	result := db.Query( /* ... */ ) // Runtime panic
    }
    

    If it’s not obvious what’s going on here, the short variable declaration db, err := looks like it ought to re-declare db, but it doesn’t, because the original declaration is in a different scope. As such, it actually creates a new variable of the same name, shadowing the original. This means that querySomething() has no access to the properly initialized db variable (whose scope is limited to main()), and crashes with a nil pointer dereference when executed.

  2. It causes some “spooky action at a distance”, or shall we say, “variable declaration entanglement” when making code changes. Let’s see an example that closely mimics some refactoring I’ve been doing lately for a client:

    Say we start with this code:

    func UpdateThing() error {
    	thing := MustGetThing()
    	/* a dozen lines or so of other code */
    	err := StoreThing(thing)
    	if err != nil {
    		return err
    	}
    }
    

    Now let’s say we make a change to replace MustGetThing (which panics in case of error) to a version called GetThing which returns an error:

    -	thing := MustGetThing()
    +	thing, err := GetThing()
    +	if err != nil {
    +		return err
    +	}
    

    Do you see any problem?

    The diff looks perfectly reasonable. However, now the code does not compile, with the error “no new variables on left side of :=”, because the following short declaration of err is now invalid. We need to make one additional change as well:

    -	err := StoreThing(thing)
    +	err = StoreThing(thing)
    

Quotes from The Go Programming Language Specification Version of August 2, 2023

Share this