DEV Community 👩‍💻👨‍💻

djmitche
djmitche

Posted on

Language Design Lessons: Short Variable Declarations

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
    }

    // ...
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
    // ...
}
Enter fullscreen mode Exit fullscreen mode

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 {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

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 { ... }
    }
}
Enter fullscreen mode Exit fullscreen mode

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)

Become a Moderator Do you want us to help make DEV a better place?

Fill out this survey and help us by becoming a tag moderator here at DEV.