This commit is contained in:
Daniel Flanagan 2024-06-06 17:04:11 -05:00
parent 1a90ac8620
commit d9deb85b45

369
content/rust-vs-go.md Normal file
View file

@ -0,0 +1,369 @@
---
title: "iex and dbg/1 without pry prompts"
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 consultancy on my Go code and help
me be 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.
# Why Rust
## Rust has _all_ of Go's superpowers
### Goroutines
Use `tokio::spawn`. Yes, they are different (mainly stackless versus
stackful) but the goal and considerations are effectively the same.
### Channels
Use `std::sync::mpsc`.
### Defer
Largely unnecessary thanks to the compiler, but you can implement your
own via the `Drop` trait or reach for `scopeguard`.
### 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
are not so for Rust, such as code generation. Such needs are instead fulfilled
by metaprogramming (macros).
### Explicit error handling
Hahaha!
You're gonna _love_ Rust in comparison.
## Rust is expressive
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 is factually incorrect. However, 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, pointers, 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.
## 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.
## 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.
Rust has its own issues with Cargo and its ties to GitHub, but even those can be
worked around if you like.
Rust also 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.
## Documentation
Rust's standard library and popular crates' documentation is _generally_ much
better than Go's. Doctests and examples are common.
## 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.
## 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.
## 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.
## 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.