1. 18
    1. 32

      I really don’t want to be this grumpy old man, but.. can we just stop comparing this two, completely unrelated languages? Where are all the Zig vs Haskell posts? It makes just as much sense. Go is simply not a low-level language, and it was never meant to occupy the same niches as Rust, that makes deliberate design tradeoffs to be able to target those use cases.

      With that said, I am all for managed languages (though I would lie if I said that I like Go - would probably be my last choice for almost anything), and thankfully they can be used for almost every conceivable purpose (even for an OS, see Microsoft’s attempts), the GC is no problem for the vast majority of tasks. But we should definitely be thankful for Rust as it really is a unique language for the less often targeted non-GC niche.

      1. 33

        i would read a zig vs haskell post

      2. 23

        You should understand the context a little bit: Shuttle is a company focused on deploying backend applications in Rust. Backend applications (server programs) is precisely the main use-case for Go. It absolutely makes sense for them to compare the two in this context.

        1. 10

          Java is an even more common choice for backend development, but fair enough.

          My gripe is actually the common, hand-in-hand usage of this “rust’n’go” term, which I’m afraid is more often than not depends on a misconception around Go’s low-levelness, as it was sort of marketed as a systems language.

          1. 10

            It is a “systems language”… by the original definition where low-levelness was a side-effect of the times and the primary focus was long-term maintainability and suitability for building infrastructural components.

            (Which, in the era of network microservices, is exactly what Go was designed for. Java, Go, and Rust are three different expressions of the classical definition of a “systems language”.)

            That said, there has been a lot of misconception because it’s commonly repeated that Go was created “to replace C++ as Google uses it”… which basically means “to replace Python and Ruby in contexts where they can’t scale enough, so you used C++ instead”.

            1. 5

              Great link, thanks!

              BTW Rob Pike is cited in it where he regrets calling Go a systems language, but that’s probably because he used it in the original sense, and the meaning has drifted.

      3. 5

        Go is simply not a low-level language, and it was never meant to occupy the same niches as Rust, that makes deliberate design tradeoffs to be able to target those use cases.

        Go was originally designed as a successor to C. The fact that it’s been most successful as a Python replacement was largely accidental. It does (via the unsafe package) support low-level things but, like Rust, tries to make you opt in for the small bits of code that actually need them.

        1. 5

          Go was originally designed as a successor to C. The fact that it’s been most successful as a Python replacement was largely accidental. It does (via the unsafe package) support low-level things but, like Rust, tries to make you opt in for the small bits of code that actually need them.

          Go had three explicit, publicized design goals from the start; none of them were “be a successor to C”.

          They were written relative to the three languages in use at google:

          • Expressive like c++/python
          • Fast like java/c++
          • Fast feedback cycles (no slow compile step) like java/python
          1. 11

            Go had three explicit, publicized design goals from the start; none of them were “be a successor to C”.

            Rob Pike was pretty explicit that it was intended as a C successor. He said this several times in various things that I heard and read while writing the Go Phrasebook. His goal was to move C programmers to a language with a slightly higher level of abstraction. He sold it to Google relative to their current language usage.

            1. 2

              In that case, I suppose I must defer to your closer experience!

          2. 2

            They really dropped the ball on that expressivity part. It’s as expressive as C, which is.. not a good thing.

      4. 1

        The article is about a DB-backed microservice. I’m curious whether you feel rust or go is inappropriate for this domain.

    2. 25

      I appreciate that the Go code here is Actually Good™ — it uses proper types, has proper function signatures, and does proper error handling. Thank you!

    3. 7

      The overlap in use cases for these two languages is way smaller than people seem to think

      1. 6

        I do distributed systems and web, and learning Rust made Go an obsolete language as far as I’m concerned. I legit can not think of a use case where I would pick Go over Rust.

        1. 4

          Having a team that doesn’t already know Rust is at least one reason I can think of. It takes much less time to get a team up to speed on Go than it does on Rust. Maybe compilation/test speed (eg. development velocity) is another.

          1. 1

            You are not wrong, but a team knowing one language and not the other can be used to justify any language :)

            1. 3

              Well, yes. I believe programmers in general and language zealots in particular underestimate how hard it is for a team to ramp up productively in a new language, and overestimate how productive the team will be once they have ramped up. In other words, the productivity bottleneck in programming is seldom the language itself.

    4. 12

      Heres a nasty take that I am sure people will hate:

      I love Go if I need async, the runtime genuinely helps a lot: even with the slice semantics I despise that allow for interior mutability in unexpected situations.

      I love Rust for basically everything else; but I avoid async like the plague.

      The Rust toolchain is just so excellent that I always miss it when writing Go, but Go’s async is first class. Rusts feels like pulling teeth.

      1. 2

        Complaining about Rust async has become something of a bandwagon. I write a small web app in Axum and things are… fine.

      2. 1

        What is Go’s async? I wasn’t aware that Go had async.

        1. 11

          goroutines and channels are essentially solving the problem async seeks to solve.

          1. 13

            … and they’re so good at it that people say “I wasn’t aware that Go had async”; nearly all the async stuff just kinda vanishes in go - having a runtime that can suspend a goroutine means you can just write blocking code and not think about it.

            1. 3

              However, an argument can also be made that they’re the GOTO statement of concurrency and suffer from the same control flow and maintainability issues.

              1. 4

                I agree that a bare go statement is almost always a code smell - I’d go as far as to compare it to unsafe in rust.

                However, I can count on one hand the number of times I’ve needed to use one as an applications developer. For instance, the net/http server is concurrent, but unless you go source-diving through the stdlib you wouldn’t see any concurrency primitives.

                Of those times I’ve used one, all have been for parallel-processing a loop - which doesn’t let goroutines outlive the calling function, and provides a straightforward way to propagate errors, and therefore doesn’t break the function-call abstraction.

                1. 3

                  all have been for parallel-processing a loop - which doesn’t let goroutines outlive the calling function

                  I maintain a “pithy” Go style guide, and this is almost verbatim what I recommend in one of the concurrency sections.

                  Goroutines should have well-defined lifetimes

                  • Goroutines should almost never outlive the function they’re created from
                  • Avoid “fire and forget” goroutines – know how to stop the goroutine, and verify it’s stopped
                  • Avoid spaghetti synchronization: sync.Atomic as a state bit, starting/stopping/done channels, etc.

                  This is closely related to a section about API design.

                  Write synchronous APIs

                  • By default, do work in regular blocking functions
                  • Let your callers add concurrency if they want to
                  • No: Start(), Stop(), Wait(), Done()
                  • Yes: Run(context.Context) error
                  • Model periodic tasks as sync methods that should be regularly called, not autonomous goroutines
                  • WaitGroups, Mutexes, etc. as parameters or return values are almost always a design error
                2. 1

                  That’s fair, but bear in mind that asking people to practice structured programming only helps so much, in the same way that asking people to practice memory-safe C++ coding practices only helps so much.

                  In the end, there’s a reason C only allowed structured GOTO (i.e. you can’t jump into the middle of another function), rather than the unstructured GOTO of things like assembly language that Dijkstra was writing about. If Go’s big reason for limiting language complexity and leaning on boilerplate is to limit the harm novice team members can cause, then having go instead of some more structured construct is questionable at best.

                  1. 1

                    To be fair, structured concurrency is much easier to accomplish (and check for during review) than memory-safe C++.

              2. 2

                I ran into a bunch of problems with goroutines and asked around back when I was still writing golang whether it was documented anywhere how to use them as a non-footgun… and after some searching around I was pointed to an obscure webpage with patterns to follow.

                With which I’d like to say: 👏🏼

          2. 2

            Do they? Goroutines are a lightweight, cooperatively scheduled thread. Async/await is a stripped down version of coroutines. Coroutines were originally added to Simula to make writing the interleaving of agents acting on each other straightforward and they come with a bunch of built-in stuff about plumbing data and resuming frames. Even the wimpy async/await version still retains some default plumbing that isn’t completely trivial to do with channels.

        2. 6

          It doesn’t have the async keyword but the language is deeply integrated with async I/O. Brad Fitzpatrick once told me “go is my favorite epoll library”.

        3. 3

          I’m guessing they mean channels.

    5. 5
       async fn fetch_lat_long(city: &str) -> Result<LatLong, Box<dyn std::error::Error>> {
       	let endpoint = format!(
       		"https://geocoding-api.open-meteo.com/v1/search?name={}&count=1&language=en&format=json",
       		city
       	);
       	let response = reqwest::get(&endpoint).await?.json::<GeoResponse>().await?;
       	response
       		.results
       		.get(0)
       		.cloned()
       		.ok_or("No results found".into())
       }
      

      The code is a bit less verbose than the Go version. We don’t have to write if err != nil constructs, because we can use the ? operator to propagate errors.

      That’s a library choice, not a language issue. Here’s what this would look like in Go with requests:

      func getLatLong(city string) (*LatLong, error) {
      	var response GeoResponse
      	err := requests.
      		URL("https://geocoding-api.open-meteo.com/v1/search?count=1&language=en&format=json").
      		Params("name", city).
      		ToJSON(&response).
      		Fetch(context.Background())
      	if err != nil {
      		return nil, err
      	}
      
      	if len(response.Results) < 1 {
      		return nil, errors.New("no results found")
      	}
      
      	return &response.Results[0], nil
      }
      

      It’s only longer because there’s no chaining equivalent of .get(0) in Go.

      1. 6

        The point about the question mark operator still stands, these three lines of code

            if err != nil {
        	return nil, err
        }
        

        Are just the two question marks in this line of rust (not the rest of the code on the line, literally just those two individual characters).

            reqwest::get(&endpoint).await?.json::<GeoResponse>().await?; 
        

        And that’s with the rust code having a library that seperates out getting a request and parsing the response (with two sets of errors to handle), while you have a go library that does all of that in one call (with one set of errors to handle). If the rust library was more like the go library, you would only have one question mark worth of error handling. If the go library was more like the rust library, you’d have two sets (six lines) of if err != nil ....

        Personally I think that more important than the verbosity called out in the quote is that in the rust version you can’t forget to handle the errors, while in the go version you can.

        1. 5

          There’s a library called try that can reduce the boilerplate a little. It would look like this:

          func get[E any](s []E, i int) (e E, error error) {
          	if i >= len(s) {
          		err = fmt.Errorf("out of range"))
          		return
           	}
          	return s[i], nil
          }
          
          func getLatLong(city string) (pos *LatLong, err error) {
          	defer try.Handle(&err)
          	var response GeoResponse
          	try.E(requests.
          		URL("https://geocoding-api.open-meteo.com/v1/search?count=1&language=en&format=json").
          		Params("name", city).
          		ToJSON(&response).
          		Fetch(context.Background()))
          	loc := try.E1(get(response.Results, 0))
          	return &loc, nil
          }
          

          That said, I don’t think the library will really catch on because Gophers mostly like the boilerplate. It is more verbose, but it’s easy to read and not that hard to type. There was a proposal to add try to the language from the language maintainers, and the community didn’t like it, so it didn’t go anywhere.

          Personally I think that more important than the verbosity called out in the quote is that in the rust version you can’t forget to handle the errors, while in the go version you can.

          This has become a big Rust talking point, and I can’t understand why. It just doesn’t happen with any significant frequency that people forget to do error handling in real code. If you’re paranoid, there’s a common linter called errcheck that will check it for you, but I haven’t found a need for it. A much better talking point would be that in Go, nothing keeps you from accidentally creating a race condition across Goroutines, which is a real problem.


          Incidentally, AFAICT, the Rust code has a security bug. Unless format! is URL specific (I don’t think so?), this would allow query parameters to break out:

          let endpoint = format!(
          	"https://geocoding-api.open-meteo.com/v1/search?name={}&count=1&language=en&format=json",
          	city
          );
          

          This bug isn’t Rust specific, but it just shows that no language can save you from all possible bugs if you do things wrong.

          1. 8

            This has become a big Rust talking point, and I can’t understand why.

            I mean, I personally have fixed bugs in production go code at a company I was working for that happened as a result of ignoring err… which tends to make me think it matters. Maybe that was exceptional for some reason, but I don’t see any reason to think so.

            And yes you’re right about that format statement. I’m certainly not claiming that rust eliminates all bugs, but that doesn’t mean that it isn’t better to have a language that doesn’t have a class of bugs than one that does have that same class.

            1. 2

              …that doesn’t mean that it isn’t better to have a language that doesn’t have a class of bugs than one that does have that same class.

              All else equal, sure, but all else is never equal 😉 Every benefit carries a cost. It can definitely be the case that the benefits of preventing a class of bugs at the language level are outweighed by the costs required to provide that feature.

    6. 4

      Since we are a platform-as-a-service provider, we think that we can contribute the most by showing you how to build a small web service in both languages.

      In 99% of cases, everything else being equal, Go works best in this scenario. Main benefit of Rust is memory safety without GC overhead, and it’s almost never worth it if network latencies are present.

      1. 5

        I disagree. To me, the main benefit of Rust is that it’s much more unlikely I’ll have to deal with an essential service having gone down at 3AM.

        That and the ability to easily expose bindings to tons of languages so I can reuse code are why I use Rust in place of “shell scripts” (actually Python these days) so often. Rust, like Haskell, surfaces as much as possible at compile time but, unlike Haskell, has an ecosystem leaning more toward “fearless upgrades” and less toward pushing the state of the art at the expense of a little API breakage.

      2. 2

        My personal experience between the two languages was that CPU/latency differences were small (10-20%) but memory consumption differences were huge (4-5x). Even with the cost of network hops, at a certain scale the OpEx differences can be huge.

        1. 1

          How did you measure memory consumption?

          1. 1

            It’s been long enough that I’ve forgotten the details, and it was a past job. I’m pretty sure I used one of the Datadog memory metrics.

    7. 3

      What are build times like for go compared to rust? Is it faster for comparably sized large projects?

      1. 15

        based on my own personal (anecdotal) experience, small to medium sized projects take N minutes to compile via rust, and N seconds to compile via go.

        after initial compilation, subsequent compilations will still take longer in rust, but it’s much more comparable (both languages do some caching to make things faster here).

        1. 1

          mold is our savior.

      2. 8

        Yes.

      3. 3

        If the article authors would provide the source code repositories we could easily measure the how long it would, but based on my personal experience the difference in compile times is huge. Go compile times are usually in the single digit seconds range, except for projects using a lot of Cgo or if they’re very large, like Kubernetes.

        1. 4

          Could I politely yes, and … ? Yes, the go compiler is faster, but in my experience, any large go project will use linters, many of them. Linting is slow. The linters catch many errors, like not checking a returned error which the rust compiler requires you to check. I suppose that means you can get an executable quickly, but I feel comparing Go’s compilation speed vs rust without linting doesn’t capture real world usage.

          1. 4

            In my experience, the speed of the Go compiler allows you to move quickly during development. And yes, you do spend a few extra seconds linting before committing your code or cutting a release. I don’t typically run the compiler and linter in lock step.

            During development, we tend to compile frequently but lint less often. This is especially true if you’re already using VSCode or Goland for incremental linting.

          2. 4

            Linting and compiling are completely decoupled. You can do them in parallel if you’d like.

          3. 4

            Linters are usually just as fast as the compiler, O(seconds).

    8. 1

      in case the author is reading this, i have a few corrections (i’ll add more via edits as i read):

      1. there is a small typo in the code block under the text “For a start, let’s call these two functions in order and print the result:”: func main() func main()

      2. the link attached for this "Go by Example" article is incorrect, it should point to https://gobyexample.com/time

    9. 1

      There seems to be a markup issue around:

      are [some caveats with returning impl IntoResponse] (https://docs.rs/axum/latest/axum/response/index.html)
      

      Unnecessary space between ] and (

      1. 3

        Thanks so much for the feedback! We’ve updated the article to address the changes.

        1. 1

          yay! thanks for writing it, it was a fun read.

      2. 3

        There’s also a markup typo on the Go Routing section. Look for “Which one you want to use depends on your use case”, that is probably meant to be outside the code snippet.