The success of Go heralds that of Rust
The success of Go heralds that of Rust
Looking at Go from a bird’s eye view, it’s quite hard to understand its tremendous success.
Go is, from a theoretical perspective, a very bad language, even when compared to an old language like C++ or Ada.
It lacks a lot of compile time checks and compile time logic. Its lack of destruction logic leads to a lot of boilerplate and runtime errors. Its interfaces aren’t very expressive. It has first-class-citizen data structures (slices and maps) that can’t be replicated as a library using the language itself. It forces mutability upon the user in many situations where mutability isn’t needed. It comes with a pseudo dependency manager that lacks independent versioning for separate projects. It’s very slow compared to most other popular system programming language, namely C, C++, Ada, Fortran and Rust.
And that’s just the stuff I can think of right now and easily lay out for you. Once you dig deeper into Go, you’ll find it makes some inherently flawed design choices, it’s a language built for 1980, not 2020.
In spite of that, as anyone who used Go can tell you, it’s a very good language. I count myself among those people, if I were to be stranded on an island with only 3 programming languages, I’d want Go to be one of them.
In spite of all of it flaws, it allows you to write relatively error free code that performs well. Adding a dependency to a Go program is usually much smoother of a process than adding a dependency to a C++ program.
Why Go is good
This leaves me in a very weird position. On the one hand, I could talk for hours about how horrible Go is. On the other hand, Go is obviously a very good language.
To figure out why this is true, we need to go back to one of those cliche analogies of languages as tools and programmers as problem solvers.
A lot of the modern world’s problems seem to revolve around efficient network communication, around safely making use of all your hardware threads and around easier development and deployment.
Finally, there’s been a steady trend of good open source libraries trickling into our lives, most of them being concise and simple, fit for a single purpose. Most Node or Python projects have hundreds of such dependencies, whilst most C and C++ codebases have under a dozen. C and C++ lack any standardized package management, so libraries tend to be all-encompassing monoliths (see QT and Boost), since adding new dependencies is time consuming.
Open source libraries are an important part of a modern developer’s life, but all popular system programming language lacked package management.
From this perspective, Go had a few central features, which are so amazing, they out-weighted all the bad.
- Go utilities allow you to easily download and use packages.
- Static compilation makes porting code between various environments and setting up a development environment really easy.
- Native asynchronous I/O mechanisms allow you to easily write high performance networking code.
- Built-in channels allow for easy to implement and relatively safe data transfer between [g|c]oroutines.
- The standard library and package ecosystem contains most libraries a developer could ask for.
- It’s “fast enough” for almost all usecases. Seemingly hitting a sweet spot between easy-to-use single threaded language like Python and Node and blow-your-brains-out ancient but fast behemoths like C++ and C.
Or, to put it plainly. Go is a language designed for the age of open source libraries, large-scale parallelism and networking.
Every other popular language lacks in one of those three categories.
All the remaining issues with Go stem from three design choices:
- It’s garbage collected, rather than having compile time defined lifetimes for all its resources. This harms performance, removes useful concepts (like move semantics and destructors) and makes compile-time error checking less powerful.
- It lacks immutability for all but a few (native) types.
- It lacks generics
Fix those things, and Go essentially becomes the language of the future. But, due to various problems, some related to prior design decisions and some related to the designers’ opinions, those issue will remain mostly unsolved.
For example, generics are likely coming to go 2.0, but the current implementation overlaps with other features (e.g. Interfaces) and manages to be both annoying to use and lacking in features (e.g. not usable as return types).
Or, we could find a language that “checks” all the right boxes, but doesn’t suffer from those poor design decisions.
Enter Rust
Rust just happens to be a language that is well known for acing all the things that Go can’t do.
Due to its implicit move semantics and borrow checker, it ends up being on the safest, fastest and easiest to reason about language in terms of resource management. Catching a host of errors at compile time.
The combination on templates and traits give it compile time programming capabilities close to those of C++, and often better.
Finally, Cargo is one of the best package management systems out there. Allowing easy access to a host of useful libraries, with build-in versioning and complete separation for every individual project.
In other words, Rust succeeds with top grades where Go utterly fails.
But what about the things that Go does oh-so-right ?
go get
andgo mod
are well matched by Cargo.cargo build
uses the static compilation route by default much the same waygo build
does.- This is the sticking point, Rust doesn’t yet have a native async i/o mechanism. But said mechanism is coming soon~ish. It won’t be quite as seamless as that provided by goroutines, but it will be similar with the more well-known syntax used in Node and Python.
- Not only does Rust have native support for channels between threads, it also has a robust compile time thread-safety checking mechanisms. Which means there’s no risk of concurrency errors no matter what data sharing mechanism you chose to use
- The standard library of Rust is just as rich as that of go, except for (again), some missing asynchronous networking functionality. The package ecosystem of Rust, at this point, matches that of Go in many aspects. Despite Rust being the far younger language.
- Finally, in terms of performance, Rust is a tad bit slower than C and C++ in various scenarios. But its design choices means this is only a “temporary” setback. Go is burdened with its lack of compile time logic and GC based design forever. Thus, the already huge performance gap between Go and Rust will only get bigger.
The Go effect
Once the async/await is merged into the stable release of Rust, I think we could call Rust a superior language to Go in, quite literally, every single way possible.
I don’t think people quite realize the amount of testing, debugging and crashes Rust saves them from up until they push their code into production.
The more Rust and Rust related concepts enter the programming world, the more familiar with its syntax and concepts become. The lower the barrier to entry gets.
I think Rust has passed the “critical” period in its lifetime, it has wide enough adoption to be a mainstay on the programming stage. Further more, it’s almost bullet proof when compared side by side to literally any other programming language. It’s simply a better language than any other for most use cases.
But inertia is not to be trifled with, the largest roadblock for widespread Rust adoption is convincing developers to switch.
Here, I think the reasons why Go’s popularity skyrocketed, will apply to Rust. The language is getting closer and closer to a sweet-spot.
It will be able to please C&C++ devs that want a language suited for the “modern” style of programming.
At the same time, it will be the best choice for people that want to switch from a scripting language to a system programming language, giving them safety and performance, while allowing them to still have package management and a familiar syntax.