There’s a theory called linguistic relativity which postulates that the language you speak modifies your comprehension of the world. That a word does not exist in a language does not mean that the concept itself doesn’t exist; only that it is harder to express, and therefore, a source of potential bias when attempting to describe something.1
I don’t think this hypothesis is taken into consideration often enough when describing programming. There is not a strictly 1:1 correlation between human and programming languages by any means. But when we say “This language is a good choice for the problem,” what does “good” actually mean? Well, usually we mean that a computational idea that needs to be expressed—asynchronous work, memory safety, function composition—can be stated easily. It’s not to say that one language can’t do any of those things, but it does imply that some languages make it harder to do than others.
Go is a language which is able to succinctly express high-performance concepts. This post is not going to delve into some of the negative aspects of Go which have been discussed many times before by early adopters, such as its error handling or its absolutely atrocious lack of appropriate tooling for dependency/package management. I’m also not even going to outright say Go is bad, because it’s not bad (and because I don’t want to scare future employers 😉).
It’s just that it’s only contribution to the world is to be able to quickly write performant code.
“Do what I tell you to do”
I’ll begin by picking on one aspect of Go which attacks you right as you’re trying to learn the language: you can’t write a program which includes unused imports. For example, suppose I have the following code:
package main
import "fmt"
import "math"
func main() {
fmt.Println("hello world")
}
When building this, the compiler will complain with: ./unused.go:4:8: imported and not used: "math"
. There’s no usage of math
anywhere in the running code, so it’s not necessary to import
the math
package. This is a neat way to ensure that your code is not pulling in dependencies it doesn’t need, and it makes sense. You don’t have to worry about this error ever coming up if you configure your editor to run gofmt
(a tool designed to rewrite your code to a standardized formatting) every time you save a file.
Here’s the issue, though. When writing code, sometimes it’s necessary to just try ideas out. If one direction doesn’t work, you revert your changes and try rebuilding from scratch. As a programmer, playing is just as important as perfecting. That the compiler interrupts your thought process here is not only annoying, but also, unnecessary. In any other language, this might show up as a warning, but Go insists that your iterative progress halt, and that you either comment the unused import, or figure out a way to make use of it.
But, as I said–if you run gofmt
automatically, this is no longer a problem. However…that is not always the case. Consider the following code:
package main
import "fmt"
func main() {
var word string
fmt.Println("hello world")
}
At one point, I thought I would need a string variable called word
, but I ended up changing my mind. Again, the compiler will argue with you: word declared and not used
. Only this time, gofmt
does not clean this unused variable. Why is the tool wise enough to know that an import is not being used and helps clear it away, while being too dumb to remove a variable it’s noticed that I’m not using?
What’s the behavior of a program which defines a constant that isn’t used anywhere?
package main
import "fmt"
const (
phrase = "universe"
)
func main() {
fmt.Println("hello world")
}
I introduced a const
named phrase
, but I never use it. But you know what? The compiler doesn’t complain, and gofmt
doesn’t remove it. The program runs just fine.
This level of inconsistency is the root of my frustrations with Go.
“I’m not going to help you”
Here’s another one: Go touts itself as capable of producing memory efficient software. Its language authors come from a C background, and they are all too familiar with the dangers of memory allocation, pointer referencing, and cleanup. And yet, I found it incredibly easy to produce segfaults, without even messing with pointer logic.
Consider this program:
package main
import "fmt"
func main() {
myset := []int{0, 1, 2, 3, 4}
s := myset[0:10]
fmt.Println(s)
}
As soon as you try to run it, it crashes spectacularly:
panic: runtime error: slice bounds out of range
goroutine 1 [running]:
main.main()
/Users/gjtorikian/projects/arr.go:10 +0x6d
exit status 2
myset
only has five elements, and I attempted to access a fictional 10th. Of course this won’t work, but I was still able to compile the program, run it, and hurt myself. Let’s take a look at a similar example in Rust, another language about as old as Go that also brags about its memory efficiency:
fn main() {
let myset: [i32; 5] = [1, 2, 3, 4, 5];
println!("{}", myset[10]);
}
When you attempt to build this program, the compiler responds with:
error: index out of bounds: the len is 5 but the index is 10
--> arr.rs:5:20
|
5 | println!("{}", myset[10]);
| ^^^^^^^^^
|
= note: #[deny(const_err)] on by default
error: aborting due to previous error
This time, the compiler is intelligent enough to notice that you’re attempting to do something undefined. Better still, it specifically points out where in the code the error is, and it gives you specific information (len is 5 but the index is 10
). Now, injecting this kind of intelligence into Go’s compiler is difficult, but Rust shows it is not impossible. Furthermore, when Rust detects a variable isn’t being used, it doesn’t get angry at you (unless you ask it to), and it suggests a workaround:
warning: unused variable: `unused_var`
--> arr.rs:4:9
|
4 | let unused_var: i32;
| ^^^^^^^^^^ help: consider using `_unused_var` instead
|
= note: #[warn(unused_variables)] on by default
Many people believe that Rust is a hard language to learn, and it is. The rustc
compiler is unforgiving in its demands for memory safety. It asks the programmer to think hard about how their program runs, and being able to riff on an idea isn’t easy. But you know what? The compiler is always right. It’s not only right, it’s helpful. It tries to point you to online documentation explaining what problem it’s found, and what you can do to fix it.
A compiler understanding memory management is one thing. A compiler coming back at you and saying obtusely word declared and not used
is an entirely different, much easier problem to solve. A language designer must simply care to see it as a problem. The examples above are trivial, but in a real-world scenario, it’s very easy to get a Go program to crash, with absolutely no insight other than a line number to a file which, crucially, comes after a bunch of hex garbage:
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x14aba17]
goroutine 1 [running]:
github.com/gjtorikian/metrocarddump/metrocarddump.parse.func1(0x1634f80, 0xc0000463c0, 0x1630b20, 0xc0000e02c0, 0x0, 0x0)
/Users/gjtorikian/go/src/github.com/gjtorikian/metrocarddump/metrocarddump/app.go:165 +0x777
github.com/chromedp/chromedp.ActionFunc.Do(0xc0028c6230, 0x1634f80, 0xc0000463c0, 0x1630b20, 0xc0000e02c0, 0x0, 0x0)
/Users/gjtorikian/go/src/github.com/chromedp/chromedp/actions.go:22 +0x4e
github.com/chromedp/chromedp.Tasks.Do(0xc0028a6b40, 0x6, 0x6, 0x1634f80, 0xc0000463c0, 0x1630b20, 0xc0000e02c0, 0xc0001a1400, 0xc0028923c0)
/Users/gjtorikian/go/src/github.com/chromedp/chromedp/actions.go:35 +0x90
github.com/chromedp/chromedp.(*CDP).Run(0xc000116120, 0x1634f80, 0xc0000463c0, 0x1631d40, 0xc0028923e0, 0xc000030100, 0xc002506040)
/Users/gjtorikian/go/src/github.com/chromedp/chromedp/chromedp.go:340 +0x9e
github.com/gjtorikian/metrocarddump/metrocarddump.navigate.func1(0x1634f80, 0xc0000463c0, 0x1630b20, 0xc0000e02c0, 0x0, 0x0)
/Users/gjtorikian/go/src/github.com/gjtorikian/metrocarddump/metrocarddump/app.go:111 +0x194
github.com/chromedp/chromedp.ActionFunc.Do(0xc000134440, 0x1634f80, 0xc0000463c0, 0x1630b20, 0xc0000e02c0, 0x0, 0x0)
/Users/gjtorikian/go/src/github.com/chromedp/chromedp/actions.go:22 +0x4e
github.com/chromedp/chromedp.Tasks.Do(0xc000134480, 0x4, 0x4, 0x1634f80, 0xc0000463c0, 0x1630b20, 0xc0000e02c0, 0xc00006cc00, 0x0)
/Users/gjtorikian/go/src/github.com/chromedp/chromedp/actions.go:35 +0x90
github.com/chromedp/chromedp.(*CDP).Run(0xc000116120, 0x1634f80, 0xc0000463c0, 0x1631d40, 0xc0025060a0, 0xc000134480, 0x4)
/Users/gjtorikian/go/src/github.com/chromedp/chromedp/chromedp.go:340 +0x9e
github.com/gjtorikian/metrocarddump/metrocarddump.glob..func1(0xc0001029a0)
/Users/gjtorikian/go/src/github.com/gjtorikian/metrocarddump/metrocarddump/app.go:66 +0x2e1
github.com/gjtorikian/metrocarddump/metrocarddump.NewApp.func1(0xc0001029a0)
/Users/gjtorikian/go/src/github.com/gjtorikian/metrocarddump/metrocarddump/app.go:41 +0x32
github.com/codegangsta/cli.HandleAction(0x14f8020, 0x15d4188, 0xc0001029a0, 0xc000123920, 0x0)
/Users/gjtorikian/go/src/github.com/codegangsta/cli/app.go:503 +0x7c
github.com/codegangsta/cli.(*App).Run(0xc00012a000, 0xc0000301b0, 0x1, 0x1, 0x0, 0x0)
/Users/gjtorikian/go/src/github.com/codegangsta/cli/app.go:268 +0x5b7
main.main()
/Users/gjtorikian/go/src/github.com/gjtorikian/metrocarddump/metrocarddump.go:13 +0x4f
exit
Every programming language is an attempt to express a concept; the compiler (or linter) is a handbook that double checks your sentiments. When you’re learning a new language, a good teacher corrects your tenses, and guides you towards self-sufficiency. A compiler’s role is to question you, not in order to belittle you, but in order to verify what it is you are truly after.
The Go compiler is a horrible teacher: it tells you that you’re wrong, and then walks away. It’s your fault you made a mistake: figure it out.
I wouldn’t go so far as to say that it’s a level of incompetence, or even laziness, that Go treats its users this way. It’s clearly intentional. I think the problems with the language need to be taken into context as to who designed it, and how.
We forgive JavaScript for its flaws because it was designed in 10 days to combat a monopoly. There’s more than one way to do things in Perl because it was designed by a linguist. Ruby was designed to make programmers happy.
When it comes to language design, what’s Go’s excuse?
“I know better than you”
I promised a hot take, and here it is: a language so overbearing that it won’t let you make what it considers a mistake (while allowing you to make other severe errors) without offering any help could only be developed by a company with such arrogant superiority as to readily employ someone like James Damore. Or, if that’s too bombastic: Go was designed by programmers who don’t care what you think who work at a company that also doesn’t care about you.2 They believe they are in the absolute right, and any crticism to their worldview is wrong. They didn’t write a language to solve your problems, they wrote it to solve theirs. They don’t care about your problems.
I understand why people are writing Go. Its success is intertwined with the overvaluation of a programmer. As every company became a software company, an emphasis on shipping code quickly overtook thoughtful design. That the language has annoyances is secondary to what its value is to a business. Time is money, businesses want to make more money, and therefore a tool that can get a programmer to produce “better” code faster is always going to win over one that is less prone to error, but takes more time to reason about.
When we say Go is “better” than another language, we mean marginally safer. We mean that Go can supersede untyped languages as a quicker way to produce software. With Go, you can produce performant, cross-platform binaries, with little effort, in a fairly straightforward way. In exchange, you have to surrender to its demands.
From the beginning of your very first Go project, you will encounter the GOPATH
, which is a very specific directory where all third-party Go code must reside. This restriction was introduced to make things simpler. The consensus on the Internet is “don’t fight GOPATH
.” Taken independently, it is a good idea; but the more you work with Go, the more you realize that the only way to be successful with any of its concepts is to just work with whatever it tells you. The idea of there being Only One Way to build something sickens me. Use this one browser. Use this one search engine. Use this one programming language. All your problems will disappear if you just submit.
It takes two to make a language: one to speak it, and one to hear it. The German language is well-known for its ability to concatenate words into a single, enormous phrase. Expressing a feeling is hard, and different languages have different words that reflect what their communities and societies value. It seems to me that Go is interested in talking at you, not with you. It’s a one-way language. Either you agree with it, or you don’t. And if you don’t—well, maybe you’re not the kind of person who believes that its authors are just humans, rather than infallible gods. I’m not sure they do either. If they did, they’d be much more receptive to the community’s feedback, if they cared to hear it. More than any other programming language, it’s popular to bash on Go because you know you won’t get anywhere with your critique. While searching for particularly egregious dismissals from the language authors, I found this post by Evan Miller, who already described the problem succinctly several years ago:
[Go] feels like it is designed by an obsessive personality — obsessed with build times in particular, but also having an obsession with detail, someone who rarely makes mistakes when writing code, who generally will not run code until it appears to be complete and correct…Reading Go’s mailing list and documentation, I get a similar sense of refusal-to-engage — the authors are communicative, to be sure, but in a didactic way. They seem tired of hearing people’s ideas, as if they’ve already thought of everything, and the relative success of Go at Google and elsewhere has only led them to turn the volume knob down.
“Because I said so”
Let’s return to the language itself. We know that Go is able to produce performant programs that are simple to write. But how legible are they, really?
Suppose we need to convert an integer to a string. What’s that look like in Go?
package main
import (
"strconv"
"fmt"
)
func main() {
t := strconv.Itoa(123)
fmt.Println(t)
}
Itoa
is such a bizarre name for a method! Where does this come from? Naturally, this is a reference to a method from C. And because “the creators of Go are all deeply embedded in that heritage and feel entirely comfortable with these names,” we should be okay with it, too.
How then does one convert a float? ftoa
, maybe? No, that method is actually called ParseFloat
. A-ha! So the language designers are aware of properly named functions. So why do they insist on retaining some older C references as well? Because they want to. There’s no consistency. Instead, you get someone’s half-baked memories of a way they used to program combined with newer ways of working with data types.
No language is without its problems, of course. When you want to create a new directory in Ruby, you call Dir.mkdir
, which is analogous to the mkdir
Unix command. When you want to execute the equally important mkdir -p
, you instead call FileUtils.mkdir_p
. The inconsistency between the classes used is confusing and worth criticism.
Go’s problem is that it takes its inconsistencies to an esoteric level. Have you ever tried to format a time value into a string in a language? All of the ones I have worked with rely on a format defined by strftime
, another holdover from C. For example, to configure a time value to a full day name, a full month, a full day and a full year, you write this:
Time.now.strftime("%A %B %d, %Y")
=> "Friday January 25, 2019"
Ooooh, remembering those percent-encoded references is so annoying! But many language have simply carried this syntax over, without change.
You might believe that Go would carry over this C-formatting. After all, its authors are C pros, right? Although such an assumption would be sane, it is also wrong. In Go, the exact same time formatting can be expressed like this:
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println(time.Now().Format("Monday January 02, 2006"))
}
The language authors decided that strftime
is a bad interface—which is true. But rather than come up with a more elegant system, they decided that time ought to be formatted using a literal date phrase, rather than other placeholders. If you want the full name of a month, use January
; if you want a shorter version, use Jan
. Is this really an improvement on %B
and %b
?
Nowhere is the disdain of Go’s language authors for its users more perfectly encapsulated than this. Remember: “It’s not buggy and your supposition is wrong.”
Language is imperfect, and that’s okay
I recognize that I’m in a shrinking population of programmers who question the tools and processes around them, a group of people who don’t believe a company whose business is selling all the trackable aspects of your life would also be capable of building an emotive language. Language is about communication, and Google, one of the most networked companies in the world, has historically understood very little about how people actually interact.
Python is another popular language at Google, and Python’s design principle is “There’s only one way to do it.” Needless to say, I’m not much of a fan of Python, either. When a language is reduced to “correct” and “incorrect,” rather than gradients of clarity, the speaker is restricted from freedom of expression. Phrases indisputably devolve into “right” or “wrong,” which is a horrible reduction of a creative endeavor.
I was thinking about an example of this kind of “our way or no way” thought process, and recalled the principles of Newspeak:
Newspeak was designed not to extend but to diminish the range of thought, and this purpose was indirectly assisted by cutting the choice of words down to a minimum…The intention was to make speech, and especially speech on any subject not ideologically neutral, as nearly as possible independent of consciousness. For the purposes of everyday life it was no doubt necessary, or sometimes necessary, to reflect before speaking, but a Party member called upon to make a political or ethical judgement should be able to spray forth the correct opinions as automatically as a machine gun spraying forth bullets.
By eliminating choices, a speaker of Newspeak could unambiguously shoot out sentences. Implementing concepts such as a standardized code style and a single path for dependencies are, by themselves, really good ideas. But taken in the larger context of Go’s design decisions, the implications are chilling. We might begin to understand why Go didn’t come with package management: because it doesn’t want different ways of doing things. The language is not conducive to a community of suggestions and improvements; you ought to be able to get by with what the standard package provides. Compare this mindset with, say, Node’s free-for-all attitude towards Userland. Code can use callbacks or promises, be asynchronous or not, and written in Typescript or Coffeescript. On a macroscopic level, it is confusing, but each individual programmer is free to choose the means of expression that suites them.
With Go, there is no sense of collaboration between yourself and the language. The joy in expressing a thought that satisfies the listener/compiler and also delights the speaker/programmer is not there. Go is a controlled language, the programming equivalent of Newspeak, and your role as a programmer is to use it without any capability to make personal adjustments.
Some might argue that flexibility in programming languages results in complex, unmanageable systems that don’t look like they were written with a single cohesive thought, a problem Go wants to eliminate. Programming languages are meant to accomplish tasks. They are tools, not means of communication. By reducing ambiguity, you can eliminate bugs and become more efficient.
It’s precisely that kind of “efficiency-first” attitude that disinterests me. The speed at which a task can be completed should never be the final metric for one’s skill, either as a programmer or a poet. Go is not infallible; the idea that you can be both fast and correct is an absolute myth. As with every trade-off, you can be one, or the other.
The capacity for diversity is what makes a language great, and what makes a programmer great is their ability to comprehend the rules of their language: which to abide, which to bend, and which to break. Programming is, after all, not just an endeavor between man and machine, but also between humans themselves. If we agree that creative expression is vital to human and programming languages alike, then we need to also accept that perfecting a language of previous errors—a perfection carried out by a small subset of people, rather than a broader community—is antithetical to that development, if not outright dangerous.
-
“Does Your Language Shape How You Think?” is a good summary of all of this. This won’t be a post on linguistics, sadly. ↩
-
If you won’t believe me, then at least believe Steve Yegge. ↩