370 lines
11 KiB
Markdown
370 lines
11 KiB
Markdown
|
---
|
||
|
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.
|