428 lines
14 KiB
Markdown
428 lines
14 KiB
Markdown
---
|
|
title: "Rust vs. Go"
|
|
date: "2024-06-06"
|
|
draft: true
|
|
---
|
|
|
|
A lot of folks have been pitting Go and Rust against each other. Normally, I
|
|
would abstain from throwing my opinion out on the internet, but it's come up at
|
|
work and since I heavily prefer one over the other (Rust subjectively!), I want
|
|
to write out why that is. Hopefully it is useful to others out there.
|
|
|
|
<!--more-->
|
|
|
|
# Rust versus Golang
|
|
|
|
I'm aiming to keep this succinct so the people I share this with will actually
|
|
read it. This will probably be dense and pointed, as I'm trying to construct an
|
|
argument.
|
|
|
|
This article is opinion and not stating objective fact. These are my
|
|
preferences. I have roughly-equivalent Rust experience, exposure, and knowledge
|
|
than I do Golang, though I have run, operated, and maintained far more Golang
|
|
services at my job than I have Rust.
|
|
|
|
Also by posting this I'm sure I will get free <s>flame wars</s> consultation on
|
|
my Go code and become a better programmer! 😜
|
|
|
|
## Stop Comparing! They're Different!
|
|
|
|
Of course they are! This doesn't mean they can't be used to solve the same
|
|
problems. In my experience, one is simply better than the other for the same
|
|
use-cases.
|
|
|
|
They're also the same _category_ of thing: general purpose programming
|
|
languages. Which means it's good to talk about the various strengths and
|
|
weaknesses of the two.
|
|
|
|
# Why Rust
|
|
|
|
## Rust has _all_ of Go's superpowers
|
|
|
|
Ok, except it's uber simplicity. But what you lose in simplicity, you gain in
|
|
many other areas. I'll elaborate later.
|
|
|
|
### Goroutines
|
|
|
|
Use `tokio::spawn`. Yes, they are different (mainly stackless versus
|
|
stackful) but the goal and considerations are effectively the same.
|
|
|
|
If you don't like Tokio or can't use it, there are different implementations of
|
|
this on top of Rust's `async/await` system that have the same benefits.
|
|
|
|
While it's arguably not as cohesive as Go's implementation, it's there if you
|
|
want it.
|
|
|
|
### Channels
|
|
|
|
Rust has these. See `std::sync::mpsc`.
|
|
|
|
### Defer
|
|
|
|
Largely unnecessary thanks to the compiler, but you can implement your
|
|
own via the `Drop` trait or reach for the `scopeguard` crate.
|
|
|
|
### Tooling
|
|
|
|
Again, Rust has an incredible tools ecosystem just like Go. Better in many ways
|
|
since many tools considered _necessary_ for modern and idiomatic Go are simply
|
|
not for Rust, such as code generation tools. Such needs are instead fulfilled by
|
|
metaprogramming (macros) instead.
|
|
|
|
### Explicit error handling
|
|
|
|
Haha! I guess I'm the four-millionth person to write about how awful `if err !
|
|
= nil` is, but seriously, Rust's `?` operator and pattern matching abilities are
|
|
so fantastic.
|
|
|
|
You're gonna _love_ Rust in comparison.
|
|
|
|
## Rust is expressive
|
|
|
|
Now we're getting to some of the things Rust is simply better than Go at.
|
|
|
|
Being expressive has downsides. A common question I'll find myself asking is
|
|
"Which of these four ways is the _best_ way to do this?"
|
|
|
|
But I would rather be able to have the expressions in the code mirror the intent
|
|
_more_ than be stuck with Go's simplicity.
|
|
|
|
## Rust code has fewer bugs
|
|
|
|
Obviously this isn't entirely a fair thing to say, but it is true that Rust
|
|
simply makes it incredibly difficult (effectively impossible?) to have certain
|
|
kinds of bugs in your code as opposed to Go.
|
|
|
|
I simply write my code in Rust and when it compiles it works as I intended.
|
|
Rust code and the compiler make it much more difficult to represent invalid
|
|
states.
|
|
|
|
Meanwhile Go code has nil checks, implicit interfaces, and a few other things
|
|
you will inevitably run into.
|
|
|
|
That said, Go's tooling has been improving a _lot_ around this in my opinion;
|
|
`golangci-lint` is awesome and shores up much of these issues.
|
|
|
|
## Inline tests
|
|
|
|
I strongly dislike being required to put my unit tests in separate files. Call
|
|
me crazy, but having the tests in the same file is something I find appealing.
|
|
|
|
Also
|
|
[doctests](https://doc.rust-lang.org/rustdoc/write-documentation/documentation-tests.html).
|
|
The best kind of inline test. Gotta have 'em.
|
|
|
|
## Modules and dependencies
|
|
|
|
Go's module system got bolted on a little late and suffers from the Google
|
|
monorepo assumptions/expectations situation. In some ways, this is actually
|
|
very cool. In many ways, though, it's pretty annoying. These days, if you follow
|
|
the "modern" path, things can work great.
|
|
|
|
Rust has its own issues with Cargo and its ties to GitHub, but even those can be
|
|
worked around if you like.
|
|
|
|
Overall, Rust has a more familiar and approachable module system.
|
|
|
|
## Making Changes
|
|
|
|
When you change a hunk of Go code, you can't know for sure what else in the code
|
|
may need to change as a result. If you add a field to a struct, you may need to
|
|
find the places you are building that struct and update the code and hopefully
|
|
you don't miss a spot.
|
|
|
|
Rust makes changing an existing codebase less scary. It will tell you pretty
|
|
much everywhere you need to go and change other things as a result of your small
|
|
change. It totally changes the way you modify software.
|
|
|
|
## Documentation
|
|
|
|
Rust's standard library and popular crates' documentation is _generally_ much
|
|
better than Go's. Doctests are wonderful and examples are more common in Rust
|
|
crates than in Go modules.
|
|
|
|
## Macros
|
|
|
|
Having them is better than not. They are awesome. Yes, they make the compiler
|
|
slower. Yes, they can do awful stuff.
|
|
|
|
So does Go codegen.
|
|
|
|
But being able to derive implementations and affect them with attributes is
|
|
really phenomenal. Especially if you mess with something like `bevy` or `axum`.
|
|
|
|
## Traits
|
|
|
|
Rust forces you the implement a trait explicitly while Go does not. It's a small
|
|
thing, but this again lets the code communicate the intent of the programmer.
|
|
|
|
## No surprises
|
|
|
|
Nothing that executes before `main()` while Go has freakin' `init()` and of
|
|
course you can instantiate literally anything globally.
|
|
|
|
In Rust, you don't have this, which sucks and means you do `OnceLock`/
|
|
`OnceCell`/`lazy_static!` instead and it can be painful if you're so dead set on
|
|
doing something weird like that.
|
|
|
|
But I'll take no surprises and explicitness any day.
|
|
|
|
## Examples
|
|
|
|
I wanted to include a number of code examples I found particularly
|
|
Rust-favoring. A picture is worth a thousand words!
|
|
|
|
### Obvious: Error Handling
|
|
|
|
This is a simplified function in production right now:
|
|
|
|
```go
|
|
func (t *Bot) FinishReceivingRequest(req RequestInProgress) (result *Request, err error) {
|
|
result.Data = req.Data
|
|
|
|
assignee, err := t.GetAssigneeSlackUser(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to retrieve assignee's Slack user info: %w", err)
|
|
}
|
|
result.SlackAssigneeID = assignee.ID
|
|
|
|
manager, err := t.Slack.GetUserManager(assignee.ID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to retrieve the assignee's manager Slack user info: %w", err)
|
|
}
|
|
result.SlackAssigneeMgrID = manager.ID
|
|
|
|
issueSkeleton, err := req.Data.BuildJiraIssue(t, req)
|
|
if err != nil {
|
|
return nil, logrus.Errorf("Failed to create Jira issue object: %+v", err)
|
|
}
|
|
|
|
issue, res, err := t.Jira.CreateIssue(issueSkeleton)
|
|
defer res.Body.Close()
|
|
if err != nil {
|
|
_, _, err := t.Slack.PostMessage(t.Conf.Slack.Channels.RequestThreads,
|
|
slack.MsgOptionDisableLinkUnfurl(),
|
|
slack.MsgOptionBlocks(
|
|
slack.NewSectionBlock(
|
|
&slack.TextBlockObject{
|
|
Type: "mrkdwn",
|
|
Text: "I failed to create a Jira ticket. See logs for details.\n\nError: " + err.Error(),
|
|
}, nil, nil,
|
|
),
|
|
),
|
|
)
|
|
if err != nil {
|
|
logrus.Errorf("failed to send Jira ticket creation failed message to slack: %v", err)
|
|
}
|
|
}
|
|
result.JiraIssueKey = &issue.Key
|
|
logrus.Infof("Jira issue created: %s", result.JiraIssueKey)
|
|
|
|
channelId, ts, err := t.Slack.PostMessage(
|
|
t.Conf.Slack.Channels.RequestThreads,
|
|
slack.MsgOptionDisableLinkUnfurl(),
|
|
req.MainMessageBlocks(),
|
|
)
|
|
if err != nil {
|
|
return result, fmt.Errorf("failed to post Slack message for request -- this means there is data that has been created that nobody knows about: %w", err)
|
|
}
|
|
result.SlackTimeStamp = ts
|
|
result.SlackChannelId = channelId
|
|
|
|
// fetch permalink to the posted message to update jira ticket with
|
|
url, err := t.Slack.GetPermalink(&slack.PermalinkParameters{
|
|
Channel: channelId,
|
|
Ts: ts,
|
|
})
|
|
if err != nil {
|
|
// TODO: retry?
|
|
logrus.Errorf("Failed to retrieve permalink for Slack message for request -- this means we could not link the jira ticket to the slack thread: %s", err)
|
|
}
|
|
req.SlackThreadURL = url
|
|
|
|
if issue != nil {
|
|
rl := jira.RemoteLink{
|
|
Object: &jira.RemoteLinkObject{
|
|
URL: url,
|
|
Title: "Slack Thread",
|
|
},
|
|
}
|
|
_, res, err := t.Jira.AddIssueRemoteLink(issue.ID, &rl)
|
|
defer res.Body.Close()
|
|
if err != nil {
|
|
// TODO: retry?
|
|
logrus.Errorf("Failed to add remote link to Slack message thread (ts: '%s', link: '%s') for jira issue (key: '%s'): %s", ts, url, issue.Key, err)
|
|
}
|
|
}
|
|
|
|
logrus.Infof("Request data: %+v", req)
|
|
logrus.Tracef("request's db data json: %+v", req.DataJSON)
|
|
logrus.Tracef("request's db data: %+v", req.Data)
|
|
|
|
dbResult := t.DB.Create(req)
|
|
logrus.Tracef("request db create result: %+v", dbResult)
|
|
|
|
// it's not the end of the world if we cannot update the main message, so we try it, log it, and keep going
|
|
blocks, err = req.MainMessageBlocks()
|
|
if err != nil {
|
|
logrus.Errorf("Failed to build slack blocks for original message update: %v", err)
|
|
}
|
|
|
|
_, _, _, err = t.Slack.UpdateMessage(
|
|
t.Conf.Slack.Channels.RequestThreads,
|
|
req.SlackTimeStamp,
|
|
blocks,
|
|
)
|
|
if err != nil {
|
|
logrus.Errorf("Failed to update original message: %v", err)
|
|
}
|
|
|
|
|
|
_, _, err = t.Slack.PostMessage(
|
|
channelId,
|
|
slack.MsgOptionTS(ts),
|
|
slack.MsgOptionBlocks(req.FirstReplyMessageBlocks()...),
|
|
)
|
|
if err != nil {
|
|
logrus.Errorf("failed to post follow-up message in thread: %s", err)
|
|
}
|
|
|
|
if req.Data.NeedsApproval() {
|
|
req, err = t.ApprovalSetup(req)
|
|
if err != nil {
|
|
return result, fmt.Errorf("failed to setup approval: %w", err)
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
```
|
|
|
|
Here's how it looks in my Rust dream rewrite (thanks `color_eyre::Result`):
|
|
|
|
```rust
|
|
// fetch permalink to the posted message to update jira ticket with
|
|
url, err := t.Slack.GetPermalink(&slack.PermalinkParameters{
|
|
Channel: channelId,
|
|
Ts: ts,
|
|
})
|
|
if err != nil {
|
|
// TODO: retry?
|
|
logrus.Errorf("Failed to retrieve permalink for Slack message for request -- this means we could not link the jira ticket to the slack thread: %s", err)
|
|
}
|
|
req.SlackThreadURL = url
|
|
|
|
if issue != nil {
|
|
rl := jira.RemoteLink{
|
|
Object: &jira.RemoteLinkObject{
|
|
URL: url,
|
|
Title: "Slack Thread",
|
|
},
|
|
}
|
|
_, res, err := t.Jira.AddIssueRemoteLink(issue.ID, &rl)
|
|
defer res.Body.Close()
|
|
if err != nil {
|
|
// TODO: retry?
|
|
logrus.Errorf("Failed to add remote link to Slack message thread (ts: '%s', link: '%s') for jira issue (key: '%s'): %s", ts, url, issue.Key, err)
|
|
}
|
|
}
|
|
|
|
logrus.Infof("Request data: %+v", req)
|
|
logrus.Tracef("request's db data json: %+v", req.DataJSON)
|
|
logrus.Tracef("request's db data: %+v", req.Data)
|
|
|
|
dbResult := t.DB.Create(req)
|
|
logrus.Tracef("request db create result: %+v", dbResult)
|
|
|
|
// it's not the end of the world if we cannot update the main message, so we try it, log it, and keep going
|
|
blocks, err = req.MainMessageBlocks()
|
|
if err != nil {
|
|
logrus.Errorf("Failed to build slack blocks for original message update: %v", err)
|
|
}
|
|
|
|
_, _, _, err = t.Slack.UpdateMessage(
|
|
t.Conf.Slack.Channels.RequestThreads,
|
|
req.SlackTimeStamp,
|
|
blocks,
|
|
)
|
|
if err != nil {
|
|
logrus.Errorf("Failed to update original message: %v", err)
|
|
}
|
|
|
|
|
|
_, _, err = t.Slack.PostMessage(
|
|
channelId,
|
|
slack.MsgOptionTS(ts),
|
|
slack.MsgOptionBlocks(req.FirstReplyMessageBlocks()...),
|
|
)
|
|
if err != nil {
|
|
logrus.Errorf("failed to post follow-up message in thread: %s", err)
|
|
}
|
|
|
|
if req.Data.NeedsApproval() {
|
|
req, err = t.ApprovalSetup(req)
|
|
if err != nil {
|
|
return result, fmt.Errorf("failed to setup approval: %w", err)
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
```
|
|
|
|
# Why Not Rust
|
|
|
|
While I've made it clear that I prefer Rust, I also recognize that there are
|
|
things that I consider Go to handle better.
|
|
|
|
## Standard libraries
|
|
|
|
Go has a big standard library and just includes a ton of stuff. I wish Rust had
|
|
something similar for "blessed" "core" crates. I understand the standard library
|
|
for a systems-level language must be small, but that's how I feel.
|
|
|
|
### Other Builtins
|
|
|
|
Go can kind of manage its own versions and everything and you pretty much just
|
|
do `go whatever` and get on with your life.
|
|
|
|
With Rust, you need to learn a couple different tools (but mainly just `cargo`)
|
|
such as `rustup`.
|
|
|
|
## Go is Simple
|
|
|
|
It's easy to get caught up trying to do things in Rust the "right" way and work
|
|
with the borrow checker and think about the optimal solution -- to revel in the
|
|
computer science puzzle that the compiler presents to you.
|
|
|
|
In Go, that is simply less true. You will do the obvious thing at perhaps some
|
|
slight performance cost and get back to implementing your feature and shipping
|
|
code. It's almost always going to be perfectly sufficient and generally easy to
|
|
come back to and understand.
|
|
|
|
## Go Compilation Speed
|
|
|
|
The Go compiler is wonderfully quick. In my experience, though, there are
|
|
usually codegen tools you end up adopting that offset this speed and slow things
|
|
down again that require extra vigilance and tending to keep things fast.
|
|
|
|
With Rust, you have one compiler speed: not so fast. It's still sufficiently
|
|
quick for me, especially on a Ryzen 9 5950X or an Apple M1 Max. For most
|
|
projects, it does a good job being incremental, too.
|
|
|
|
But you will build Rust
|
|
|
|
## Build Tinkering
|
|
|
|
With Go, you're just gonna build the binary and run it.
|
|
|
|
With Rust, you _do_ have to think about `--release` performance -- such as when
|
|
writing Leetcode solutions against your friends.
|
|
|
|
I'm getting nitpicky here, but... it's my article.
|
|
|
|
## Versioning
|
|
|
|
Rust has issues with MSRV "promises" being largely unenforceable due to Cargo
|
|
lockfiles not being `--locked` by default. This is pretty mind-boggling to me.
|
|
Could also be a skill issue.
|