Why I think Go is a Terrible Language | Blog
Index<br>Projects Posts About
↑ #Why I think Go is a Terrible Language<br>Published April 26, 2026 28 min read Source
thoughtsprogrammingsoftware
Table of Contents<br>I’ll start with a disclaimer that I know will bother some of you. Maybe bother<br>you a lot, or maybe make you laugh: this is not a screed from someone who<br>doesn’t understand Go. You wish it were, and frankly, I wish it was.<br>Unfortunately for me, I have written Go in the past and I have also shipped Go<br>to “prod” in the past. In fact, I continue to write Go when I’m too lazy or<br>tired to care about correctness—for reasons we’ll talk about shortly. It is<br>also worth stating out right that while I am still in the process of converting<br>to more… sane languages, some of my most critical infrastructure components<br>have been Go for as long as I can remember. They are still Go, and they still<br>hold.
This post has started as a Hedgedoc document I uploaded in response to questions<br>asking me why I think Go is designed by people who understand language design<br>very well, but refuse to follow established and beloved conventions. I am still<br>behind this statement and throughout this post you will see exactly why I<br>believe this. I have read much of the spec and the proposals. I have read, or at<br>least tried to read, the team’s stated reasoning and surrounding memos. The blog<br>posts about why the things are the way they are and the such. Their defenses,<br>arguments, and more.
What follows is a case. It is my case about why I think Go is a terrible*<br>language. This is, put most simply, my attempt at a precise, specific, and<br>deliberately uncharitable document where the language deserves what is coming.<br>It is about why Go is a failure of a design philosophy dressed up and paraded<br>around as pragmatism.
It is my pleasure to say that this is not one for beginners who seek<br>reassurance. This is also not for experts who are extremely comfortable with<br>their language. My goal is not changing your mind, but making my case out in the<br>open. This is also not a hate mail towards Go, but a strongly worded opinion<br>essay. I intend to write something truly wholesome for Rust in the future, and<br>one truly vile for Zig. For now, let’s talk about Go and its design choices.
Failure by Design
The first thing I want to talk about, and perhaps the lowest hanging fruit that<br>everyone reaches for, is the error handling patterns of Go. This is simply<br>because it really is that bad. I think the designers fully understood<br>exceptions, sum types, and structured error propagation. Quite sure they not<br>only understand those patterns, but also see the individual beauty of it. They<br>knew about Haskell’s Either, about ML’s type-safe exceptions and Java’s<br>checked exceptions which are flawed indeed but at least attempt correctness.<br>Instead, Go enforces explicit error returns everywhere, even when the result is<br>repetitive boilerplate that actively obscures control flow. Here, let me give<br>you a better idea. A typical Go function doing three fallible operations would<br>look something like this:
func process(path string) error {
f, err := os.Open(path)
if err != nil {
return err
data, err := io.ReadAll(f)
if err != nil {
return err
result, err := parse(data)
if err != nil {
return err
return store(result)
The actual logic (open, read, parse, and store) is four lines. FOUR. The error<br>plumbing, however, is twelve. The signal-to-noise ratio is, if my math is<br>correct, 3:1, and this is called explicit. Now compare it to Rust, where ?<br>propagates errors with identical semantics but doesn’t consume your screen free<br>estate.
fn process(path: &str) -> ResultError> {
let data = fs::read_to_string(path)?;
let result = parse(&data)?;
store(result)
How many lines is that? 1, 2, 3… Oh yeah, FOUR.
The crucial difference isn’t syntax sugar my dear reader. In Rust,<br>Result is a real type. E is a real type parameter. You can write<br>generic combinators over it, map_err, and_then, unwrap_or_else. The error<br>type is part of the function’s contract in the type system, not a convention you<br>hold in your head.
In Go, error is a single-method interface. Any type with Error() string<br>satisfies it, which means there is no structured hierarchy, no enforcement and<br>absolutely no static knowledge of what errors a function can actually produce.<br>Somewhere around Go 1.12 or 1.13, the Go team added errors.Is and errors.As<br>as a post-hoc attempt to recover structure from this mess. This is simply an<br>admission of guilt. They rejected typed errors, and then recreated a weaker,<br>ad-hoc version of the same concept without proper exhaustiveness, without<br>enforced structure and without syntax support.
io.EOF is the clearest and perhaps the best illustration of where the model<br>collapses. It is a sentinel value. A global error variable callers check with<br>==. Entire packages special-case it. In a language where Error is a proper<br>enum, EOF would be just another variant the compiler forces you to handle but in<br>Go it...