From d9deb85b4573286613fb91d1d31810645a4a53b9 Mon Sep 17 00:00:00 2001 From: Daniel Flanagan Date: Thu, 6 Jun 2024 17:04:11 -0500 Subject: [PATCH] Versus --- content/rust-vs-go.md | 369 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 369 insertions(+) create mode 100644 content/rust-vs-go.md diff --git a/content/rust-vs-go.md b/content/rust-vs-go.md new file mode 100644 index 0000000..479cadf --- /dev/null +++ b/content/rust-vs-go.md @@ -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. + + + +# 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.