Skip to content

Instantly share code, notes, and snippets.

@jerith
Last active October 29, 2019 15:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jerith/1a1f3b668c37212f6b3476f0761b4c7c to your computer and use it in GitHub Desktop.
Save jerith/1a1f3b668c37212f6b3476f0761b4c7c to your computer and use it in GitHub Desktop.
Why Go is Discrimination

Why Go is Discrimination

Go is a very opinionated programming language. It values “simplicity” above all else, and is intended to “combine the ease of programming of an interpreted, dynamically typed language with the efficiency and safety of a statically typed, compiled language”. These are laudable goals, but unfortunately Go fails to achieve them.

Let’s start with simplicity. While there is no doubt that Go itself is “simple” (for many definitions of the word), code written in Go is anything but. The primitive error handling mechanisms are neither concise nor clear, and the severely limited static type system (especially the lack of generics) makes it impossible to build many useful abstractions cleanly. As a result, Go code tends to contain very long functions that mix many levels of abstraction.

The primary value of a “high-level” dynamically typed language is the ability to express the intention of the functionality cleanly without extraneous implementation detail getting in the way. After all, “programs must be written for people to read, and only incidentally for machines to execute” as Abelson (not Knuth) said. Go fails utterly at this.

The primary values of a “low-level” statically typed language are runtime efficiency and the ability to detect certain kinds of errors before the program actually runs. Go fails utterly at the second, which is arguably the more important.

A Practical Example

Go’s primary error handling mechanism nicely demonstrates both of these failings at once. Here’s a Go program that reads a message from a file, prints it, prints the number of bytes in the message, and handles any errors:

package main

import (
	"fmt"
	"io/ioutil"
	"log"
)

func readAndPrint(filename string) (int, error) {
	data, err := ioutil.ReadFile(filename)
	if err != nil {
		return 0, err
	}
	fmt.Println("Message:", string(data))
	return len(data), nil
}

func main() {
	mlen, err := readAndPrint("message.txt")
	if err != nil {
		log.Fatal("File reading error", err)
	}
	fmt.Println("Message length:", mlen)
}

Only about half the actual code expresses the intended functionality. The rest is boilerplate error handling. Here’s the equivalent in Python:

def read_and_print(filename):
    data = open(filename, "b").read()
    print("Message:", data.decode("utf8"))
    return len(data)

mlen = read_and_print("message.txt")
print("Message length:", mlen)

Much shorter and cleaner, but since all the error handling is implicit it’s not really a fair comparison. Let’s try again in Rust:

use std::io;
use std::fs;

fn read_and_print(filename: &str) -> io::Result<usize> {
    let data = fs::read_to_string(filename)?;
    println!("Message: {}", data);
    Ok(data.as_bytes().len())
}

fn main() {
    let mlen = read_and_print("message.txt").unwrap();
    println!("Message length: {}", mlen);
}

There we go. All the error handling is explicit, but every line of code still clearly expresses the intent of the programmer without any extraneous boilerplate getting in the way.

However! Aside from being clearer, both the Python and Rust implementations have another significant advantage over the Go: it’s impossible to accidentally fail to handle the error. Consider the following variant of the Go readAndPrint function:

func readAndPrint(filename string) (int, error) {
	data, _ := ioutil.ReadFile(filename)
	fmt.Println("Message:", string(data))
	return len(data), nil
}

This is completely valid code that the compiler will happily accept without any warnings. While it’s a little contrived in this case (ReadFile’s caller must accept both return values if it wants either), it’s incredibly easy to forget to check for errors if you don’t actually care about the return value – when writing a file, perhaps. More significantly, nothing stops us using the value of data even though the read failed. In Python, an exception is raised before call to read returns. In Rust, the Result type must be unwrapped (either by pattern matching, an explicit unwrap call (which panics on errors), or the ? operator) before the value can be used. While it’s still possible to ignore errors from calls if you don’t care about the return value, the compilers warns about it (unless explicitly silenced) which means it very seldom happens in practice.

But What Makes This Discrimination?

So far, all we’ve concluded is that Go is a language with some problems. However, the nature and extent of those problems makes it qualitatively different from most other “mainstream” programming languages. To understand why, we need to talk about cognitive styles and gender biases. (The discrimination is much broader than just gender, but that’s where most of the research has been done, and it’s sufficient to make the point.)

The GenderMag project uses personas built on five problem-solving facets to analyse software usability in terms of inclusiveness. In the interests of brevity let’s consider only two of these facets: information processing style, and attitude towards risk. Here are two partial personas:

Abi tends to prefer a comprehensive information processing style (and thus likes to have a complete understanding of a problem before trying to solve it) and is quite risk-averse (solve this problem properly, move on to the next).

Tim, on the other hand, prefers a selective information processing style (and thus gathers just enough information to come up with a potential solution before trying it, gathering more information to try again if it fails) and is more tolerant of risk (just try again later if it doesn’t work out).

Tim quite likes Go. He’s comfortable bouncing up and down between different levels of abstraction, because he’s focused on the details of this function right now and will worry about the rest of the codebase later. He’s happy to tinker with bits of code here and there until his program works. He doesn’t mind missing the occasional bug, because software always has bugs and it’s generally no big deal to find and fix them later.

Abi hates Go. She struggles to design her program’s architecture, because it’s difficult to separate different levels of abstraction. It takes a lot of effort for her to understand the code she reads, because all the low-level error checks get in the way of figuring out what a function actually does. She doesn’t have much confidence that the code she’s written behaves correctly, because it’s so hard to ensure that data structures are always correctly initialized and errors are always correctly handled.

As we can see, Go is significantly more suited to Tim than to Abi. The “simplicity” of the language comes at the cost of excluding features that favour people who don’t think the same way as the designers and the core community. (Performing a similar analysis for other programming languages is left as an exercise for the reader.)

Why Does This Matter?

Here’s the thing: Successful software projects need both Abi and Tim. Neither is “better” than the other, they have different strengths and weaknesses. If Go were just another programming language, it wouldn’t be that big a deal. Unfortunately, it has become the de facto language of modern cloud computing infrastructure. Almost everything in the kubernetes ecosystem is written in Go, which means that Abi is being systematically (if unintentionally) excluded in favour of Tim. Without Abi to cover the weaknesses in his style (as he covers the weaknesses in hers), Tim writes software full of biases he can’t see and bugs he doesn’t notice – the very same software that an ever-increasing amount of critical infrastructure is built on top of.

This makes me sad. Not just because I’m more of an Abi than a Tim in this story. Not just because a large number of the tools and behaviours I rely on to build robust, reliable software don’t work in Go. Not just because I spend so much of my professional life dealing with the same kinds of problems over and over again in a bunch of different systems, all of which are written in Go. It mostly makes me sad because of all the unnecessary new barriers that have sprung up between me and the better world I’m trying to build for tens of millions of people who desperately need so many of the things that you and I don’t even notice we’re taking for granted.

References

@jerith
Copy link
Author

jerith commented Oct 29, 2019

This is a first draft. I have no idea what I want to do with it. Any feedback, thoughts, discussion, or ideas about where to take it would be appreciated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment