The error handling in Go is SO verbose. When reading my code (or even reviewing other people's code) in order to understand at a high level what is going on, I feel like I'm squinting through a mesh wire window.
Compare this example in Go:
city := c.Query("city")
latlong, err := getLatLong(city)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
weather, err := getWeather(*latlong)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
weatherDisplay, err := extractWeatherData(city, weather)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.HTML(http.StatusOK, "weather.html", weatherDisplay)
To this code in Rust: let lat_long = fetch_lat_long(¶ms.city).await?;
let weather = fetch_weather(lat_long).await?;
let display = WeatherDisplay::new(params.city, weather);
Maybe on first glance the Rust code can seem alien (what is a `?` doing there, what is actually going on with `.await`, etc) but when you are writing a 100k line application in Rust, you learn the patterns and want to be able to see the domain application logic clearly. And yet, there's no hidden errors or exceptions. When this code fails, you will be able to clearly identify what happened and which line the error occurred on.Prototyping even small applications in Go is verbose. And worse still, error prone. It's easy to be lazy and not check for errors and oops 3 months in your code fails catastrophically.
I know a lot of people like Go on here but in my mind Go only makes sense as a replacement for Python (static compilation, better error handling than Python, faster etc). If you don't know exactly what you want to build, maybe it is faster to prototype it in Go? I still would reach for Rust in those instances but that's just me. For large applications there's no question in my mind that Rust is a better choice.
Edit:
people have pointed out that I'm not comparing the same thing, which is true, I apologize for the confusing. But even code where Go propagates the errors, it is much more verbose (and my point still stands)
err := db.Get(&latLong, "SELECT lat, long FROM cities WHERE name = $1", name)
if err == nil {
return latLong, nil
}
latLong, err = fetchLatLong(name)
if err != nil {
return nil, err
}
err = insertCity(db, name, *latLong)
if err != nil {
return nil, err
}
And this is extremely common. More discussion in the thread below.I think Rust is a fantastic systems language that is misapplied to the web. I think Python was a fantastic scripting language that is misapplied to the web, too, so you can put that in context.
I counted the number of lines in my work projects, and I have $WORK projects that are 100k lines of code. Maintaining that in Go would seem like a nightmare to me, but in Rust that is so much nicer. My personal projects range from 10k - 35k and in all of those I much prefer the ones where I'm writing and maintaining Rust vs Go when it comes to similar complexity.
Handle(err)
which may take a line, but isn't the 3 lines everyone refers to, and often has a lot of extra logic.
Control flow wise, Go is much easier to understand than chains of ? and await.
`match result { Ok(v) => v, Err(e) => return e.into() }`
This means that the code related to error handling is all in one place, and your business logic can just focus on the happy path.
err := db.Get(&latLong, "SELECT lat, long FROM cities WHERE name = $1", name)
if err == nil {
return latLong, nil
}
latLong, err = fetchLatLong(name)
if err != nil {
return nil, err
}
err = insertCity(db, name, *latLong)
if err != nil {
return nil, err
}
In Rust propagating errors is a lot more succinct and easy to do. It is usually what you want to do as well (you can think of Python and C++ exceptions as essentially propagating errors). The special case can be handled explicitly. In Go, you have to handle everything explicitly, and if you don't you can fail catastrophically.I guess it comes down to what features the language provides that makes it easy to do "the right thing" (where "the right thing" may depend on where your values lie; for example, I value correctness, readability of domain logic, easy debugging etc). And in my opinion, it's easy to do what I consider bad software engineering in languages like Go.
The only issue there is if you want to return the error with code 200 (which you shouldn't, but it's been known to happen). In that case the Go code and the Rust code will look a bit closer to each other because then you can't use `?` this way (without writing some more boilerplate elsewhere).
(I like Rust, C, C++ etc. as well.)
Like take a look at this pattern:
err := db.Get(&latLong, "SELECT lat, long FROM cities WHERE name = $1", name)
if err == nil {
return latLong, nil
}
latLong, err = fetchLatLong(name)
if err != nil {
return nil, err
}
err = insertCity(db, name, *latLong)
if err != nil {
return nil, err
}
Is it really necessary to have the error handling explicit in this case? Go through any of your go code. I find this kind of error handling is 90% of the error handling I write.Kubernetes is over 1.5M loc and I've not seen problem with error handling.
can you build some utility function check_r, which will panic if found error code, so your Go code will look like:
latlong := check_r(getLatLong(city))
Go should be compared to other high-level garbage collected languages: Java, Python, C# -- that kind of thing.
Rust should be compared to C++ and C, but I'd also love to see it compared to other languages with advanced type systems, or contrasted with languages with more traditional or simple type systems.
I think they also get compared because they attract programmers that value similar things, just coming at it from two different directions.
- static types
- small, static binaries for deployments
- lower memory requirements and value semantics where appropriate
- a reasonably large ecosystem
- cross-platform with strong support for linux/cloud
These two languages usually end up at the top of my list when given these requirements.Btw. I really liked an introduction article about Rust in some Linux magazine (don't remember which one). Write hello world, print a number to stdout, handle option and result, no mention of pointers or algebraic data types theory. I am sure will get a lot of people into using it without scaring anyone. You really need very little to get started.
Why group by "high-level" (whatever that means) and gc?
Why not say "why compare rust and go - go belongs in the minimal type system category with zig, C, python, lisp. rust belongs in the inferred thpe system category with haskell, ocaml, f# and so on"?
Or (as is the case with the article), why not group them by popularity for a domain/task and do a comparison in that light?
When I have a dumb idea (they’re all dumb), depending on what its core features are, I’ll prototype it in Go, TypeScript, or Python. Inevitably I’ll want to or I will actually rebuild it with rust once it’s relatively stable.
It’s like Go, TypeScript, and Python are pencil and rust is ink.
Go is my favourite for strictly back end prototyping, but I don’t love it for processing data. That’s where Python shines. Then sometimes I’ll use a full stack framework like Next or Remix and build a backend there, but transfer it (or important parts of it) to rust eventually.
My favourite deployment is a totally static site with a rust backend. But it’s equally easy with Go, and the initial development with Go is always way faster for me. It’s just the long term maintenance story that I don’t love with Go.
Also, I prefer Rust’s type system quite a bit. It could just be intuitive to me. I do find several teams I’ve worked with on Go projects have not actually understood the Go type system, and they’ve run into pain as a result very regularly.
It's not weird. Weird is "I have a language I use for everything. Quick scripts! Network servers! My shell! Games! Batch processing CSV records from random FTP servers! Everything! Any implication that my language is not the best choice for all these things must be met with maximum hostility!" is the weird thing. Popular, but weird.
A professional programmer should have at a bare minimum a static language, a dynamic scripting language, basic shell familiarity (be it Unix or Powershell, I'm OS agnostic here, but you should be able to use whatever it is), and probably at least some basic SQL of some sort. And as your career advances, it'll only grow from there.
Time to rewrite my shell scripts in rust.
I'm mostly joking but there might be a couple cases where that makes sense for me...
I love this analogy
Some people say the learning curve for Rust is steep. But, in a way you're learning how to write safer code using the latest in language techniques.
There are other ways to write safe code than just reference counting systems. And arguably, a software engineer would learn more best practices by managing memory more directly.
Do you need them? Will you enjoy using it?
I learned both of these simultaneously in two quirky ways.
I learned Go using the "Learn Go with Tests" book https://quii.gitbook.io/learn-go-with-tests/
I learned Rust through a book called "From JavaScript to Rust" https://github.com/vinodotdev/node-to-rust/releases/download...
The thing that stood out the most to me while learning these was their focus on composition over inheritance. Even though I really like C-based languages, I have never liked the inheritance push from the 90s-2000s.
I really like some of the things in Go, but overall I felt I just didn't need it. .NET, Java, etc are more mature with comparable or better capabilities across the board. It's just hard for me to justify Go outside of stylistic preference.
I really, really like Rust but ever since learning it almost 2 years ago, I still have not found a single use case for it. It's partially the maturity of the competition, but it's also that even in HPC, ML/DL, etc I've done just fine with the "legacy" stack.
I would love to hear from people what their thoughts are on this. What are some scenarios where you see these languages thriving? What makes you want to use them? What's the best use case? Would love to evolve my thinking.
And with go embed[1], that single binary can be literally _everything_ needed by the application. Even non-go stuff can be embedded, like for example, a compiled React application.
I think this is very outdated claim: https://www.graalvm.org/
I think JVM specifically has many design issues which allows rust to squeeze good extra mile compared to JVM hpc solution.
Um. No. Go was originally created at Google in 2007 by Robert Griesemer, Rob Pike, and Ken Thompson.
Some key reasons for its creation include:
Improving productivity of Google engineers - The creators felt existing languages like C++ took too long to compile and had other productivity issues. Go was designed to be simpler and faster to compile.
Supporting concurrency and multi-core machines - Go has built-in concurrency features like goroutines and channels to allow easy parallelism and concurrency on multi-core machines. This was becoming increasingly important with multi-core CPUs.
Improved code readability and maintainability - Go was designed with a clean, minimal syntax that avoided complex features like inheritance in favor of simplicity. This improved code readability and made programs easier to maintain. Better performance than interpreted languages - While Go is a compiled language like C++, it was designed to provide performance closer to those languages while maintaining some productivity benefits of interpreted languages like Python.
Supporting networked systems and servers - Go has good built-in support for networking and servers, making it well-suited for building networked systems, web servers, and other server tools.
References: https://go.dev/talks/2012/splash.article
that's possibly Google marketing hyperbole : check out what the guys were doing just before changing from Bell Labs to Google.
How is this not a web service?
The thing I feel like misses from the article is the more "second thought but vital" stuff, like observability. The tracing ecosystem for Rust is a blessing, and so boilerplate-free that you sprinkle it all over and never forget about it. From experience, having to manually access the span, put data in it, and defer close, that people don't bother doing them. Yay macros!
With Go it's really easy to throw stuff at the wall, and it'll stick. Rust encourages you to put glue on it before throwing stuff.
Also once you get used to axum and the various extractors, be warned, you'll hardly go back. It's so natural and powerful.
How so? While the Go seems very high quality, they are a Rust shop so I assumed the Rust is even better (I don't know Rust).
What would actually be done for such an app would at least be one variant for each error type you can encounter (e.g. Db(sqlx::Error), Api(reqwest::Error), ...) with something like thiserror. Then in the conversion to Response, there'd be a (admittedly) large match that maps out every error kind to a response code. In this example, they conflated both the external api request and database error to a 500, while before they properly made the api request a 404 (which is not always the case), and the db error to a 500.
For real apps you gain a lot. By doing this legwork, you get terrific developer experience as virtually all errors that can happen get mapped to a proper status code and handled by a single `?`, and terrific user experience as all errors ... (you get the idea).
Don't get me wrong, that's still an improvement! They do show it is easy to have the infrastructure to do so. In the go version, there is literally no thought given to this. Sprinkle
return c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
everywhere and done!As is evident from the code, Go is simpler and cleaner. In 3 months it would be much easier to reason about than the Rust version.
I am happy with our decision to migrate to Go from Common Lisp. I also wonder what is the long term memory usage of the Rust system. With Go I have found very little memory use which remains constant for months before machine swap.
You don't need a database, just an in-memory cache. All you're storing is recent weather info, which you get from an external server. Caching is just to prevent hitting the external server too hard.
Then the program becomes a single-file executable, which means you can get rid of Docker.
If you set this up to run under FCGI, it can run on some $5/month shared hosting account.
Or, at least, they would be after applying optimizations to them.
In short, both of these would serve more rps than you will likely ever need on even the lowest end virtual machines. The underlying API provider will probably cut you off from querying them before you run out of RPS.
First, Go has other deployment options other than Docker or providers that support docker containers. For example, Google App Engine (GAE) has a command line interface very similar to Shuttle that enables deployment and hosting of your Go application. This is probably the best 'apples' to 'apples' comparison.
Go and Rust are very similar in this way because both can be compiled to a statically linked binary with zero runtime dependencies[1]. This is important because both Javascript and Python are interpreted, frequently with run-time dependencies in addition to the interpreter, that can make hosting and deployment more difficult.
Since both languages compile to a statically linked binary, they can be hosted virtually anywhere. Any old linux box or VCS with a modern os should work just fine. Optionally, one can serve from behind Nginx or Caddy for extra functionality.
I've chosen this approach on a side project. It deploys with a simple `make deploy` command that runs tests, creates the static binary, and deploys the build asset over SSH and cycles the systemd service. It can be rolled back with `make rollback` which reverts by simply changing the symlink and restarting the service.
[1] Assumes a modern OS.
what was the compilation time of both projects, esp. after using crates like 'anyhow'? How long did it take to have a full fix-build-test cycle in both languages? How many refactorings per time unit could a smart coder do in both ecosystems?
I've found that refactors are more involved in Rust (because of some "viral" features that require changing code in a lot of places, adding lifetimes to a type being the most obvious example) requiring more effort from the developer, but that after handling the complaints from the compiler the resulting code is usually properly ported to the new design. This has not been my experience in other languages, particularly dynamically typed ones. It is partially helped by some design patterns that are prevalent in Rust libraries, like typed builders or more complex trait bounds that ensure correct usage of the exposed APIs.
So congrats! Relevant part FTA:
Which language is right for you?
Go:
- easy to learn, fast, good for web services
- batteries included. We did a lot with just the standard library.
- Our only dependency was Gin, which is a very popular web framework.
Rust:
- fast, safe, evolving ecosystem for web services
- no batteries included. We had to add a lot of dependencies to get the same functionality as in Go and write our own small middleware.
- the final handler code was free from distracting error handling, because we used our own error type and the ? operator. This makes for very readable code, at the cost of having to write additional adapter logic.