Things I Would Remove from Go

If less is more, Go could gain by losing weight

Published Jan 15, 2019

The Go programming language espouses “less is more”. It prefers fewer features and “one way of doing things”. However, it still has some fat to lose! This article highlights what I consider unnecessary, and suggests the path to gradual deprecation and removal.

Goes without saying: this is an opinion piece. If we disagree, that’s cool!

This is just what I consider relatively easy to remove. I have other complaints about Go, mostly related to its deep fundamentals that would be very hard or impossible to change. They’re not mentioned in this piece.

We’re not allowed to break existing code under Go1. However, it seems plausible to migrate most existing code in advance, preparing it for the hypothetical Go2 that removes the deprecated features, alongside other breaking changes it’s expected to make. The following migration strategy seems realistic:

  • Go1 adds two minor syntactic features (see below)
  • A tool like go fix converts existing code to the “new” style, avoiding the “deprecated” features
  • Both “old” and “new” code continues to run under Go1
  • Go2 drops the unnecessary features

TOC

Language Changes

Remove := in favor of var

Arguments

1. Having two equivalent assignment forms is redundant.

2. := can’t justify itself with brevity. Compared to var, it requires one or two fewer keystrokes to type, but involves Shift and an awkward movement between : and =. Subjectively, I find var easier and faster to type.

3. Code sometimes needs to be converted between :=, var and const. For example, you have a string that’s initially produced by fmt.Sprintf, but as you edit the code, it becomes a const. Or vice versa. I find these conversions fiddly and awkward. Converting between var and const is noticeably easier.

Moving a declaration between local and global scopes also involves converting between := and var. This should be unnecessary.

4. Some idiomatic code already prefers var. For example, it’s commonly used for zero values:

var buf bytes.Buffer
buf.WriteString("hello world!")
_ = buf.Bytes()

5. As shown above, var allows to specify the type. Type inference is nice, but sometimes you have to spell it out:

num := 10

num := float64(10)

var num float64 = 10

var num = float64(10)

Without :=, you’d have less choice, which is good.

6. var also allows the blank identifier:

var _ = 123 // compiles
_ := 123    // doesn't compile

7. var is also better for code highlighting. While writing a Go syntax definition for Sublime Text, I found that it’s impossible to correctly scope the following:

one,
    two := someExpression

Scoping the variable names as declarations with := requires multiline lookahead or backtracking, neither of which is supported in the modern Sublime Text syntax engine.

With var, this can be properly scoped without multiline lookahead or backtracking:

var one,
    two = someExpression

Migration

Completely embracing var requires an addition to the language. Various forms of if, for, select, and switch currently support := but not var:

// compiles ok
select {
    case err := <-errChan:
    case msg := <-msgChan:
}

// doesn't compile
select {
    case var err = <-errChan:
    case var msg = <-msgChan:
}

For Go1, adding the missing var support would be a safe, backwards-compatible change.

See the related gofmt change.

Remove parenthesized lists from var, const, type, import

Let’s start with arguments in favor of the feature.

Currently, parenthesized lists have exactly one non-aesthetic reason to exist: const (...) enables the use of iota, acting as its scope.

import is traditionally listed, so the keyword doesn’t repeat:

import (
    "bytes"
    "encoding"
    "encoding/base64"
)
import "bytes"
import "encoding"
import "encoding/base64"

That’s a weak-ass justification for an entire language feature, made even weaker by goimports which edits your imports automatically.

Now, arguments against the feature.

Code should be convenient to type and edit. I think having options hinders that. Every time you write adjacent vars, some of your neurons are wasted on choosing between:

var one = _
var two = _

and:

var (
    one = _
    two = _
)

Worse, it occasionally leads to menial conversions between the two. That’s a waste of brainpower and typing. Let’s say you have a single var:

const ichi = 10

Now you’re adding another:

const ichi = 10
const ni = 20

You might be compelled to convert to the list style:

const (
    echi = 10
    ni   = 20
)

We’ve now wasted some brainpower and typing. Without lists, this would not have happened.

For consistency, the go.mod syntax should also remove lists.

Maybe remove iota due to removing lists

iota requires parenthesized const (...) for scoping. Removing lists also leads to removing iota.

While I tend to avoid iota, I don’t have a strong argument against it. If keeping iota in the language is important, then instead of removing lists entirely, we could just consider them non-idiomatic unless iota is used.

Remove new in favor of &

new was relevant when & was allowed only on “storage locations” such as variables and inner fields. Now that & is allowed on composite literals, new is close to obsolete.

new is limited to a zero value, while & allows content:

client := new(http.Client)
client.Timeout = time.Minute

client = &http.Client{Timeout: time.Minute}

Currently, & doesn’t work with non-composite literals:

// doesn't compile
_ = &"hello world!"

Before new can be removed, & needs to be extended to support primitive literals. That would make it strictly more powerful than new.

Allowing & on primitives would also make it easier to print Go data structures as code. Currently, pretty-printing libraries have to resort to ugly workarounds to support those types.

Note that most code can already be converted to &. Code like new(string) or new(int) should be extremely rare in the wild.

For Go1, extending & to primitive literals would be a safe, backwards-compatible change.

Remove dot-import: import . "some-package"

Dot-import splurges all exported definitions from another package into the current scope:

import . "fmt"

func main() {
    Println("hello world!")
}

Having read a considerable amount of code in multiple languages with this import style, I’m convinced that it’s always a bad idea. Subjectively, it makes the code harder to understand and harder to track down the definitions. Objectively, it makes the code more fragile against changes.

Remove if-assignment and derivatives: if _ := _ ; _ {}

Subjectively, I find this form annoying to type and annoying to read. Objectively, it’s a choice, and this post is predicated on “choice is bad”. This wastes everyone’s brainpower; anyone reading the code has to be aware of both syntactic forms.

Instead of two options:

if ok := _; ok { _ }

ok := _
if ok { _ }

Let’s leave just one option:

var ok = _
if ok { _ }

If subscoping the variable is vital, just use a block. This also allows you to subscope more than one variable.

{
    var ok = _
    if ok { _ }
}

Tool Changes

Gofmt: align adjacent non-listed var, const, type, import

Currently, gofmt aligns adjacent assignments only in parenthesized lists:

const (
    ichi = 10
    ni   = 20
    san  = 30
)

const ichi = 10
const ni = 20
const san = 30

After removing parenthesized lists, we probably want gofmt to align adjacent non-parenthesized assignments:

const ichi = 10
const ni   = 20
const san  = 30

Misc

While writing this post, I tried to argue that complex numbers should be moved from built-ins to the standard library, but ended unconvinced.

Arguments pro:

  • removing built-ins simplifies the language
  • can implement additional math functions as methods
  • can implement encoding and decoding methods for various formats

Arguments contra:

  • breaks code
  • additional functions can be provided as a package, mirroring math
  • support for encoding and decoding can be added to the corresponding packages: strconv, fmt, encoding/json, encoding/xml, etc.

In the end, I’m not convinced that it’s worthwhile.


Have any thoughts? Let me know!


This blog currently doesn't support comments. Respond using me@mitranim.com, twitter, or one of my other contacts.

Subscribe using one of: Atom RSS Feedly