Maison > Article > développement back-end > Sortie de Viddy v.Migration de Go vers Rust
Dans cet article, j'aimerais partager mes expériences et mes connaissances acquises lors de la réimplémentation de Viddy, un outil TUI que j'ai développé, de Go à Rust pour la version v1.0.0. Viddy a été initialement développé comme une version moderne de la commande watch, mais cette fois, j'ai relevé le défi de la réimplémenter dans Rust. J'espère que cet article servira de référence utile à ceux qui souhaitent développer avec Rust.
https://github.com/sachaos/viddy
Viddy a été développé comme une alternative moderne à la commande watch trouvée dans les systèmes d'exploitation de type Unix. En plus des fonctionnalités de base de la commande watch, Viddy offre les fonctionnalités clés suivantes, qui sont mieux illustrées dans la démo mentionnée plus loin :
À l'origine, mon objectif était d'implémenter Viddy dans Rust, mais en raison de problèmes techniques, j'ai décidé de donner la priorité à la version en utilisant Go, un langage que je connaissais mieux. Cette fois, j'ai pu surmonter ces défis et enfin atteindre mon objectif initial, ce qui rend cette version particulièrement significative pour moi.
Il est important de noter que je n'ai eu aucune insatisfaction avec le langage Go lui-même. Cependant, comme la mise en œuvre initiale était davantage une preuve de concept (PoC), il y avait de nombreux domaines que, après examen, je souhaitais améliorer. Ces domaines étaient devenus des obstacles à la correction des bugs et à l’extension des fonctionnalités. Ce désir croissant de reconstruire le projet à partir de zéro a été une motivation importante.
De plus, j'avais un fort intérêt pour Rust et, au fur et à mesure que je progressais dans l'apprentissage du langage, j'avais envie d'appliquer mes connaissances à un vrai projet. Même si j'avais étudié Rust à travers des livres, j'ai trouvé difficile de vraiment saisir les caractéristiques uniques du langage et d'acquérir un sentiment de maîtrise sans expérience pratique.
L'objectif principal lors de la réimplémentation était de donner la priorité à la version. Plutôt que de me concentrer sur la réalisation de l'implémentation la plus optimale, j'ai décidé de reporter les optimisations telles que l'utilisation de la mémoire et la concision du code et j'ai cherché à publier une version le plus rapidement possible. Même si cette approche n’est peut-être pas quelque chose dont je peux me vanter, elle m’a permis de poursuivre la réécriture dans une langue inconnue sans me décourager.
Par exemple, à ce stade, j'ai implémenté le code en utilisant des clonages fréquents sans pleinement considérer la propriété. Il y a beaucoup de place à l'optimisation, le projet a donc beaucoup de potentiel d'amélioration !
De plus, il existe de nombreuses parties où j'aurais pu écrire de manière plus élégante en utilisant des chaînes de méthodes. Je pense que l'utilisation de chaînes de méthodes aurait pu réduire l'utilisation des instructions if et for, rendant le code plus déclaratif. Cependant, mon vocabulaire Rust limité, combiné à ma réticence à faire plus de recherches, m'a amené à implémenter de nombreuses parties de manière simple pour l'instant.
Une fois cette version publiée, je prévois de revoir la propriété, d'effectuer des optimisations et de refactoriser le code pour répondre à ces problèmes. Si vous examinez le code et remarquez des domaines qui pourraient être améliorés, j'apprécierais grandement que vous puissiez ouvrir un problème ou soumettre un PR pour partager vos idées !
Dans le processus de migration vers Rust, j'ai noté quelques avantages et inconvénients par rapport à Go. Ce ne sont que mes impressions, et comme je suis encore débutant avec Rust, je pourrais avoir quelques malentendus. Si vous repérez des erreurs ou des idées fausses, j’apprécierais vos commentaires !
Dans Rust, la propagation des erreurs vous permet d'écrire du code concis qui revient tôt lorsqu'une erreur se produit. Dans Go, une fonction pouvant renvoyer une erreur est définie comme ceci :
func run() error { // cool code }
Et lorsque vous appelez cette fonction, vous gérez l'erreur comme ceci. Par exemple, si une erreur se produit, vous pouvez renvoyer l'erreur plus tôt à l'appelant :
func caller() error { err := run() if err != nil { return err } fmt.Println("Success") return nil }
Dans Rust, une fonction qui peut renvoyer une erreur s'écrit comme ceci :
use anyhow::Result; fn run() -> Result<()> { // cool code }
Et si vous souhaitez renvoyer l'erreur au début de la fonction appelante, vous pouvez l'écrire de manière concise en utilisant le ? opérateur :
fn caller() -> Result<()> { run()?; println!("Success"); return Ok(()); }
Au début, j'étais un peu dérouté par cette syntaxe, mais une fois que je m'y suis habitué, je l'ai trouvée incroyablement concise et pratique.
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 |
Par exemple, dans Go, la sérialisation et la désérialisation JSON sont prises en charge par la bibliothèque standard, mais dans Rust, vous devez utiliser des caisses tierces comme serde et serde_json. De plus, il existe diverses options pour les environnements d'exécution asynchrones, et vous devez les sélectionner et les intégrer vous-même. Bien qu'il existe des bibliothèques qui peuvent être considérées comme des standards de facto, la forte dépendance à l'égard de bibliothèques tierces soulève des inquiétudes quant à l'augmentation des coûts de maintenance.
Cela dit, dans Rust, il semble sage d'ajuster votre état d'esprit et d'être plus ouvert à dépendre de caisses externes.
Pour ce projet, j'ai utilisé une caisse appelée Ratatui pour créer l'application TUI dans Rust. Ratatui propose des modèles que j'ai trouvés extrêmement utiles, j'aimerais donc les présenter ici.
Semblables aux applications GUI, les applications TUI sont pilotées par des événements. Par exemple, lorsqu'une touche est enfoncée, un événement est déclenché et une action est effectuée. Ratatui fournit la fonctionnalité permettant d'afficher les blocs TUI sur le terminal, mais il ne gère pas les événements par lui-même. Par conséquent, vous devez créer votre propre mécanisme de réception et de gestion des événements.
Les modèles fournis par Ratatui incluent dès le départ ce genre de structure, vous permettant de construire rapidement une application. De plus, les modèles sont livrés avec des configurations CI/CD utilisant les actions GitHub, le mappage de touches et les configurations de style qui peuvent être personnalisées en lisant à partir de fichiers.
Si vous envisagez de créer une TUI dans Rust, je vous recommande fortement d'envisager l'utilisation de ces modèles.
Pour faire savoir à la communauté que Viddy v1.0.0 est la version réimplémentée dans Rust, je l'ai annoncé via un Issue GitHub et sur Reddit. Heureusement, cela a donné lieu à divers retours et rapports de bugs, et certains contributeurs ont même trouvé des problèmes par eux-mêmes et soumis des PR. Sans ce soutien de la communauté, j'aurais peut-être publié la version avec de nombreux bugs encore présents.
Cette expérience m'a rappelé les joies du développement open source. Cela a boosté ma motivation et je suis vraiment reconnaissant pour l'aide de la communauté.
Depuis un certain temps, les utilisateurs de Viddy réclament une fonctionnalité qui leur permettrait de sauvegarder l'historique des sorties de commandes et de les consulter ultérieurement. En réponse, nous avons implémenté une fonctionnalité « lookback » dans cette version qui enregistre les résultats de l'exécution dans SQLite, vous permettant de relancer Viddy une fois la commande terminée et de revoir les résultats. Cette fonctionnalité facilite le partage de l'historique des modifications des sorties de commandes avec d'autres.
D'ailleurs, le nom « Viddy » lui-même est un clin d'œil au cinéma, et j'ai l'intention de continuer à intégrer des thèmes liés au cinéma dans le projet. J'aime particulièrement le nom « lookback » pour cette nouvelle fonctionnalité, car il s'aligne sur ce thème. De plus, le film d'animation japonais Look Back était absolument fantastique.
Actuellement, Viddy utilise une icône Gopher, mais depuis que le langage d'implémentation est passé à Rust, cela pourrait créer une certaine confusion. Cependant, l’icône est fantastique, j’ai donc l’intention de la conserver telle quelle. ?
L'expression « Viddy well, Gopher, viddy well » aurait peut-être également pris un sens légèrement différent maintenant.
Grâce au défi de réécrire Viddy de Go to Rust, j'ai pu explorer en profondeur les différences et les caractéristiques de chaque langue. Des fonctionnalités telles que la propagation des erreurs de Rust et le type Option se sont avérées extrêmement utiles pour écrire du code plus sûr et plus concis. En revanche, la puissance expressive de Rust devenait parfois source de stress, surtout lorsque je me sentais obligé d'écrire le code le plus élégant possible. De plus, la plus petite bibliothèque standard de Rust a été reconnue comme un nouveau défi.
Malgré ces défis, donner la priorité à la sortie et se concentrer sur la sortie de quelque chose de fonctionnel a permis à la réécriture de progresser. Le soutien de la communauté pour tester et améliorer la version RC a également été un facteur de motivation important.
À l'avenir, je prévois de continuer à développer et à maintenir Viddy dans Rust pour améliorer encore mes compétences avec le langage. J'espère que cet article servira de référence utile à ceux qui envisagent de se lancer dans Rust. Enfin, si vous voyez des points à améliorer dans le code de Viddy, j'apprécierais grandement vos commentaires ou vos relations publiques !
https://github.com/sachaos/viddy
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!