>  기사  >  백엔드 개발  >  Viddy v.Migration from Go to Rust 출시

Viddy v.Migration from Go to Rust 출시

WBOY
WBOY원래의
2024-08-22 19:05:33737검색

소개

이 기사에서는 v1.0.0 릴리스용 Go to Rust에서 제가 개발해 온 TUI 도구인 Viddy를 다시 구현하면서 얻은 경험과 통찰력을 공유하고 싶습니다. Viddy는 원래 watch 명령의 최신 버전으로 개발되었지만 이번에는 Rust에서 다시 구현하는 데 도전했습니다. 이 글이 Rust를 이용한 개발에 관심이 있는 분들에게 유용한 참고 자료가 되기를 바랍니다.

비디 소개

https://github.com/sachaos/viddy

Viddy는 Unix 계열 운영 체제에 있는 watch 명령에 대한 현대적인 대안으로 개발되었습니다. watch 명령의 기본 기능 외에도 Viddy는 다음과 같은 주요 기능을 제공하며 이는 나중에 언급되는 데모에서 더 잘 설명됩니다.

  • 페이저 기능: 명령 출력을 스크롤할 수 있습니다.
  • 타임머신 모드: 과거 명령 출력을 검토할 수 있습니다.
  • Vim과 유사한 키 바인딩

원래는 Viddy를 Rust로 구현하려고 했으나 기술적인 문제로 인해 좀 더 익숙한 언어인 Go를 사용하여 출시를 우선적으로 하기로 결정했습니다. 이번에 그러한 어려움을 극복하고 마침내 처음의 목표를 실현할 수 있게 되어 이번 출시는 나에게 특히 의미가 깊었습니다.

데모

Release of Viddy v.Migration from Go to Rust

재작성 동기

저는 Go 언어 자체에 불만이 없었다는 점을 기억하는 것이 중요합니다. 하지만 원래 구현은 개념 증명(PoC)에 가까웠기 때문에 검토를 통해 개선하고 싶은 부분이 많았습니다. 이러한 영역은 버그 수정과 기능 확장에 장애물이 되었습니다. 프로젝트를 처음부터 다시 구축하려는 열망이 커져가는 것이 중요한 동기가 되었습니다.

게다가 저는 Rust에 큰 관심을 갖고 있었고, 언어를 배우면서 제가 알고 있는 지식을 실제 프로젝트에 적용하고 싶었습니다. 비록 책을 통해 Rust를 공부했지만, 실습 경험 없이는 언어의 고유한 기능을 진정으로 파악하고 숙달감을 얻는 것이 어렵다는 것을 알았습니다.

재작성을 통해 얻은 통찰력

완벽한 구현보다 릴리스 우선 순위

재구현 과정에서 가장 중점을 두었던 점은 출시 우선순위를 정하는 것이었습니다. 가장 최적의 구현을 달성하는 데 얽매이기보다는 메모리 사용 및 코드 간결성과 같은 최적화를 연기하기로 결정하고 최대한 빨리 릴리스를 출시하는 것을 목표로 했습니다. 이 접근 방식은 자랑할 만한 것은 아니지만, 낙담하지 않고 낯선 언어로 다시 작성할 수 있게 해주었습니다.

예를 들어 이 단계에서는 소유권을 충분히 고려하지 않고 빈번한 복제를 사용하여 코드를 구현했습니다. 최적화할 여지가 많기 때문에 프로젝트에는 개선의 여지가 많습니다!

이 외에도 메소드 체인을 사용하면 좀 더 우아하게 작성할 수 있었던 부분이 많습니다. 메소드 체인을 사용하면 if 및 for 문의 사용이 줄어들어 코드가 더 선언적으로 만들어질 수 있다고 생각합니다. 그러나 제한된 Rust 어휘와 더 많은 조사를 꺼리는 점 때문에 지금은 많은 부분을 간단한 방식으로 구현하게 되었습니다.

이번 릴리스가 출시되면 이러한 문제를 해결하기 위해 소유권을 다시 확인하고, 최적화를 수행하고, 코드를 리팩터링할 계획입니다. 코드를 검토하고 개선할 수 있는 부분을 발견했다면 문제를 제기하거나 PR을 제출하여 통찰력을 공유해 주시면 진심으로 감사하겠습니다!

Rust에서 다시 작성하는 것의 장점과 단점

Rust로 마이그레이션하는 과정에서 Go에 비해 몇 가지 장단점을 언급했습니다. 이것은 단지 제 감상일 뿐이고 아직 Rust의 초보이기 때문에 오해가 있을 수도 있습니다. 혹시 실수나 오해가 있다면 피드백 주시면 감사하겠습니다!

? 오류 전파

Rust에서는 오류 전파를 통해 오류 발생 시 조기에 반환되는 간결한 코드를 작성할 수 있습니다. Go에서는 오류를 반환할 수 있는 함수를 다음과 같이 정의합니다.

func run() error {
    // cool code
}

그리고 이 함수를 호출하면 이런 오류를 처리하게 됩니다. 예를 들어 오류가 발생하면 호출자에게 오류를 조기에 반환할 수 있습니다.

func caller() error {
    err := run()
    if err != nil {
        return err
    }

    fmt.Println("Success")
    return nil
}

Rust에서는 오류를 반환할 수 있는 함수를 다음과 같이 작성합니다.

use anyhow::Result;

fn run() -> Result<()> {
    // cool code
}

그리고 함수 호출 초기에 오류를 반환하고 싶다면 ? 연산자:

fn caller() -> Result<()> {
    run()?;
    println!("Success");
    return Ok(());
}

처음에는 이 구문이 좀 헷갈렸는데, 익숙해지고 나니 엄청나게 간결하고 편리하네요.

? 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<u32> = 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<Duration> {
    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::<f64>()?;
            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<Duration> {
    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::<f64>()?;
            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<Duration, Error> {
    |        ^^^^^^^^^^^^^^
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<Duration> {
    humantime::parse_duration(s)
        .map(Duration::from_std)
        .or_else(|_| s.parse::<f64>().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를 다시 시작하고 결과를 검토할 수 있습니다. 이 기능을 사용하면 명령 출력의 변경 내역을 다른 사람과 더 쉽게 공유할 수 있습니다.

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

데모

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 from Go to Rust 출시의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.