What made you choose Rust over Go?

I am really torn between Rust and Go. Both have redeeming features, but one of the things that really makes me think I should stick with Rust is Rust's consistent position as developers' most favorite programming language, according to the StackOverflow Developer Survey over the past few years.

So, pardon me in advance for a really generic and opinionated question, but what is it that you like about Rust? What, in particular, made you or your organizations choose it over Go?

3 Likes

Not my opinion, but this was recently on Hacker News:

I have no experience with Go, so no comment from me.

6 Likes

well, there you have it, generics :stuck_out_tongue:

  • No, seriously, I love parametric polymorphism.
  • And ownership with move-by-default, and the lack of shared mutability.
  • And the fact that Rust is an expression language.
  • And the strong type system with extensive type inference.
  • And macros, especially #[derive].
  • And pattern matching.
  • And my ability to control when to allocate.
11 Likes
  • Error handling. In Rust it's easy and robust. You can't not handle an error. In Go it's a burden: if you don't remember to handle an error, it won't be handled. And of course it's if err != nil over and over again.

  • Non-nullable types are great too. I know exactly what's optional, and I know that if my program compiles, it won't blow up on an unexpected nil. In go nil keeps sneaking up on me, especially that structs can't enforce fields to be initialized. Which types can be nil is a mish-mash: I keep wanting nil strings for unspecified data, but nil maps that panic at insert cause me headaches.

  • No resource leaks. Drop guarantees things are always freed. There were times when I forgot to call defer foo.close().

  • Tight control over memory layout and allocation. I work on image processing, so when calling several functions per pixel it matters if they allocate, use dynamic interfaces or not. Go can work with this, but needs extra care, and lack of generics makes it harder to handle e.g. RGB and RGBA each in optimal way.

  • Rust slices and Vec are predictable. append in Go is bizarre. It's neither in-place, nor functional. If you call append(foo, …) it will work until it won't. bar := append(foo, …) looks like it's creating a new slice, but in unfortunate circumstances it may be overwriting beginning of another slice somewhere else!

  • Safe parallelism. Goroutines are easy to spawn, but not easy to guarantee they're correct in larger programs. Channels are nice for data streams, but not data-parallel problems. rayon rocks.

  • Ability to make static libraries. Products I develop end up being linked into various applications from desktop to servers, iOS and Android apps. Go is problematic in some of these environments.

  • For a while Cargo was much better than GOPATH. It's gotten better with go dep, but Cargo still feels more polished.

  • Go is undeniably a smaller and simpler language, but I guess I'm an advanced user, so I'm OK with a bit more complexity for convenience. For example, I love Rust's everything-is-an-expression, enums + match. In Go I feel like I'm stuck in the 1st gear.

28 Likes

I find that, while both languages are good implementations of their goals, those goals seem to largely be opposites and I -- personally -- like Rust's goals and dislike Go's goals.

Go is based around simplicity, so that it's easy and fast to compile and that any given line is straight-forward and that you can start writing it after minimal learning, at the cost of limited capabilities for abstraction such that the duplication means one can easily lose the forest in the trees.

Rust instead gives great power to create efficient and safe abstractions, at the cost that it can take a while to learn and when used poorly one can create something concise but quite difficult to understand and slow to compile.

I can't pick between those two worldviews for you, but they're different enough that hopefully one of them jumps out as more "you".

(I also feel like Go would have been a great language as a competitor to Java 1 back in 1996. These days it feels dated to me, however -- for example we now know from Java and C# that, yes, you should have generics, and if you don't you end up adding them later but still have warts from their absence -- but that's a more personal stylistic impression than the things above that I think are more objectively true.)

Edit: At work (non-Rust) I just found a bug that turned out to be yet another instance of someone using a non-concurrent HashMap in multiple threads at once, and it caused failures in the service for a real user after passing all the tests. Not having to worry about that in Rust is incredible.

10 Likes

The difference for me was like going from an automatic road car to a manual race car. You get so much more out of the car, but every now and again you'll have to work for it. On the other hand, squeezing that kind of performance out of the road car isn't even possible.

So whether you should choose Go or Rust depends: are you a commuter or a race car driver?

There is also another aspect to me starting to use Rust: while garbage collection is generally considered a good thing, I find myself rather biased against it (it often enough causes pathological programs when you need to scale up, it doesn't manage resources other than RAM, and you give up runtime performance predictability) so given an alternative where I can have my memory safety without runtime overhead, I'll choose that.
Of course I was in a position to make that choice in the first place, which not everyone is.

Finally there are features that are now turning out to be crucial to what I'm doing e.g. procedural macros, a very strong type system, enums and pattern matching.
Go is not developed in these areas at all.

6 Likes

I've been writing Go ~daily since before 1.0 came out. I've been writing Rust ~daily also since before its 1.0 release. I still do. I primarily write Go at work and Rust in my free time these days, although I sometimes write Rust at work and sometimes write Go in my free time.

Go's goals actually deeply resonate with me. I very much appreciate its fast compilation times, opinionated and reasonably orthogonal design and the simplicity of the overall tooling and language. "simplicity" is an overloaded term that folks love to get pedantic about, so I'll just say that I'm using it in this context to refer to the number of different language constructs one needs to understand, and invariably, how long it takes (or how difficult it is) for someone to start becoming productive in the language.

I actually try hard to blend some of those goals that I find appealing into Rust code that I write. It can be very difficult at times. In general, I do my best to avoid writing generic code unless it's well motivated, or manifests in a way that represents a common pattern among all Rust code. My personal belief is that Go's lack of generics lends considerably to its simplicity. Writing in Go code heavily biases towards less generic code, and thus, more purpose driven code and less YAGNI code. I don't mean this to be a flippant comment; the best of us succumb to writing YAGNI code.

So if I like Go's goals so much, why do I use Rust? I think I can boil it down to two primary things.

The first is obvious: performance and control. The projects I tend to take on in my free time bias towards libraries or applications that materially benefit from as much performance tuning as you want. Go has limits here that just cannot be breached at all, or cannot be breached without sacrificing something meaningful (such as code complexity). GC is certainly part of this, and not just the GC itself, but the affects that GC has on the rest of your code, such as memory barriers. Go just makes it too hard to get optimal codegen in too many cases. And this is why performance critical routines in Go's standard library are written in Assembly. I don't mind writing Assembly when I have to, but I don't want to do it as frequently as I would have to in Go. In Rust, I don't have to.

The second reason is harder to express, but the most succinct way I can put it is this: Go punishes you more than Rust does when you try to encapsulate things. This is a nuanced view that is hard to appreciate if you haven't used both languages in anger. The principle problems with Go echo a lot of the lower effort criticism of the language. My goal here is to tie them to meaningful problems that I hit in practice. But in summary:

  • The lack of parametric polymorphism in Go makes it hard to build reusable abstractions, even when those abstractions don't add too much complexity to the code. The one I miss the most here is probably Option<T>. In Go, one often uses *T instead as a work-around, but this isn't always desirable or convenient.
  • The lack of a first class iteration protocol. In Rust, the for loop works with anything that implements IntoIterator. You can define your own types that define their own iteration. Go has no such thing. Instead, its for loop is only defined to work on a limited set of built-in types. Go does have conventions for defining iterators, but there is no protocol and they cannot use the for loop.
  • Default values. I have a love-hate relationship with default values. On the one hand, they give Go its character and make a lot of things convenient. But on the other hand, they defeat attempts at encapsulation. They also make other things annoying, such as preventing compilation errors in struct literals when a new field is added. (You can avoid this by using positional struct literal syntax, but nobody wants to do that because of the huge sacrifice in readability.)

So how do these things hamper encapsulation? The first two are pretty easy to exemplify. Consider what happens if you want to define your own map type in Go. Hell, it doesn't even have to be generic. But maybe you want to enforce some invariant about it, or store some extra data with it. e.g., Perhaps you want to build an ordered map using generics. Or for the non-generics case, maybe you want to build a map that is only permitted to store certain keys. Either way you slice it, this map is going to be a second class citizen to Go's normal map type:

  • You can't reuse the mymap[key] syntax.
  • You can't reuse the for key, value := range mymap { construct.
  • You can't reuse the value, ok := mymap[key] syntax.

Instead, you wind up needing to define methods for all of these things. It's not a huge deal, but now your map looks different from most other maps in your program. Even Go's standard library sync.Map suffers from this. The icing on the cake is that it's not type safe because it achieves generics by using the equivalent of Rust's Any type and forcing callers to perform reflection/type conversions.

Default values are a completely different beast. Their presence basically makes it impossible to reason conclusively about the invariants of a type. Say for example you define an exported struct with hidden member fields:

type Foo struct {
    // assume there are important invariants
    // that relate these three values
    a, b, c int
}

func NewFoo() Foo { ... }

func (f *Foo) DoSomething() { ... }

Naively, you might assume that the only way to build Foo is with NewFoo. And that the only way to mutate its data is by calling methods on Foo such as DoSomething. But this isn't quite the full story, since callers can write this:

var foo Foo

Which means the Foo type now exists as a default value where all of its component members are also default values. This in turn implies that any invariant you might want to come up with for the data inside Foo must account for that component's default values. In many cases, this isn't a huge deal, but it can be a real pain. To the point where I often feel punished for trying to hide data.

You can get a reprieve from this by using an unexported type, but that type cannot appear anywhere (recursively) in an exported type. At least in that case, you can be guaranteed to see all such constructions of that type in the same package.

None of these things are problems in Rust. YMMV over how much you consider the above things to be problems, but I've personally found them to be the things that annoy me the most in Go. An honorary mention is of course sum types, and in particular, exhaustive checks on case analysis. I've actually tried to solve this problem to some extent, but it's not ideal for a number of reasons because of how heavyweight it is: https://github.com/BurntSushi/go-sumtype --- From my perspective, the lack of sum types just means you can move fewer invariants into the type system. For me personally, sum types are a huge part of my day-to-day Rust coding and greatly lend to its readability. Being able to explicitly enumerate the mutually exclusive states of your data is a huge boon.

Anyway, Rust has downsides too. But I'm tired of typing. My main pain points with Rust are: 1) complexity of language features, 2) churn of language/library evolution and 3) compile times. Go suffers from basically none of those things, which is pretty nice.

52 Likes

I tried Go before trying Rust. I heard Go was a good language for build web servers so I tried it out. After doing a couple of simple tutorials, I tried to solve an easy problem that I do all the time in Node. That was concurrently waiting for a batch of requests and collecting the results. I actually found this to be not straightforward. I eventually got it to work after a long time. Then I learned that without generics I couldn't reuse my code that worked. I would have to memorize the pattern and retype it from memory or copy and paste. Both of these are error prone. After that I never looked back at Go.

On the other hand, I have found learning Rust to be a joy. The compiler helps me solve bugs before I run my code. The standard library documentation is usually so well written that it can actually teach me how something is implemented and why it is important. And I actually find Rust to be more expressive than Javascript which was my favorite language before I got into Rust. This is mostly down to the fact that types typically have more methods and they are generally more useful as well. And macros. Oh my god, macros. They are so useful when a library is able to use them effectively. Honestly, I would like Typescript 10x more if it had macros.

11 Likes

There is a document on the fuchsia project that describes their policies for accepted languages and their analysis of the pros and cons of each language.

Disclaimer: Don't expect a very detailed analysis.

1 Like

I am old enough and have had to learn and use enough languages over the years that some years ago I promised myself never to waste time learning YAFL (Yet Another F'g Language). Looking back it seemed like unnecessarily having to learn new syntax, new libraries and frame works, new methods just so one could get the same old stuff done.

That promise got broken pretty soon as I found myself having to get into Javascript to get anything done in the browser, real-time interaction with web sockets, 3D visualization with THREE,js/Babylon.js. Happily I found JS is a pretty sophisticated language and soon adopted it for server side stuff with node.js, also in some embedded systems. The async/event driven programming model of JS is a breeze compared to the bad old days of threads in C/C++ etc and performs very well. I was happy to have become familiar with JS.

Round about that time I also did some experiments in Go, for those server side processes I mentioned above. Immediately I found the problem with garbage collection. My test servers were stuttering and stalling. The last thing I needed for my real-time visualizations. Amazingly my equivalent code in JS performed better in that respect so node.js it was. People have said that the GC situation is much improved in Go since then and it is much less of a problem on 64 bit machine. I never tried again but from what I read now a days it is still an issue.

Time passes and I find myself getting into Verilog and Scala for logic design in FPGA, only hobby stuff mind. So when I got hint of Rust I was warmed up to breaking my promise yet again...

What pushed me over the edge was a presentation on Rust by Bryan Cantrill, I think it was this one: "The Summer of RUST" https://www.youtube.com/watch?v=LjFM8vw3pbU&t=46s. Bryan has my utmost respect as an old time C programmer and Unix wiz, so I was moved to have a look into this new Rust thing.

Wow, finally a language with a compelling new feature, memory safety without garbage collection or run time. A language that I could potentially use for my server side stuff and in embedded systems and in the browser thanks to WASM. A language that fixes all the problems of my beloved C without the bind boggling complexity of C++ whilst maintaining performance and determinism.

Pretty soon all new development in our new little company was being done in Rust. Amazingly getting going with a web server, NATs messaging, our database etc is not much harder to do in Rust than node.js. Everything has been running fine for some months now. It's nice not to have to worry about those unexpected memory and type errors.

It's not all a bed of roses though. You see, I write my Rust as if it were C. Until such time as the compiler tells me I cannot and clippy gets me to clean up after myself. But there is still a whole world of Rust that boggles my mind. The emphasis on "Functional Programming" style is part of it, macros is another. That means there is a lot of discussion in this forum and about Rust elsewhere that leaves me cold. Perhaps I'll get the hang of it eventually.

The other dark cloud on the horizon is Python. Turns out if you want to get into AI/Machine Leaning etc with Tensorflow and the like all the turorials, discussions, docs, use Python (and C++ of course but I don't want to go backwards). That is perhaps a whole other story.

As for Go, well I never did get to look at recent Go. I'm into the idea of being able to use the same language from embedded/IoT gadgets to the cloud and anywhere else. As such Go is off the table for me.

My apologies for the long ramble above.

11 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.