Thompson and Pike are living legends. They had absolutely nothing left to prove. Yet they came together for another major project, backed by the most powerful technology corporation in the world, and they produced... this?
Go feels like a random hodgepodge of ideas from three or four computer science undergraduates – not like the creation of engineering superstars with a century of experience between them.
And in hindsight and after 15 years of cleaning up the mess we know that they just needlessly made a generation of developers suffer through primitive abstractions.
And Java is not a badly designed language at all, it did make several tradeoffs at places for various reasons, but it had a clear design goal and I think it did live up to it.
The most glaring issues were fixed in Java5 (similar to how golang just had their generics story improved). It took us all the way to Java8 to have a semi-pleasant language. I'm still waiting to see if Golang's generics are good enough to make abstractions/monadic operations similar to the Java's Streams widespread. If memory serves, basic pattern matching is coming in Java20, only 12+ years after Scala.
In contract to, say, virtual threads, a lot of these are language-level features. So they could have been done _much_ earlier. A generation of developers had their taste for abstractions skewed by the early days Java. Doing the job of the compiler in their heads. Now the next generation finds itself in the same situation with Golang.
I think this slow moving is really an advantage for Java, such a widely used language can’t allow breaking changes willy-nilly so any language feature will effectively become written in stone. Also, surprisingly many things can be expressed purely by “objects with method” APIs (Steele’s growing a language)
A new language on the other hand could have actually iterated fast because relatively few programs are written for them in the early years, go is just not a good language.
How nebulous it is is really well demonstrated in Pike's talk[0] where he somehow believes that a for loop is "simpler" than methods like "filter" and "map", and makes the very strange and incorrect claim that it's going to be faster as well (I suspect he was thinking "in Go" because those would be slower, given a lack of generics).
A lot of Go's design philosophy is, very bluntly, directed to bad programmers. For example, Pike talks about how a developer using a language with many features might spend 30 minutes just iterating on a few lines of code, because there are so many features available to use within that code. That is a very junior level problem - junior level programmers aren't very good at understanding that there's low return on investment for that sort of work so they coo over fancy code that works exactly as well as boring code.
Similarly, junior developers have a hard time learning features, so they have a hard time reading code that uses them and Go puts readability above all else. A senior developer should surely hear "filter and map are complicated?" and scoff but a junior developer would go "I have no idea what those are".
And there are significant constraints because of that lack of features - years of not having generics because it's too complex, for example.
We need junior developers to be able to approach new code but we also need to give powerful tools to the engineers who understand how to use them properly, especially when they're needed most.
This is why I think that Go is a bad language, but not just because it's missing some feature X or Y or Z, but because it is effectively an attempt to create simplicity by saying "you can't handle complexity". I also think it fails in its goals right off the bat because of goroutines and having some basic footguns, but that's not worth getting into.
What I think is good is that we can watch how a language built with this philosophy progresses over time. For one thing, it inevitably takes on "complexity" (generics), which I think is notable. At some point those people who love that they can jump in and know everything do actually run into the cases where those features are higher ROI (ie: writing higher performance libraries with nice APIs requires generics) and there are enough of them that they can push for those changes.
It's also interesting to watch a language grow with such a custom toolchain. Go has its own compiler, which makes a lot of sense for some of their goals, but it has also meant that they've had to reimplement (or just have not implemented) lots of optimizations.
To be clear, in case I've somehow insulted someone who writes Go, if you like Go I'm not saying "you are a bad developer", I'm saying that Go was designed for bad developers. I know lots of very good developers who like Go.
Just a trivial example: trying to use append(x, y) without re-assigning to x? Congrats, it will compile without warnings, work fine for a while, then at one point append will reallocate and bang, have fun letting the newbie debug that on a Friday evening.
[~] $ cat main.go
package main
import "fmt"
func main() {
x := []int{1, 2}
append(x, 3)
fmt.Println(x)
}
[~] $ go run main.go
# command-line-arguments
./main.go:7:2: append(x, 3) (value of type []int) is not used
and if you assign the result of the append to something else, x is unchanged.No one wants that in Go, please stop trying to shove it down like it’s THE solution.
A lot of Go users are fine with the way error currently work, because they’re considered as values, and their handling don’t break the flow like exceptions do.
Sure, Results would have been better, but this ship has sailed. I’d settle for allowing inline « if » expressions, but besides that, error handling in Go is fine.
Did you worked in Java? Did you worked with async and exceptions?
Java, C#, hell, even python are better designed languages than Go.
Enumerated Types > Checked Exceptions > Error values > Unchecked Exceptions.
The spectrum I'm ranking on is how much assistance the compiler gives me in ensuring I am aware of the error. Java has support for the second best option but culturally avoids it like the plague. Go has support for the third best option and culturally leans hard into it. C# and Python are ranked strictly at the bottom.
The preference in many language communities for an error handling path that encourages little awareness of the errors that can occur reeks of laziness to me. Laziness that I will, without a doubt, fall victim too without superhuman amounts of diligence on my part to avoid.
Any problem that requires superhuman diligence just screams out for a robot to handle in my experience. Java, C#, Python and a myriad of other languages fail hard here. Go gets a little better culturally by focusing on checkers that are built into the compiler toolchain with a default to running culturally as well as having type a signature for return values clearly indicate the error. But the gold standard for error handling has to go to any language that has enumerated types. Rust, ML variants such as OCaml or Haskell as common examples.
And golang has them in the form of panics and they suck in goroutines.
If I want to read a json file, I have to have a named variable for the file, one for the byte string, and finally one for the parsed object (the only thing I care about).
Then for each statement I have to check if an error happened and return it if it did.
Whereas in something like Haskell I could use a Maybe and compose the functions to only forward the value at each step if it was present. Then at the end I would have a Maybe with the final value, which I could then return to the caller.
Golang makes the most common thing to do as verbose as the least common thing.
This was a golden opportunity to implement some typical form of exception handling. Instead you have to bundle your errors together and then unwrap them (often using multiple methods to handle the cases of both a single error and a bundle of errors) and then still implement even more logic to walk the tree you made?