Please see the series introduction for context on this post.
I'll start this series off with something that every Go programmer is familiar with, even if it quickly fades "background noise" with daily use: :=
and =
.
Short Variable Declarations Are (Almost) Redundant
The :=
keyword is referred to as a "short variable declaration". It is governed by some complex rules that dictate when it can and cannot be used.
I suspect that the :=
vs. =
distinction comes from a similar situation with C and C++, where a symbol's definition is distinct from its declarations. This has implications for compiler complexity, as in many cases all it needs is a declaration in order to interact with a symbol. A similar concern may have led to Go's use of this distinction, as compiler speed is an important Go feature.
In practice, as I am modifying code, this distinction generates a lot of noise. For example, consider a function
func doThings() error {
x, err := thing1();
if err != nil {
return err
}
err = thing2(x);
if err != nil {
return err
}
// ...
}
If this function is modified to remove the call to thing1
, the =
in the call to thing2
must be changed to :=
. This is all the more annoying when doing something temporary like commenting out the call to thing1
for a moment. In practice, I just add and remove :
as the compiler instructs me until the code compiles.
Which means, the compiler can infer which form is correct in most cases. Could it do so in all cases? If so, could the language drop the construct or set up gofmt
to automatically add or remove :
as necessary?
The Rules
Well, not quite. Inanc Gumus has a nice summary of the rules:
You can't use it twice for the same variable
legal := 42
legal ?? 43
The ??
here must unambiguously be =
.
You can use them twice in multi-variable declarations(*) and You can use them for multi-variable declarations and assignments
foo, bar := someFunc()
foo, bing ?? someFunc()
bar, bing ?? someFunc()
Again, the first ??
is completely specified: it must be :=
because bing
is new. The second is also clear: it must be =
because neither variable is new.
You can use them if a variable is already declared with the same name before and You can reuse them in scoped statement contexts like if, for, switch
var foo int = 34
func someFunc() {
foo ?? 42
// ...
}
This example is ambiguous. If ??
is replaced with =
, then someFunc
will modify the global foo
. If ??
is replaced with :=
, then foo
is a local variable that shadows the global foo
, and the global will not be modified.
foo := 34
if foo ?? aFunction(); foo == 34 {
// ...
}
The situation is the same here: depending on the presence of those two little dots :
, this will either modify or shadow the global foo
.
Shadowing
In general, shadowing is bad, as it can easily lead to difficult-to-detect errors, especially when moving code around. From a safety perspective, languages shouldn't allow programmers to do the sort of things that get programmers in trouble.
What a Drag
The distinction between :=
and =
slows down the process of writing code. Go programmers will be familiar with the situation: you decide to swap the order of two error
-returning operations, and the editor lights up with a syntax error, leaving you to add and remove :
characters until it's happy. Or, you comment out a chunk of code for testing, and must make a half-dozen :
-related changes below that point, only to revert them when the test is complete.
The drag is minor, to be sure, but these things add up over months and years. And it's also true that automation could fix this. For example, gofmt
could insert or remove :
as appropriate, and govet
could warn about the ambiguous cases. But this just adds additional churn to diffs with the appearance and disappearance of :
characters, with no meaning for the human reader.
Safety
Another consequence of this design decision is the risk of subtly incorrect code slipping by review, and even introducing security vulnerabilities. Here's a particularly insidious case:
func login(username) {
var userID int
if (remoteUserLookups) {
userID, err := lookupUser(username)
if err != nil { ... }
}
}
Do you know what that :=
does with userID
? Are you sure? Would you catch it in review? Could this confusion hide a security vulnerability?
The answer is that it makes a new userID
that shadows the function-scope variable. I expect this is why the language spec refers to this as "redeclaring" when one of several variables to the left of :=
is already defined. That's pretty subtle.
Even recognizing this subtlety, it takes more time and more thought for authors and reviewers to ensure the correctness of this code, further slowing development.
Lessons
The two lessons I see here (and we'll see these recur in this blog series) are
- requiring humans to think about unnecessary things slows development; and
- subtle syntactic differences with substantial differences in meaning can lead to dangerous bugs.
Top comments (0)