首页 后端开发 Golang 发布 Viddy v.Migration 从 Go 到 Rust

发布 Viddy v.Migration 从 Go 到 Rust

Aug 22, 2024 pm 07:05 PM

介绍

在本文中,我想分享我在重新实现 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 中,可以返回错误的函数是这样定义的:

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 并查看结果。此功能可以更轻松地与其他人共享命令输出的更改历史记录。

顺便说一句,“Viddy”这个名字本身就是对电影的致敬,我计划继续将电影相关的主题融入到项目中。我特别喜欢这个新功能的名称“回顾”,因为它与这个主题相符。另外,日本动画电影Look Back也非常棒。

演示

Release of Viddy v.Migration from Go to Rust

关于图标

目前,Viddy 使用 Gopher 图标,但由于实现语言已切换为 Rust,这可能会引起一些混乱。然而,这个图标非常棒,所以我打算保持原样。 ?

“Viddy well,Gopher,viddy well”这句话现在可能也有稍微不同的含义。

结论

通过将 Viddy 从 Go 重写为 Rust 的挑战,我能够深入探索每种语言的差异和特点。 Rust 的错误传播和 Option 类型等功能被证明对于编写更安全、更简洁的代码非常有用。另一方面,Rust 的表达能力有时会成为压力的来源,尤其是当我觉得有必要编写尽可能最优雅的代码时。此外,Rust 中较小的标准库被认为是一个新的挑战。

尽管存在这些挑战,但优先发布版本并专注于让某些功能可用使得重写能够取得进展。社区对 RC 版本测试和改进的支持也是一个重要的动力。

展望未来,我计划继续使用 Rust 开发和维护 Viddy,以进一步提高我的语言技能。我希望这篇文章能为那些考虑使用 Rust 的人提供有用的参考。最后,如果您发现 Viddy 的代码有任何需要改进的地方,我将非常感谢您的反馈或 PR!

https://github.com/sachaos/viddy

以上是发布 Viddy v.Migration 从 Go 到 Rust的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

热门话题

Laravel 教程
1602
29
PHP教程
1504
276
如何在GO中构建Web服务器 如何在GO中构建Web服务器 Jul 15, 2025 am 03:05 AM

搭建一个用Go编写的Web服务器并不难,核心在于利用net/http包实现基础服务。1.使用net/http启动最简服务器:通过几行代码注册处理函数并监听端口;2.路由管理:使用ServeMux组织多个接口路径,便于结构化管理;3.常见做法:按功能模块分组路由,并可用第三方库支持复杂匹配;4.静态文件服务:通过http.FileServer提供HTML、CSS和JS文件;5.性能与安全:启用HTTPS、限制请求体大小、设置超时时间以提升安全性与性能。掌握这些要点后,扩展功能将更加容易。

进行音频/视频处理 进行音频/视频处理 Jul 20, 2025 am 04:14 AM

音视频处理的核心在于理解基本流程与优化方法。1.其基本流程包括采集、编码、传输、解码和播放,每个环节均有技术难点;2.常见问题如音画不同步、卡顿延迟、声音噪音、画面模糊等,可通过同步调整、编码优化、降噪模块、参数调节等方式解决;3.推荐使用FFmpeg、OpenCV、WebRTC、GStreamer等工具实现功能;4.性能管理方面应注重硬件加速、合理设置分辨率帧率、控制并发及内存泄漏问题。掌握这些关键点有助于提升开发效率和用户体验。

使用默认情况选择 使用默认情况选择 Jul 14, 2025 am 02:54 AM

select加default的作用是让select在没有其他分支就绪时执行默认行为,避免程序阻塞。1.非阻塞地从channel接收数据时,若channel为空,会直接进入default分支;2.结合time.After或ticker定时尝试发送数据,若channel满则不阻塞而跳过;3.防止死锁,在不确定channel是否被关闭时避免程序卡住;使用时需注意default分支会立即执行,不能滥用,且default与case互斥,不会同时执行。

在GO中开发Kubernetes运营商 在GO中开发Kubernetes运营商 Jul 25, 2025 am 02:38 AM

编写KubernetesOperator的最有效方式是使用Go语言结合Kubebuilder和controller-runtime。1.理解Operator模式:通过CRD定义自定义资源,编写控制器监听资源变化并执行调和循环以维护期望状态。2.使用Kubebuilder初始化项目并创建API,自动生成CRD、控制器和配置文件。3.在api/v1/myapp_types.go中定义CRD的Spec和Status结构体,运行makemanifests生成CRDYAML。4.在控制器的Reconcil

去休息API示例 去休息API示例 Jul 14, 2025 am 03:01 AM

如何快速实现一个Go编写的RESTAPI示例?答案是使用net/http标准库,按照以下三个步骤即可完成:1.设置项目结构并初始化模块;2.定义数据结构和处理函数,包括获取所有数据、根据ID获取单个数据、创建新数据;3.在main函数中注册路由并启动服务器。整个过程无需第三方库,通过标准库即可实现基本的RESTAPI功能,并可通过浏览器或Postman进行测试。

如何获得Golang测试的代码覆盖率 如何获得Golang测试的代码覆盖率 Jul 12, 2025 am 02:58 AM

使用gotest内置命令生成覆盖率数据:运行gotest-cover./...显示每个包的覆盖率百分比,或使用gotest-coverprofile=coverage.out./...生成详细报告,并通过gotoolcover-html=coverage.out-ocoverage.html查看具体未覆盖代码行。在CI中集成覆盖率报告:先生成coverage.out文件,再通过第三方工具如codecov或coveralls上传分析,例如使用curl--data-binary@coverage.o

如何为项目设置Golangci-lint 如何为项目设置Golangci-lint Jul 12, 2025 am 03:06 AM

golangci-lint的安装步骤为:1.使用二进制安装或Goinstall命令安装;2.验证安装是否成功;配置方法包括:3.创建.golangci.yml文件以启用/禁用linters、设置排除路径等;集成方式为:4.在CI/CD(如GitHubActions)中添加lint步骤,确保每次提交和PR自动运行lint检查。

进行接口{} vs 进行接口{} vs Jul 11, 2025 am 02:38 AM

在Go语言中,interface{}和any是完全相同的类型,从Go1.18开始,any被引入作为interface{}的别名,主要目的是提升代码的可读性和语义清晰度;1.any更适合用于表达“任意类型”的场景,如函数参数、map/slice元素类型、通用逻辑实现等;2.interface{}更适合用于定义接口行为、强调接口类型或兼容旧代码的情况;3.两者的性能和底层机制完全一致,编译器会将any替换为interface{},不会带来额外开销;4.使用时需注意类型安全问题,可能需要配合类型断言或

See all articles