In diesem Artikel möchte ich meine Erfahrungen und Erkenntnisse teilen, die ich bei der Neuimplementierung von Viddy, einem von mir entwickelten TUI-Tool, von Go to Rust für die Version v1.0.0 gewonnen habe. Viddy wurde ursprünglich als moderne Version des Watch-Befehls entwickelt, aber dieses Mal habe ich mich der Herausforderung gestellt, ihn in Rust neu zu implementieren. Ich hoffe, dass dieser Artikel als nützliche Referenz für diejenigen dient, die sich für die Entwicklung mit Rust interessieren.
https://github.com/sachaos/viddy
Viddy wurde als moderne Alternative zum Watch-Befehl entwickelt, der in Unix-ähnlichen Betriebssystemen zu finden ist. Zusätzlich zur Grundfunktionalität des Watch-Befehls bietet Viddy die folgenden Hauptfunktionen, die in der später erwähnten Demo besser veranschaulicht werden:
Ursprünglich wollte ich Viddy in Rust implementieren, aber aufgrund technischer Herausforderungen habe ich beschlossen, der Veröffentlichung Priorität einzuräumen und Go zu verwenden, eine Sprache, mit der ich besser vertraut war. Dieses Mal konnte ich diese Herausforderungen meistern und endlich mein ursprüngliches Ziel verwirklichen, was diese Veröffentlichung für mich besonders bedeutungsvoll macht.
Es ist wichtig anzumerken, dass ich mit der Go-Sprache selbst nicht unzufrieden war. Da es sich bei der ursprünglichen Implementierung jedoch eher um einen Proof of Concept (PoC) handelte, gab es viele Bereiche, die ich nach der Überprüfung verbessern wollte. Diese Bereiche waren zu Hindernissen für die Behebung von Fehlern und die Erweiterung der Funktionalität geworden. Dieser wachsende Wunsch, das Projekt von Grund auf neu aufzubauen, war ein wesentlicher Motivator.
Außerdem hatte ich ein starkes Interesse an Rust und als ich beim Erlernen der Sprache Fortschritte machte, wollte ich mein Wissen auf ein echtes Projekt anwenden. Obwohl ich Rust anhand von Büchern studiert hatte, fand ich es schwierig, die einzigartigen Eigenschaften der Sprache wirklich zu erfassen und ohne praktische Erfahrung ein Gefühl der Beherrschung zu erlangen.
Das Hauptaugenmerk bei der Neuimplementierung lag auf der Priorisierung der Veröffentlichung. Anstatt mich auf die optimale Implementierung einzulassen, beschloss ich, Optimierungen wie Speichernutzung und Codeprägnanz aufzuschieben und so schnell wie möglich eine Veröffentlichung herauszubringen. Obwohl dieser Ansatz vielleicht kein Grund zur Prahlerei ist, erlaubte er mir, die Umschreibung in einer unbekannten Sprache durchzusetzen, ohne entmutigt zu werden.
Zu diesem Zeitpunkt habe ich beispielsweise den Code durch häufiges Klonen implementiert, ohne die Eigentumsverhältnisse vollständig zu berücksichtigen. Es gibt viel Raum für Optimierungen, das Projekt hat also viel Verbesserungspotenzial!
Außerdem gibt es viele Stellen, an denen ich mithilfe von Methodenketten eleganter hätte schreiben können. Ich glaube, dass die Verwendung von Methodenketten die Verwendung von if- und for-Anweisungen hätte reduzieren können, wodurch der Code deklarativer geworden wäre. Mein begrenzter Rust-Wortschatz, gepaart mit meiner Zurückhaltung, mehr Recherche zu betreiben, veranlasste mich jedoch, viele Teile vorerst auf unkomplizierte Weise umzusetzen.
Sobald diese Version veröffentlicht ist, plane ich, die Eigentumsverhältnisse noch einmal zu prüfen, Optimierungen durchzuführen und den Code umzugestalten, um diese Bedenken auszuräumen. Wenn Sie den Code überprüfen und Bereiche bemerken, die verbessert werden könnten, würde ich mich sehr freuen, wenn Sie ein Problem eröffnen oder eine PR einreichen könnten, um Ihre Erkenntnisse zu teilen!
Während der Migration zu Rust sind mir einige Vor- und Nachteile im Vergleich zu Go aufgefallen. Das sind nur meine Eindrücke, und da ich mit Rust noch ein Anfänger bin, kann es sein, dass ich einige Missverständnisse habe. Wenn Ihnen Fehler oder Missverständnisse auffallen, würde ich mich über Ihr Feedback freuen!
In Rust können Sie durch die Weitergabe von Fehlern prägnanten Code schreiben, der frühzeitig zurückkehrt, wenn ein Fehler auftritt. In Go ist eine Funktion, die einen Fehler zurückgeben kann, wie folgt definiert:
func run() error { // cool code }
Und wenn Sie diese Funktion aufrufen, behandeln Sie den Fehler wie folgt. Wenn beispielsweise ein Fehler auftritt, können Sie den Fehler frühzeitig an den Anrufer zurücksenden:
func caller() error { err := run() if err != nil { return err } fmt.Println("Success") return nil }
In Rust wird eine Funktion, die einen Fehler zurückgeben kann, wie folgt geschrieben:
use anyhow::Result; fn run() -> Result<()> { // cool code }
Und wenn Sie den Fehler früh in der aufrufenden Funktion zurückgeben möchten, können Sie ihn prägnant schreiben, indem Sie das ? Betreiber:
fn caller() -> Result<()> { run()?; println!("Success"); return Ok(()); }
Anfangs war ich etwas verwirrt von dieser Syntax, aber als ich mich daran gewöhnt hatte, fand ich sie unglaublich prägnant und praktisch.
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.
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.
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.
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.
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.
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.
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:
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.
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.
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 |
❯ 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
❯ 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 ===============================================================================
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 |
# 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
# 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
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 |
In Go werden beispielsweise JSON-Serialisierung und -Deserialisierung von der Standardbibliothek unterstützt, in Rust müssen Sie jedoch Crates von Drittanbietern wie serde und serde_json verwenden. Darüber hinaus gibt es verschiedene Optionen für asynchrone Laufzeiten, die Sie selbst auswählen und integrieren müssen. Zwar gibt es Bibliotheken, die als De-facto-Standards gelten können, die starke Abhängigkeit von Bibliotheken Dritter gibt jedoch Anlass zur Sorge hinsichtlich erhöhter Wartungskosten.
Dennoch scheint es in Rust ratsam zu sein, die Denkweise zu ändern und offener für die Abhängigkeit von externen Kisten zu sein.
Für dieses Projekt habe ich eine Kiste namens Ratatui verwendet, um die TUI-Anwendung in Rust zu erstellen. Ratatui bietet Vorlagen an, die ich äußerst nützlich fand, daher möchte ich sie hier vorstellen.
Ähnlich wie GUI-Anwendungen sind TUI-Anwendungen ereignisgesteuert. Wenn beispielsweise eine Taste gedrückt wird, wird ein Ereignis ausgelöst und eine Aktion ausgeführt. Ratatui bietet die Funktionalität zum Rendern von TUI-Blöcken auf dem Terminal, verarbeitet jedoch keine Ereignisse selbst. Daher müssen Sie Ihren eigenen Mechanismus zum Empfangen und Bearbeiten von Ereignissen erstellen.
Die von Ratatui bereitgestellten Vorlagen enthalten diese Art von Struktur von Anfang an, sodass Sie schnell eine Anwendung erstellen können. Darüber hinaus enthalten die Vorlagen CI/CD-Setups mit GitHub-Aktionen, Tastenzuordnung und Stilkonfigurationen, die durch Lesen aus Dateien angepasst werden können.
Wenn Sie planen, eine TUI in Rust zu erstellen, empfehle ich dringend, die Verwendung dieser Vorlagen in Betracht zu ziehen.
Um die Community wissen zu lassen, dass Viddy v1.0.0 die in Rust neu implementierte Version ist, habe ich es über ein GitHub-Problem und auf Reddit angekündigt. Glücklicherweise führte dies zu verschiedenen Rückmeldungen und Fehlerberichten, und einige Mitwirkende fanden sogar selbst Probleme und reichten PRs ein. Ohne diese Community-Unterstützung hätte ich möglicherweise die Version veröffentlicht, in der noch viele Fehler vorhanden wären.
Diese Erfahrung erinnerte mich an die Freuden der Open-Source-Entwicklung. Es hat meine Motivation gestärkt und ich bin wirklich dankbar für die Hilfe der Community.
Viddy-Benutzer haben sich seit einiger Zeit eine Funktion gewünscht, die es ihnen ermöglichen würde, den Verlauf der Befehlsausgaben zu speichern und sie später zu überprüfen. Als Reaktion darauf haben wir in dieser Version eine „Lookback“-Funktion implementiert, die die Ausführungsergebnisse in SQLite speichert, sodass Sie Viddy nach Abschluss des Befehls neu starten und die Ergebnisse überprüfen können. Diese Funktion erleichtert das Teilen des Änderungsverlaufs von Befehlsausgaben mit anderen.
Übrigens ist der Name „Viddy“ selbst eine Anspielung auf das Kino, und ich habe vor, weiterhin filmbezogene Themen in das Projekt einzubeziehen. Der Name „Lookback“ für diese neue Funktion gefällt mir besonders gut, da er zu diesem Thema passt. Auch der japanische Animationsfilm Look Back war absolut fantastisch.
Derzeit verwendet Viddy ein Gopher-Symbol, aber da die Implementierungssprache auf Rust umgestellt wurde, könnte dies zu Verwirrung führen. Da das Symbol jedoch fantastisch ist, habe ich vor, es so zu belassen, wie es ist. ?
Der Satz „Viddy well, Gopher, viddy well“ könnte jetzt auch eine etwas andere Bedeutung haben.
Durch die Herausforderung, Viddy von Go to Rust neu zu schreiben, konnte ich die Unterschiede und Eigenschaften jeder Sprache eingehend erforschen. Funktionen wie die Fehlerausbreitung von Rust und der Option-Typ erwiesen sich als äußerst nützlich, um sichereren und prägnanteren Code zu schreiben. Andererseits wurde die Ausdruckskraft von Rust manchmal zu einer Quelle von Stress, insbesondere wenn ich das Gefühl hatte, einen möglichst eleganten Code schreiben zu müssen. Darüber hinaus wurde die kleinere Standardbibliothek in Rust als neue Herausforderung erkannt.
Trotz dieser Herausforderungen konnten wir die Neufassung vorantreiben, indem wir der Veröffentlichung Priorität einräumten und uns darauf konzentrierten, etwas Funktionales herauszubringen. Auch die Unterstützung der Community beim Testen und Verbessern der RC-Version war ein wesentlicher Motivator.
Für die Zukunft habe ich vor, Viddy in Rust weiterzuentwickeln und zu pflegen, um meine Sprachkenntnisse weiter zu verbessern. Ich hoffe, dass dieser Artikel als hilfreiche Referenz für diejenigen dient, die darüber nachdenken, Rust zu übernehmen. Wenn Sie schließlich Bereiche für Verbesserungen im Viddy-Code sehen, würde ich mich über Ihr Feedback oder Ihre PRs sehr freuen!
https://github.com/sachaos/viddy
Das obige ist der detaillierte Inhalt vonVeröffentlichung von Viddy v.Migration von Go to Rust. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!