Last time, I started talking about why some of Go‘s time-saving features are actually false economy. Today, I want to go through our first example: short variable declarations.
In Go, you can declare a variable inside a function with an initial value using the syntax
x := 5
This is the same as
var x int = 5
except that you get to leave off “var” and the type. You can also reassign to another value of the same type if you declare another variable at the same time:
x := 1
x, y := 5, 10
Initial reaction: Hooray, type inference! This isn’t just saving us those 7 characters, but also the work of bothering to figure out exactly what types our variables should have. This might not matter too much for an int, but it’s nice to not always have to look up all the return types on every function we call.
But now the convenience starts to break down. The reassignment above worked because y is a new variable. If you don’t introduce any new variables, you can’t use the short form. That means this code is invalid:
x := 5
// Do something
x := 10
That’s a little annoying. Now I have to remember if I’ve previously declared
x so that I know whether I should use x :=
or x =
. That’s not really
worse than being unable to redeclare a variable in C++, so I guess I’ll forgive
it. Of course, the difference between :=
and =
is only a single character
instead of an entire variable declaration, but I can probably live with it.
When you combine this rule with multiple assignments, stuff starts to get weird. Imagine this perfectly fine code:
x := f()
x, err := g(x)
This can easily be broken by introducing a new use of err above:
y, err := h()
x := f()
x, err := g(x) // oops: no new variables on left side of :=
Again, this would be a problem in other languages that don’t let you redeclare variables. Then again, those languages generally don’t give you syntax for declaration and assignment that only differ by a single character, so at least you can quickly spot the mistake. I always have to be reminded by the compiler, so suddenly my edit-compile-run cycle becomes edit-compile-oops-compile-run. The speed advantage of Go’s compiler disappears quickly if this happens too often.
Even worse, the code above can also be broken by deleting a variable:
x := f()
x, _ := g(x) // oops: should have changed that := into =
What kind of crazy setup is that? Of course, it makes sense when you know what
:=
is actually doing, but that’s a major abstraction leak. When writing
code, I have to mentally replace the syntactic sugar with what it logically
expands to in order to predict what the compiler will do. So much for
intuitive. Hey, at least the compiler’s error message is clear, right?
This could be fixed in at least three ways:
-
Don’t allow
:=
at all. Always declare variables the long way. This keeps the compiler fast and gets rid of a lot of the potential for easy mixups, but it would be a shame to have to explicitly write out every variable type every time. -
Only allow
:=
if every variable in the list is new. This isn’t quite as convenient as the current usage when you’re reusing an err variable through a chain of function calls, but at least it makes the breakages happen at less surprising times. -
Make
:=
work even if every variable has been previously declared, as long as the types are compatible. The compiler already has to check if every variable in a short declaration is new, and it already has to check if the types being assigned to the non-new variables are compatible, so there shouldn’t be any extra work here.At that point, you could simply let
=
do the work instead of a separate:=
operator. Declaring variables explicitly would only be needed for documentation or giving something an explicit type. The Go compiler already warns about unused variables, so you wouldn’t even have to worry about accidently introducing a new variable by “declaring” a typo.
Obviously, I favor #3. That would change short variable declarations from a source of constant aggravation into a nice, type-inferencing assignment operator.