1. 30

  2. 4

    I’ve written some Go and some Rust. I feel like I usually enjoy Rust more, though I struggle to explain why.

    I think, for Rust, I find the error handling really ergonomic. Using ? in a function that does a bunch of things that can fail is just so much nicer than having every other line be a if err == nil { return err }. I also find it easier to follow how references work in Rust, oddly enough maybe. And using modules through Cargo is just so nice, while Go modules is kind of a messy hack in comparison. Oh and the macros are just so nice too.

    But on Go’s side, Go concurrency is really awesome and smooth, especially compared to the half-complete hacks that are tokio and the Rust async system. Did I mention how nice the built-in channels are, and how a bunch of places in the standard lib use them? And easy cross-compilation is pretty nice too. And you gotta love that massive standard library. And I suppose not having to wrestle with complex too-clever generic hierarchies is nice sometimes too.

    1. 16

      side-note: i think it’s a bit off-topic (and meme-y, rust strike force, etc. :) to compare to rust when the article only speaks of go :)

      Using ? in a function that does a bunch of things that can fail is just so much nicer than having every other line be a if err == nil { return err }.

      i really like the explicit error handling in go and that there usually is only one control flow (if we ignore “recover”). i guess that’s my favorite go-feature: i don’t have to think hard about things when i read them. it’s a bit verbose, but that’s a trade-off i’m happy to make.

      1. 7

        i really like the explicit error handling in go

        I would argue that Go’s model of error handling is a lot less explicit than Rust’s - even if Go’s is more verbose and perhaps visually noticeable, Rust forces you to handle errors in a way that Go doesn’t.

        1. 1

          I have just read up un rusts error handling, it seems to be rather simila, except that return types and errors are put together as “result”: https://doc.rust-lang.org/book/ch09-00-error-handling.html

          my two cents: i like that i’m not forced to do things in go, but missing error handling sticks out as it is unusual to just drop errors.

          1. 4

            Well since it’s a result, you have to manually unwrap it before you can access the value, and that forces you to handle the error. In Go, you can forget to check err for nil, and unless err goes unused in that scope, you’ll end up using the zero value instead of handling the error.

            1. 1

              i like that i’m not forced to do things in go, but missing error handling sticks out as it is unusual to just drop errors

              The thing is, while it may be unusual in Go, it’s impossible to “just drop errors” in Rust. It’s easy to unwrap them explicitly if needed, but that’s exactly my point: it’s very explicit.

          2. 3

            The explicit error handling is Very Visible, and thus it sticks out like a sore thumb when it’s missing. This usually results in better code quality in my experience.

            1. 2

              It did occur to me that it may come off like that :D It’s harder to make interesting statements about a language without comparing it to its peers.

              IMO, Rust and Go being rather different languages with different trade-offs that are competing for about the same space almost invites comparisons between them. Kind of like how temping it is to write comparisons between Ruby, Python, and Javascript.

              1. 1

                I think Swift fits in quite well in-between. Automatic reference counting, so little need to babysit lifetimes, while using a powerful ML-like type system in modernised C-like syntax.

            2. 15

              But on Go’s side, Go concurrency is really awesome and smooth

              Concurrency is an area I feel Go really lets the programmer down. There is a simple rule for safe concurrent programming: No object should be both mutable and shared between concurrent execution contexts at the same time. Rust is not perfect here, but it uses the unique ownership model and the send trait to explicitly transfer ownership between threads so you can pass mutable objects around, and the sync trait for safe-to-share things. The only safe things to share in safe rust are immutable objects. You can make other things adopt the sync trait if you’re willing to write unsafe Rust, but at least you’re signposted that here be dragons. For example, the ARC trait in Rust (for atomic reference counting), which gives you a load of read-only shared references to an object and the ability to create a mutable reference if there are no other outstanding references.

              In contrast, when I send an object down a channel in Go, I still have a pointer to it. The type system gives me nothing to help avoid accidentally aliasing an object between two threads. To make things worse, the Go memory model is relaxed consistency atomic, so you’re basically screwed if you do this. To make things even worse, core bits of the language semantics rely on the programmer not doing this. For example, if you have a slice that is in an object that is shared between two goroutines, both can racily update it. The slice contains a base and a length and so you can see tearing: the length from one slice and the base from another. Now you can copy it, dereference it and read or write past the end of an array. This is without using anything in the unsafe package: you can violate memory safety (let alone type safety) purely in ‘safe’ Go, without doing anything that the language helps you avoid.

              I wrote a book about Go for people who know other languages. It didn’t sell very well, in part because it ended up being a long description of things that Go does worse than other languages.

              1. 2

                That’s a worthwhile point. I haven’t been bitten by the ability to write to Go object that have already been sent down a channel yet, but I haven’t worked on any large-scale long-term Go projects. I’ve found it straightforward enough to just not use objects after sending. But then, the reason why we build these fancy type systems with such constraints is that even the best developers have proved to be not very good at consistently obeying these limits on large-scale projects.

                I’m hoping that the Rust issues with async and tokio are more like teething pains for new tech than a fundamental issue, and that eventually, it will have concurrency tools that are both as ergonomic as Go’s and use Rust’s thread safety rules.

                1. 5

                  I’ve found it straightforward enough to just not use objects after sending.

                  This is easy if the object is not aliased, but that requires you to have the discipline of linear ownership before you get near the point that sends the object, or to only ever send objects allocated near the sending point. Again, the Go type system doesn’t help at all here, it lets you create arbitrary object graphs with N pointers to an object and then send the object. The (safe) Rust type system doesn’t let you create arbitrary object graphs and then gives strong guarantees on what is safe to send. The Verona type system is explicitly designed to allow you to create arbitrary (mutable or immutable) object graphs and send them safely.

              2. 9

                And using modules through Cargo is just so nice, while Go modules is kind of a messy hack in comparison.

                I have always found Rust’s module system completely impenetrable. I just can’t build a mental model of it that works for me. I always end up just putting keywords and super:: or whatever in front in various combinations until it happens to work. It reminds me of how I tried to get C programmes to compile when I was a little kid: put more and more & or * in front of expressions until it works.

                And of course they changed in Rust 2018 as well which makes it all the more confusing.

                1. 3

                  Yeah, I’ve had the same experience. Everything else about Cargo is really nice, but modules appear to be needlessly complicated. I have since been told that they are complicated because they allow you to move your files around in whatever crazy way you prefer without having to update imports. Personally I don’t think this is a sane design decision. Move your files, find/replace, move on.

                  1. 2

                    And of course they changed in Rust 2018 as well which makes it all the more confusing.

                    One of the things they changed in Rust 2018, FYI, was the module system, in order to make it a lot more straightforward. Have you had the same problem since Rust 2018 came out?

                  2. 6

                    For me Go is the continuation of C with some added features like CSP. Rust is/was heavily influenced by the ML type of languages which is extremely nice. I think ML group is superior in my ways to the C group. ADTs are the most trivial example why.

                    1. 4

                      I generally agree. I like ML languages in theory and Rust in particular, but Rust and Go aren’t in the same ballpark with respect to developer productivity. Rust goes to impressive lengths to make statically-managed memory user-friendly, but it’s not possible to compete with GC. It needs to make up the difference in other areas, and it does make up some of the difference in areas like error handling (?, enums, macros, etc and this is still improving all the time), IDE support (rust-analyzer has been amazing for me so far), and compiler error messages, but it’s not yet enough to get into a competitive range IMO. That said, Rust progresses at a remarkable pace, so perhaps we will see it get there in the next few years. For now, however, I like programming in Rust–it satisfies my innate preference to spend more time building something that is really fast, really abstract, and really correct–but when I need to do quality work in a short time frame in real world projects, I still reach for Go.

                      1. 9

                        To me Go seems like a big wasted opportunity. If they’d only taken ML as a core language instead of a weird C+gc hybrid, it would be as simple (or simpler) as it is, but much cleaner, without nil or the multi-return hack. Sum types and simple parametric polymorphism would be amazing with channels. All they had to do was to wrap that in the same good toolchain with fast compilation and static linking.

                        1. 2

                          Yeah, I’ve often expressed that I’d like a Go+ML-type-system or a Rust-lite (Rust with GC instead of ownership). I get a lot of “Use OCaml!” or “Use F#”, but these miss the mark for a lot of reasons, but especially the syntax, tooling, and ecosystem. That said, I really believe we overemphasize language features and under-emphasize operational concerns like tooling, ecosystem, runtime, etc. In that context, an ML type system or any other language feature is really just gravy (however, a cluster of incoherent language features is a very real impediment).

                          1. 1

                            Nothing is stopping anyone from doing that. I’d add that they make FFI to C, Go, or some other ecosystem as easy as Julia for the win. I recommend that for any new language to solve performance and bootstrapping problem.

                          2. 3

                            Then, you have languages like D that compile as fast as Go, run faster with LLVM, have a GC, and recently an optional borrow checker. Contracts, too. You get super productivity followed by as much speed or safety as you’re willing to put in effort for.

                            Go is a lot easier to learn, though. The battle-tested, standard libraries and help available on the Internet would probably be superior, too.

                            1. 4

                              I hear a lot of good things about D and Nim and a few others, but for production use case, support, ecosystem, developer marketshare, tooling, etc are all important. We use a lot of AWS services, and a lot of their SDKs are Python/JS/Go/Java/dotnet exclusively and other communities have to roll their own. My outsider perspective is that D and Nim aren’t “production ready” in the sense that they lack this sort of broad support and ecosystem maturity, and that’s not a requirement I can easily shrug off.

                              1. 2

                                I absolutely agree. Unless easy to handroll, those kind of things far outweigh advantages in language design. It’s what I was hinting at in 2nd paragraph.

                                It’s also why it’s wise for new languages to plug into existing ecosystems. Clojure on Java being best example.

                      2. 1

                        Defer was unique to Go but Swift has since adopted it as well, although it is subtly different.

                        1. 8

                          defer wasn’t unique to Go even when it was introduced given that D has had scope(exit) for longer.

                          1. 1

                            Not arguing, just reminded of a gotcha people should know: go’s defer is not scope exit, it’s function exit.

                          2. 4

                            GCC has __cleanup__ attribute for ages.

                            1. 2

                              ZIg also has defer, and errdefer, which I haven’t seen elsewhere.

                              1. 4

                                And unlike Go, Zig gets the scoping of defer correct.

                                1. 2

                                  Sorry to mention D again in this thread, but scope(failure) is pretty much similar to errdefer.