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.
I don't think Java has better capabilities across the board compared to Go. Go's ability to be compiled to a single executable is a strength that Java does not have. I learned Go after reading a post on HN about someone that used Go as a strong tool language, instead of shell script. I partly agree with that, and Java is not that either.
100% this. Not worrying about the runtime environment is a huge improvement.
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.
That's important for CLIs. As the server-side deployment ecosystem has moved towards bundling and isolating dependencies at the OS level (ironically driven by Go projects Docker and Kubernetes) it's mattered less for backend applications. You can just SCP a Go binary around, just like you could always SCP JARs around, but a shop trendy enough to be using Go is unlikely to be doing that.
True, but SCPing a JAR around still requires a JRE of some sort of another and all that entails. Sure, containerization helps that but, in turn, it too is another dependency in the eventual execution.
Long term, I think static binaries win, probably due to advances in WASI/WASM; where the whole execution stack (as we think of it today) is thrown away and replaced.
Not just CLIs, think about prometheus node_exporter or any kind of agent you need to install on a server or deploy to people's workstations or to external customers where you don't have any kind of control.
Single binary, it's definitively one of the killer feature of go.
I liked the article but I have a nit with the Deployment section:
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.
I’m in a weird spot where I like both equally, but for slightly different reasons.
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.
"I’m in a weird spot where I like both equally, but for slightly different reasons."
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.
If you violate general Go programming standards and write code that panics rather than erroring, because a lot of time in shell scripts "bail out loudly the instant anything goes wrong" actually is the error handling you want (that's all "set -e" is in shell in the end, after all), Go is still not a great shell language, but it's tolerable if you have advanced usages. Being able to use all the io.Reader & io.Writer based code can help you do certain very advanced things relatively nicely.
But you need to be doing something seriously funky for it to be worth it and cross into the positive in the cost/benefits analysis. I actually have a service that does this, doing some fairly sophisticated stuff with git repos and what is in them. But it's certainly not something I reach for because Go am best languages for all task.
not needing to learn and master another lang for scripting looks like significant pros to me. I am actually using java for my scripting because of that, which is stranger choice than golang.
If for no other reason than you will encounter code in dynamic scripting languages in your job, statistically speaking.
In my own experience, people who only know one or the other often end up with gaping holes in their skillset. One I see a lot even yea verily here on HN is people who only work with dynamic scripting languages do not realize just how slow their languages are, and pour vast amounts of ultimately wasted effort into speeding their scripting language up when the answer is to stop using them. On the flip side, people who know only a static language, and it's often only one static language, tend to have serious tunnel vision in general about software engineering; I would almost say it's like they don't so much know how to "program" as to drive their IDE in that one language. These people are often the worst about whatever the dogma is in their particular language and will end up driving themselves mindlessly off a cliff, even in their "native" language, when solving problems their dogma doesn't work well for. Example: The thing we've almost all seen with Java codebases that have patterns freaking everywhere, but where they aren't actually doing anything; the factoryfactory that only ever constructs one type of thing in the end, the template code pattern that has only one thing in the code that uses it, etc.
Having at least that much diversity in your language toolkit helps you escape the sort of thing Dijkstra was talking about with how BASIC was ruining people. The effect he was really observing is that in general, if you only have one language, and especially if you spend too much time in that one language before branching out, you tend to start mistaking the details of your language for the foundational reality of computing, and that causes all sorts of "difficult to characterize well in an HN post" problems for you. In Dijkstra's day, that happened to be BASIC, but today there are many languages that can cause that problem. Perhaps Javascript is the leader, not because of any specific characteristic of the language itself but because it is relatively easy to be a "web programmer" and never end up branched out into anything else. I see the negative effects of this from a bunch of people who think that the JS solution is the only solution, and can't even process the idea that if another language does it some other way, there may in fact be reasons for that and it may be a better solution in some ways. I wish that were sarcasm, but it really isn't, it's experience from helping people online.
I'm similar, but I think I'm going to replace Python and Go with Lisp and leave Go for CLI utilities that don't require heavy text parsing and concurrent but not CPU-bound (distributed) apps. Rust for low level or latency/performance sensitive things, which is most of what I do these days.
While the Rust in this article could be a bit better (though that's nitpicking and explicitly not the point), I find it showcases the biggest difference between the two: once you've laid out the foundations which takes a bit of time, the resulting code is a lot leaner. The handlers are short, concise, and the separation of concerns is natural. Good job the shuttle team, I now want to try your service :D
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.
The one thing that stood out was the custom error type that wraps up Anyhow (which is a glorified Box<dyn Error + Send + Sync + 'static> aka void* for errors).
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
I found the article quite unbiased, specially coming from a Rust focused platform service.
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.
"Being originally created to simplify building web services, Go has a number ..."
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.
I feel your comment is equivalent to the author's statement, rather than being a refutation of it. Most work at Google involves writing web servers. Thus, e.g., "simplifying building web services" and "improving productivity of Google engineers" are kind of the same thing.
> 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.
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.
Why are there so many comparisons between Rust and Go? They don't seem to be particularly similar languages, either in use-case or style, but it often feels like the community wants there to be a competition between the two, which doesn't seem appropriate to me.
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 the comparison comes because Rust has fairly solid high-level ergonomics for a systems programming language, which allows to to "bleed" into comparison with traditional higher level languages even where one might initially think it doesn't belong. However, it's ultimately designed for systems programming so it will be hard for it to win on higher level ergonomics evolving in languages like Go or other.
I think they also get compared because they attract programmers that value similar things, just coming at it from two different directions.
I would agree with this one. I used to say stuff like "Go is kinda like Ruby/Python, looking down the stack, and Rust is kinda like C or C++, looking up the stack." It makes sense that there's some point where those two trajectories meet in the middle, even if on some level they have their own distinct strengths and weaknesses.
I find that for many projects I start, I seem to want:
- 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.
Go has the ability to do what C or C++ can do to some extent while offering simplicity, easy to read and understand while rust gives you full control with its powerful type system because of that those complicated things can just be wrapped around someone’s library or function (.unwarp, .clone, Rc, Arc) coupled with rust ability to span in different domains of programming, rust will be compared to higher programming languages
If you are writing a web application, as many people do, they are direct competitors, as you could easily use either one of them (or many others) and get the job done. And for many people in this case developer experience is 10x more important than GC or some advanced language features (that 90% of webapps won't care about).
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.
There's all sorts of ways to categorize languages.
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?
Rust async story is still playing out. Go green threads approach is pretty solid.
My go to stack for personal projects:
Go
SQLC for db access
https://github.com/a-h/templ (kinda JSX for go) for templating
HTMX
Postgres
I have a feeling Mojo will ship 1.0 before Rust finishes sorting out async
I wouldn't say that reference counting systems are a "software engineering best practice". Mainly because the engineer is not consciously doing anything themselves. It's not like a builder pattern or something. They are just doing what the compiler wants them to do. In fact, there are many ways to get around the compiler errors that feel like an anti-pattern (like using `.clone()` everywhere).
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.
IMO the problem is Rust is very opinionated about how you structure your application, which might not actually match up with what the most logical structure for a particular case is. It depends.
Not to ignite a flame war, but to make a point about daily bread activities:
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?
> 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.
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.
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.
Go is a great replacement for Python as a web backend language (which Python really is not). I'm not sold on Rust as a web backend language, though: It ends up being a little too hard to work with (hello `async`) in that application, and you need to import a lot of 3rd party dependencies that are very opinionated. That stuff and the complexities of working with the borrow checker and async adds a lot of complexity to your large, long-running applications that you don't have to manage in Go.
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 agree that Go's web backend features make it fun to prototype an application. But the moment I want to do anything more complicated, then I'm not sure.
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.
It sounds like you have a strong personal preference for Rust, which is fine. I'm pretty sure nobody loves Go as much as many people love Rust.
Even 100k LOC is pretty small for a software project, and likely doesn't need more than a few engineers. The advantage of the simplicity of Go shows up when you have to coordinate across >100 people, many of which are kind of mediocre, and you need all of them to ship features. If everyone in the world were a genius who is obsessed with writing clean code, Rust would be a fantastic language to work in at that scale, but they are not.
For clarification, these are 100k LOC projects where I'm the only software engineer. I've worked on larger projects in C++ with other engineers, and would absolutely continue to prefer Rust as the size of the codebase increases. I guess my point is that Rust scales in a way that few languages do. Go comes close though :)
This has been my primary objection with Go, as well. I wonder if it's just a lack of practice and that I'd eventually git gud, but I find it so hard to flow through code to get a general idea of what's going on. It's basically impossible to use code "paragraphs" to separate logical groupings of functionality because of the `if err != nil` blocks, and leads to a very choppy reading experience. With any non-trivial logic, I've found Go to be detrimental to my understanding of what's going on.
Sure, but this code propagates the errors and that has the same problem:
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 point of verbosity in Go error handling is context. In Go, you rarely just propagate errors, you prepend them with context information:
val, err := someOperation(arg)
if err != nil {
return nil, fmt.Errorf("some operation with arg %v: %v", arg, err)
}
It really sucks when you're debugging an application, and the only information you have is "read failed" because you just propagated errors. Where did the read happen? In what context?
Go errors usually contain enough context that they're good enough to print to console in CLI applications. Docker does exactly that - you've seen one if you've ever tried executing it as a user that isn't in "docker" group:
docker: Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Post http://%2Fvar%2Frun%2Fdocker.sock/v1.35/containers/create: dial unix /var/run/docker.sock: connect: permission denied. See 'docker run --help'.
If your error is 5 or 10 levels deep, do you prepend contextual information every time? External libraries typically have good error messages already, why do I have to prepend basically useless information in front of it?
Not to pick on any of these projects, but this pattern is way too common to not have a some sugar:
Wait that’s interesting and I haven’t formulated it this way.
It reminds me of A Philosophy of Software Design:
The utility of a module (or any form of encapsulation such as a function) is greater, the smaller the interface relative to the implementation. Shallow ve deep modules.
Error handling might be a proxy to determine the semantic depth of a module.
Another perspective: (real) errors typically occur when a program interacts with the outside world AKA side effects etc.
Perhaps it’s more useful to lift effects up instead of burying them. That will automatically put errors up the stack, where you actually want to handle them.
> Perhaps it’s more useful to lift effects up instead of burying them. That will automatically put errors up the stack, where you actually want to handle them.
This perspective also made me think of another advantage of Go error handling over traditional try/catch logging:
In other programming languages, when doing some asynchronous work in some background worker or whatever, exceptions become useless as error reporting mechanisms, because stack traces don't correspond to the logical flow of the program. I remember Java's Spring to be exceptionally painful in this regard.
In Go, since errors are values and you're expected to add context information whenever appropriate, you can shift and shuffle those errors between stacks, and still keep useful context information when you eventually read those errors.
It does the same thing as the Go version. The Go version requires you to pass into the context the data to be sent back to the client, while the Rust version uses the return value of the function as the data to be sent back. The framework then serializes that appropriately.
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).
Funny, I have the exact opposite reaction to those examples. I look at the Rust code and think "what happens when something goes wrong?" There's no way to tell from the code you gave. The error handling is somewhere else. Whereas, I can see exactly how the Go code is going to behave.
I like that errors are in your face too, but with the caveat that only when they matter. And in Go, the lazy thing will result is a bad time. You can always bet on people being lazy.
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.
If those calls can cause errors, then yes, it's necessary to handle them. Maybe you're content to have a contextless stack trace printed whenever things fail, but I like a little order.
1k, 100k, 10m loc does not change anything because no project depends on all the loc as a single unit, everything is split into modules / packages / class / functions.
Kubernetes is over 1.5M loc and I've not seen problem with error handling.
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.
I think this over complication makes the comparison more realistic. Yes for this exact example it's too much, but it gives a better understanding of what a more complex real-world example would look like.
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.
Author here. It took me about 2 hours to code the Go service and maybe 4 to do the Rust part. The reason why Rust took longer was mainly because of the better error handling and the few more higher level abstractions. I could have added unwraps everywhere to get about the same experience, but yeah.
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.