發布 Viddy v.Migration 從 Go 到 Rust

WBOY
發布: 2024-08-22 19:05:33
原創
574 人瀏覽過

介紹

在本文中,我想分享我在重新實作 Viddy(我一直在開發的 TUI 工具)過程中獲得的經驗和見解,從 Go 到 Rust v1.0.0 版本。 Viddy 最初是作為 watch 命令的現代版本開發的,但這一次,我接受了用 Rust 重新實現它的挑戰。我希望這篇文章能為有興趣使用 Rust 進行開發的人提供有用的參考。

關於維迪

https://github.com/sachaos/viddy

Viddy 是作為類 Unix 作業系統中 watch 命令的現代替代品而開發的。除了 watch 指令的基本功能外,Viddy 還提供了以下關鍵功能,這些功能在後面提到的示範中得到了更好的說明:

  • 尋呼機功能:允許您捲動命令的輸出。
  • 時間機器模式:使您能夠查看過去的命令輸出。
  • 類似 Vim 的按鍵綁定

本來我的目標是用 Rust 實現 Viddy,但由於技術上的挑戰,我決定優先使用我更熟悉的語言 Go 來發布。這次,我克服了這些挑戰,終於實現了我最初的目標,讓這個版本對我來說特別有意義。

示範

Release of Viddy v.Migration from Go to Rust

重寫的動機

要注意的是,我對 Go 語言本身並沒有不滿意的地方。然而,由於最初的實現更多的是概念驗證 (PoC),因此經過審查,我希望改進許多領域。這些領域已經成為修復錯誤和擴展功能的障礙。從頭開始重建計畫的日益強烈的願望是一個重要的動力。

此外,我對 Rust 有著濃厚的興趣,隨著我學習這門語言的進步,我想將我的知識應用到一個真正的專案中。雖然我透過書本學習過 Rust,但我發現如果沒有實務經驗,要真正掌握語言的獨特功能並獲得掌握感是很有挑戰性的。

從重寫中獲得的見解

優先發布而不是完美實施

重新實現期間的主要焦點是確定發布的優先順序。我決定推遲記憶體使用和程式碼簡潔性等優化,並致力於盡快發布版本,而不是陷入實現最佳實現的困境。雖然這種方法可能不值得誇耀,但它讓我能夠用不熟悉的語言重寫,而不會灰心喪志。

例如,在這個階段,我使用頻繁克隆的方式實現程式碼,而沒有充分考慮所有權。有很大的優化空間,所以該專案有很大的改進潛力!

此外,有很多部分我可以用方法鏈寫得更優雅。我相信使用方法鏈可以減少 if 和 for 語句的使用,使程式碼更具聲明性。然而,我有限的 Rust 詞彙量,加上我不願意做更多的研究,導致我現在以簡單的方式實現了許多部分。

此版本發布後,我計劃重新審視所有權、執行最佳化並重構程式碼以解決這些問題。如果您碰巧查看程式碼並發現任何可以改進的地方,如果您能提出問題或提交 PR 來分享您的見解,我將不勝感激!

用 Rust 重寫的優點和缺點

在遷移到 Rust 的過程中,我注意到了與 Go 相比的一些優點和缺點。這些只是我的印象,由於我還是 Rust 的初學者,我可能會有一些誤解。如果您發現任何錯誤或誤解,我將不勝感激您的回饋!

?傳播錯誤

在 Rust 中,傳播錯誤允許您編寫簡潔的程式碼,並在錯誤發生時儘早返回。在 Go 中,可以傳回錯誤的函數是這樣定義的:

雷雷

當你呼叫這個函數時,你會像這樣處理錯誤。例如,如果發生錯誤,您可能會提前將錯誤傳回給呼叫者:

雷雷

在 Rust 中,可以回傳錯誤的函數是這樣寫的:

雷雷

如果你想在呼叫函數中儘早回傳錯誤,你可以使用 ? 來簡潔地寫。營運商:

雷雷

起初,我對這種語法有點困惑,但是一旦習慣了它,我發現它非常簡潔和方便。

? Option Type

In Go, it's common to use pointer types to represent nullable values. However, this approach is not always safe. I often encountered runtime errors when trying to access nil elements. In Rust, the Option type allows for safe handling of nullable values. For example:

fn main() { // Define a variable of Option type let age: Option = Some(33); // Use match to handle the Option type match age { Some(value) => println!("The user's age is {}.", value), None => println!("The age is not set."), } // Use if let for concise handling if let Some(value) = age { println!("Using if let, the user's age is {}.", value); } else { println!("Using if let, the age is not set."); } // Set age to 20 if it's not defined let age = age.unwrap_or(20); }
登入後複製

As shown in the final example, the Option type comes with various useful methods. Using these methods allows for concise code without needing to rely heavily on if or match statements, which I find to be a significant advantage.

? The Joy of Writing Clean Code

It's satisfying to write clean and concise code using pattern matching, method chaining, and the mechanisms mentioned earlier. It reminds me of the puzzle-like joy that programming can bring.

For example, the following function in Viddy parses a string passed as a flag to determine the command execution interval and returns a Duration.

By using the humantime crate, the function can parse time intervals specified in formats like 1s or 5m. If parsing fails, it assumes the input is in seconds and tries to parse it accordingly.

// https://github.com/sachaos/viddy/blob/4dd222edf739a672d4ca4bdd33036f524856722c/src/cli.rs#L96-L105 fn parse_duration_from_str(s: &str) -> Result { match humantime::parse_duration(s) { Ok(d) => Ok(Duration::from_std(d)?), Err(_) => { // If the input is only a number, we assume it's in seconds let n = s.parse::()?; Ok(Duration::milliseconds((n * 1000.0) as i64)) } } }
登入後複製

I find it satisfying when I can use match to write code in a more declarative way. However, as I will mention later, this code can still be shortened and made even more declarative.

? Fewer Runtime Errors

Thanks to features like the Option type, which ensure a certain level of safety at compile time, I found that there were fewer runtime errors during development. The fact that if the code compiles, it almost always runs without issues is something I truly appreciate.

? Helpful Compiler

For example, let's change the argument of the function that parses a time interval string from &str to str:

fn parse_duration_from_str(s: str /* Before: &str */) -> Result { match humantime::parse_duration(s) { Ok(d) => Ok(Duration::from_std(d)?), Err(_) => { // If the input is only a number, we assume it's in seconds let n = s.parse::()?; Ok(Duration::milliseconds((n * 1000.0) as i64)) } } }
登入後複製

When you try to compile this, you get the following error:

error[E0308]: mismatched types --> src/cli.rs:97:37 | 97 | match humantime::parse_duration(s) { | ------------------------- ^ expected `&str`, found `str` | | | arguments to this function are incorrect | note: function defined here --> /Users/tsakao/.cargo/registry/src/index.crates.io-6f17d22bba15001f/humantime-2.1.0/src/duration.rs:230:8 | 230 | pub fn parse_duration(s: &str) -> Result { | ^^^^^^^^^^^^^^ help: consider borrowing here | 97 | match humantime::parse_duration(&s) { | +
登入後複製

As you can see from the error message, it suggests that changing the s argument in the humantime::parse_duration function to &s might fix the issue. I found the compiler’s error messages to be incredibly detailed and helpful, which is a great feature.

? The Stress of Thinking "Could This Be Written More Elegantly?"

Now, let's move on to some aspects that I found a bit challenging.

This point is closely related to the satisfaction of writing clean code, but because Rust is so expressive and offers many ways to write code, I sometimes felt stressed thinking, "Could I write this more elegantly?" In Go, I often wrote straightforward code without overthinking it, which allowed me to focus more on the business logic rather than the specific implementation details. Personally, I saw this as a positive aspect. However, with Rust, the potential to write cleaner code often led me to spend more mental energy searching for better ways to express the logic.

For example, when I asked GitHub Copilot about the parse_duration_from_str function mentioned earlier, it suggested that it could be shortened like this:

fn parse_duration_from_str(s: &str) -> Result { humantime::parse_duration(s) .map(Duration::from_std) .or_else(|_| s.parse::().map(|secs| Duration::milliseconds((secs * 1000.0) as i64))) }
登入後複製

The match expression is gone, and the code looks much cleaner—it's cool. But because Rust allows for such clean code, as a beginner still building my Rust vocabulary, I sometimes felt stressed, thinking I could probably make my code even more elegant.

Additionally, preferences for how clean or "cool" code should be can vary from person to person. I found myself a bit unsure of how far to take this approach. However, this might just be a matter of experience and the overall proficiency of the team.

? Smaller Standard Library Compared to Go

As I’ll mention in a later section, I found that Rust’s standard library feels smaller compared to Go’s. In Go, the standard library is extensive and often covers most needs, making it a reliable choice. In contrast, with Rust, I often had to rely on third-party libraries.

While using third-party libraries introduces some risks, I’ve come to accept that this is just part of working with Rust.

I believe this difference may stem from the distinct use cases for Rust and Go. This is just a rough impression, but it seems that Go primarily covers web and middleware applications, while Rust spans a broader range, including web, middleware, low-level programming, systems programming, and embedded systems. Developing a standard library that covers all these areas would likely be quite costly. Additionally, since Rust’s compiler is truly outstanding, I suspect that a significant amount of development resources have been focused there.

? Things I Don’t Understand or Find Difficult

Honestly, I do find Rust difficult at times, and I realize I need to study more. Here are some areas in Viddy that I’m using but haven’t fully grasped yet:

  • Concurrent programming and asynchronous runtimes
  • How to do Dependency Injection
  • The "magic" of macros

Additionally, since the language is so rich in features, I feel there’s a lot I don’t even know that I don’t know. As I continue to maintain Viddy, I plan to experiment and study more to deepen my understanding.

Rust vs. Go by the Numbers

While it’s not entirely fair to compare the two languages, since the features provided aren’t exactly the same, I thought it might be interesting to compare the number of lines of source code, build times, and the number of dependencies between Rust and Go. To minimize functional differences, I measured using the RC version of Viddy (v1.0.0-rc.1), which does not include the feature that uses SQLite. For Go, I used the latest Go implementation release of Viddy (v0.4.0) for the measurements.

Lines of Source Code

As I’ll mention later, the Rust implementation uses a template from the Ratatui crate, which is designed for TUI development. This template contributed to a significant amount of generated code. Additionally, some features have been added, which likely resulted in the higher line count. Generally, I found that Rust allows for more expressive code with fewer lines compared to Go.

Lines of Code
Go 1987
Rust 4622
Go
❯ tokei =============================================================================== Language Files Lines Code Comments Blanks =============================================================================== Go 8 1987 1579 43 365 Makefile 1 23 18 0 5 ------------------------------------------------------------------------------- (omitted) =============================================================================== Total 10 2148 1597 139 412
登入後複製
Rust
❯ tokei =============================================================================== Language Files Lines Code Comments Blanks =============================================================================== (omitted) ------------------------------------------------------------------------------- Rust 30 4622 4069 30 523 |- Markdown 2 81 0 48 33 (Total) 4703 4069 78 556 =============================================================================== Total 34 4827 4132 124 571 ===============================================================================
登入後複製

Build Time Comparison

The Rust implementation includes additional features and more lines of code, so it’s not a completely fair comparison. However, even considering these factors, it’s clear that Rust builds are slower than Go builds. That said, as mentioned earlier, Rust’s compiler is extremely powerful, providing clear guidance on how to fix issues, so this slower build time is somewhat understandable.

Go Rust
Initial Build 10.362s 52.461s
No Changes Build 0.674s 0.506s
Build After Changing Code 1.302s 6.766s
Go
# After running go clean -cache ❯ time go build -ldflags="-s -w" -trimpath go build -ldflags="-s -w" -trimpath 40.23s user 11.83s system 502% cpu 10.362 total # Subsequent builds ❯ time go build -ldflags="-s -w" -trimpath go build -ldflags="-s -w" -trimpath 0.54s user 0.83s system 203% cpu 0.674 total # After modifying main.go ❯ time go build -ldflags="-s -w" -trimpath go build -ldflags="-s -w" -trimpath 1.07s user 0.95s system 155% cpu 1.302 total
登入後複製
Rust
# After running cargo clean ❯ time cargo build --release ...(omitted) Finished `release` profile [optimized] target(s) in 52.36s cargo build --release 627.85s user 45.07s system 1282% cpu 52.461 total # Subsequent builds ❯ time cargo build --release Finished `release` profile [optimized] target(s) in 0.40s cargo build --release 0.21s user 0.23s system 87% cpu 0.506 total # After modifying main.rs ❯ time cargo build --release Compiling viddy v1.0.0-rc.0 Finished `release` profile [optimized] target(s) in 6.67s cargo build --release 41.01s user 1.13s system 622% cpu 6.766 total
登入後複製

Comparison of Non-Standard Library Dependencies

In Go, I tried to rely on the standard library as much as possible. However, as mentioned earlier, Rust's standard library (crates) is smaller compared to Go's, leading to greater reliance on external crates. When we look at the number of libraries Viddy directly depends on, the difference is quite noticeable:

Number of Dependencies
Go 13
Rust 38

예를 들어 Go에서는 JSON 직렬화 및 역직렬화가 표준 라이브러리에서 지원되지만 Rust에서는 serde 및 serde_json과 같은 타사 크레이트를 사용해야 합니다. 또한 비동기 런타임에 대한 다양한 옵션이 있으며 이를 직접 선택하고 통합해야 합니다. 사실상의 표준으로 간주될 수 있는 라이브러리가 있지만 타사 라이브러리에 대한 과도한 의존으로 인해 유지 관리 비용이 증가할 수 있다는 우려가 제기됩니다.

그래도 Rust에서는 사고방식을 조정하고 외부 상자에 따라 더 개방적인 태도를 취하는 것이 현명한 것 같습니다.

기타 주제

Ratatui 템플릿은 편리합니다

이 프로젝트에서는 Ratatui라는 상자를 사용하여 Rust에서 TUI 애플리케이션을 구축했습니다. Ratatui는 제가 매우 유용하다고 생각하는 템플릿을 제공하므로 여기에 소개하고 싶습니다.

GUI 애플리케이션과 유사하게 TUI 애플리케이션은 이벤트 중심입니다. 예를 들어 키를 누르면 이벤트가 트리거되고 일부 작업이 수행됩니다. Ratatui는 터미널에서 TUI 블록을 렌더링하는 기능을 제공하지만 이벤트 자체를 처리하지는 않습니다. 따라서 이벤트를 수신하고 처리하기 위한 고유한 메커니즘을 만들어야 합니다.

Ratatui에서 제공하는 템플릿에는 처음부터 이러한 구조가 포함되어 있어 애플리케이션을 빠르게 구축할 수 있습니다. 또한 템플릿에는 GitHub Actions를 사용한 CI/CD 설정, 키 매핑, 파일에서 읽어 맞춤설정할 수 있는 스타일 구성이 함께 제공됩니다.

Rust에서 TUI를 생성할 계획이라면 이 템플릿 사용을 고려해 보시기 바랍니다.

커뮤니티와 Reddit에서 RC 테스트 요청

Viddy v1.0.0이 Rust에서 다시 구현된 버전임을 커뮤니티에 알리기 위해 GitHub Issue와 Reddit을 통해 발표했습니다. 다행히 이로 인해 다양한 피드백과 버그 리포트가 나왔고, 일부 기여자는 스스로 문제를 발견해 PR을 제출하기도 했습니다. 이러한 커뮤니티 지원이 없었다면 버그가 여전히 많은 버전을 출시했을 수도 있습니다.

이 경험은 나에게 오픈 소스 개발의 즐거움을 일깨워주었습니다. 덕분에 의욕이 더욱 높아졌고 커뮤니티의 도움에 진심으로 감사드립니다.

Viddy의 새로운 기능

한동안 Viddy 사용자들은 명령 출력 기록을 저장하고 나중에 검토할 수 있는 기능을 요청해 왔습니다. 이에 대한 대응으로 이번 릴리스에서는 실행 결과를 SQLite에 저장하는 "룩백" 기능을 구현했습니다. 이를 통해 명령이 완료된 후 Viddy를 다시 시작하고 결과를 검토할 수 있습니다. 이 기능을 사용하면 명령 출력의 변경 내역을 다른 사람과 더 쉽게 공유할 수 있습니다.

그나저나, "비디"라는 이름 자체가 영화에 대한 고개를 끄덕이는 것인데, 저는 계속해서 영화와 관련된 주제를 프로젝트에 담아낼 계획입니다. 저는 특히 이 새로운 기능에 대한 "룩백"이라는 이름을 좋아합니다. 이 주제와 일치하기 때문입니다. 그리고 일본 애니메이션 영화Look Back도 정말 환상적이었어요

데모

Release of Viddy v.Migration from Go to Rust

아이콘 정보

현재 Viddy에서는 Gopher 아이콘을 사용하고 있지만 구현 언어가 Rust로 전환되었기 때문에 이로 인해 약간의 혼란이 발생할 수 있습니다. 하지만 아이콘이 너무 멋져서 그대로 유지할 생각입니다. ?

"Viddy Well, Gopher, Viddy Well"이라는 문구도 지금은 조금 다른 의미로 받아들여졌을지도 모르겠습니다.

결론

Go에서 Rust로 Viddy를 다시 작성하는 도전을 통해 각 언어의 차이점과 특성을 깊이 탐구할 수 있었습니다. Rust의 오류 전파 및 Option 유형과 같은 기능은 보다 안전하고 간결한 코드를 작성하는 데 매우 유용한 것으로 입증되었습니다. 반면에 Rust의 표현력은 때때로 스트레스의 원인이 되기도 했습니다. 특히 가능한 가장 우아한 코드를 작성해야 한다고 느꼈을 때 더욱 그렇습니다. 게다가 Rust의 더 작은 표준 라이브러리는 새로운 도전으로 인식되었습니다.

이러한 어려움에도 불구하고 릴리스 우선순위를 정하고 기능적인 기능을 구현하는 데 집중함으로써 재작성이 진행될 수 있었습니다. RC 버전을 테스트하고 개선하는 데 있어 커뮤니티의 지원도 중요한 동기가 되었습니다.

앞으로 저는 언어 실력을 더욱 향상시키기 위해 Rust에서 Viddy를 계속 개발하고 유지 관리할 계획입니다. 이 기사가 Rust를 고려하는 사람들에게 유용한 참고 자료가 되기를 바랍니다. 마지막으로 Viddy의 코드에 개선할 부분이 있으면 피드백이나 PR을 보내주시면 정말 감사하겠습니다!

https://github.com/sachaos/viddy

以上是發布 Viddy v.Migration 從 Go 到 Rust的詳細內容。更多資訊請關注PHP中文網其他相關文章!

來源:dev.to
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!