DEV Community

Cover image for Go To Do Rust
Andrei Merlescu
Andrei Merlescu

Posted on

Go To Do Rust

In this post, I want to go from step by step what it would look like building a taskr binary in both Go and Rust side by side so you can see conversion conventions and why Rust wins in the battle between the gopher and the crab.

Starting Point

package main

import "fmt"

func main() {
   fmt.Println("Hello World")
}
Enter fullscreen mode Exit fullscreen mode
fn main() {
   println!("Hello World")
}
Enter fullscreen mode Exit fullscreen mode

From both of these applications, you should be working in a directory structure that looks like:

~/work/personal/learning-go/taskr/main.go
~/work/personal/learning-rust/taskr/main.rs
Enter fullscreen mode Exit fullscreen mode

To get the ~/work/personal/learning-go directory, create it and then create a new Go file using:

mkdir -p ~/work/personal/learning-go/taskr
cd ~/work/personal/learning-go/taskr
go mod init taskr
Enter fullscreen mode Exit fullscreen mode

To get the ~/work/personal/learning-rust/taskr directory, run:

mkdir -p ~/work/personal/learning-rust
cargo new taskr
Enter fullscreen mode Exit fullscreen mode

From that command you'll get a Cargo.toml file which will be versioned to 0.1.0.

The program's data structure

We're building out a task, so lets define what that looks like in Go and in Rust.

type Status string

const (
    StatusTodo Status = "Todo"
    StatusDone Status = "Done"
)

type Task struct {
    ID     uint32 `json:"id"`
    Title  string `json:"title"`
    Status Status `json:"status"`
}
Enter fullscreen mode Exit fullscreen mode
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
enum Status {
    Todo,
    Done,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
struct Task {
    id: u32,
    title: String,
    status: Status,
}
Enter fullscreen mode Exit fullscreen mode
Decorator Usage
Debug lets me print the value using {:?} format
Serialize generate JSON code at compile time
Deserialize read JSON code at compile time
Clone allows you to duplicate a value
PartialEq allows equality comparisons between Status values

These decorators will be used throughout the program later. Specifically, the Debug will be used in println!() statements. serde import provides Serialize and Deserialize in order to engage with JSON files at compile time. Without PartialEq, the status would not be comparable. It's required.

Let's define what the print_usage() function is going to look like in Go and in Rust.

func printUsage() {
    fmt.Println("taskr - A simple task manager")
    fmt.Println()
    fmt.Println("Usage:")
    fmt.Println("  taskr add <title>    Add a new task")
    fmt.Println("  taskr list           List all tasks")
    fmt.Println("  taskr done <id>      Mark a task as done")
    fmt.Println("  taskr delete <id>    Delete a task")
    fmt.Println("  taskr search <query> Search for a task")
}
Enter fullscreen mode Exit fullscreen mode
fn print_usage() {
    println!("taskr - A simple task manager");
    println!();
    println!("Usage:");
    println!("  taskr add <title>    Add a new task");
    println!("  taskr list           List all tasks");
    println!("  taskr done <id>      Mark a task as done");
    println!("  taskr delete <id>    Delete a task");
    println!("  taskr search <query> Search for a task");
}
Enter fullscreen mode Exit fullscreen mode

This should go above the main() func in both main.go and main.rs respectively.

Command About
taskr add <title> Add a new task
taskr list List all tasks
taskr done <id> Mark a task as done
taskr delete <id> Delete a task
taskr search <id> Search for a task

Implementing the print_usage() func in both Go and Rust is very simple and is done in the main() func respectively:

func main() {
    if len(os.Args) < 2 {
        printUsage()
        return
    }
}
Enter fullscreen mode Exit fullscreen mode
fn main() {

    // .collect() returns the .args() into a Vec<String> format
    let args: Vec<String> = std::env::args().collect();

    // ensure that there are enough arguments to run the program
    if args.len() < 2 {
        print_usage();
    }
}
Enter fullscreen mode Exit fullscreen mode

The big difference here is the Go os.Args and the Rust std::env::args().collect(). Clearly, Rust is more verbose, and without .collect() the value of the .args() could be a tuple of <String, Err>, but .collect() returns a Vec<String> if there are no Err thrown.

With this basic program functionality, we can implement in the main() func in both languages what that switching is going to look like parsing the 2nd and 3rd arguments respectively.

func main(){ 
    // ... existing

    switch args[1] {
        // 1. taskr search <query>
        case "search":

        // 2. taskr add <title>
        case "add":

        // 3. taskr list
        case "list":

        // 4. taskr done <id>
        case "done":

        // 5. taskr delete <id>
        case "delete":

        // 6. taskr <else> catch-all
        default:

    }

    //- end of main
}
Enter fullscreen mode Exit fullscreen mode
fn main() {
    // ... existing

    match args[1].as_str() {

        // 1. taskr search <query>
        "search" => {}

        // 2. taskr add <title>
        "add" => {}

        // 3. taskr list
        "list" => {}

        // 4. taskr done <id>
        "done" => {}

        // 5. taskr delete <id>
        "delete" => {}

        // 6. taskr <else> catch-all
        _ => {
           eprintln!("Invalid command");
            print_usage();
            std::process::exit(1); // SIGTERM 1
        }

    } //- match arg[1]

    //- end of main
}
Enter fullscreen mode Exit fullscreen mode

The match (rust) and switch (go) statements are both reading from args[1]. The 0 in args[0] would be the filename of the binary running. In the example of taskr list, the args[0] is taskr and args[1] is list. In rust, the .as_str() is used to ensure that the underlying type is a string, since the statement is a String in "statement" => { } (rust) and case "statement": (go).

Next, we are going to be implementing the TaskStore, but before we do that, we need to ensure that our programs respectively have the correct imports.

import (
    "encoding/json"
    "errors"
    "fmt"
    "os"
    "strconv"
    "strings"
)
Enter fullscreen mode Exit fullscreen mode
// serde is used for JSON structure support at compile time
use serde::{Deserialize, Serialize};

// read and write on file functions
use std::fs;

// access the io::Result type for propagating I/O errors
use std::io;

// owned mutable file path
use std::path::PathBuf;
Enter fullscreen mode Exit fullscreen mode

These imports allow each of the programs to respectively handle the functionality needed. Within Go, you'll interact with the os package for I/O related work and in Rust, you interact with std::io instead. Files are handled in Go using the os package, as well as the fs and ioutil packages. Rust consolidates it into a two packages called std::io and std::fs. Both languages are designed to be cross platform natively. One last thing to point out about this is the std::path::PathBuf and how its very similar to Go's filepath package offering. In Rust, the PathBuf type is specifically for filesystem paths. On Windows, you split your directory tree using \ and on Linux, you split your directory tree using /. Those differences are handled under the hood by the filepath (Go) and std::path::PathBuf (Rust) automatically.

The Task Store

The bulk of the logic of the application is going to expose TaskStore in both Go and Rust. How these are implemented matter and you'll see why that is.

type TaskStore struct {
    tasks    []Task
    filePath string
}
Enter fullscreen mode Exit fullscreen mode
struct TaskStore{
    tasks: Vec<Task>,
    file_path: PathBuf,
}
Enter fullscreen mode Exit fullscreen mode

Both of these snippets show you how similar Go and Rust can appear. The []Task in Go becomes a Vec<Task> in Rust and the filePath uses Rust's PathBuf. In Go, the struct can use and store the filePath as a string since filepath package returns only String types back - Rust gives you an entirely new type called PathBuf whereas Go gives you a wrapper for it.

When implementing the methods on the TaskStore, this is where Go and Rust diverge quite radically.

func NewTaskStore(filePath string) (*TaskStore, error) {}

func (s *TaskStore) Save() error {}

func (s *TaskStore) Add(title string) {}

func (s *TaskStore) Complete(id uint32) error {}

func (s *TaskStore) Delete(id uint32) error {}

func (s *TaskStore) List() {}

func (s *TaskStore) Search(query string) []*Task {}
Enter fullscreen mode Exit fullscreen mode
impl TaskStore {

    fn new(file_path: PathBuf) -> io::Result<Self> {}

    fn save(&self) -> io::Result<()> {}

    fn add(&mut self, title: String) {}

    fn complete(&mut self, id: u32) -> Result<(), String> {}

    fn delete(&mut self, id: u32) -> Result<(), String> {}

    fn list(&self) {}

    fn search(&self, query: &str) -> Vec<&Task> {}
}
Enter fullscreen mode Exit fullscreen mode

There is a pretty clean 1:1 relationship between the implemented methods of the TaskStore between Go and Rust and specifically what I want to note here for you is how in Rust everything is wrapped inside of the impl TaskStore {} block itself, whereas in Go, this can be literally anywhere at the root level of the file inside the package main project. The order of the routes does not matter in either language.

Rust has a convention called io::Result<Self> which in Go simply looks like (<Type>, error). The NewTaskStore method in Go is unattached per-se to the TaskStore struct itself, but it does return the Go equivalent of (*TaskStore, error) as io::Result<Self>. When io::Result<Self> is used, the method must also include Ok(()) in order to satisfy the contract.

At this point, let's ensure that our Go program has two little helper functions in it in order to improve runtime readability - this is common in Go.

func parseID(arg string) uint32 {
    id, err := strconv.ParseUint(arg, 10, 32)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Invalid task ID: %s\n", arg)
        os.Exit(1)
    }
    return uint32(id)
}

func mustSave(s *TaskStore) {
    if err := s.Save(); err != nil {
        fmt.Fprintf(os.Stderr, "Error saving tasks: %v\n", err)
        os.Exit(1)
    }
}
Enter fullscreen mode Exit fullscreen mode

This application does not need or use helper functions in the Rust implementation. In Rust we're using u32 for the id and in Go that is a uint32 which imports the strconv package. Rust does not need to import any packages.

Before implementing the specifics in the TaskStore, lets consume it in the main() func.

func main() {

    // ... after checking the length of args

    store, err := NewTaskStore("tasks.json")
    if err != nil {
        fmt.Fprintf(os.Stderr, "Error loading tasks: %v\n", err)
        os.Exit(1)
    }

    // ... before switching args[1]

}
Enter fullscreen mode Exit fullscreen mode
fn main() -> io::Result<()> {

    // ... after checking the length of args

    let database = PathBuf::from("tasks.json");

    let mut store = match TaskStore::new(database){
        Ok(store) => store,
        Err(e) => {
            eprintln!("Error loading tasks: {}", e);
            std::process::exit(1); // SIGTERM 1
        }
    };

    // ... before matching args[1]

}
Enter fullscreen mode Exit fullscreen mode

Both of these implementations will leverage the current working directory and consume the resource tasks.json if it exists. If it does not exist, the program will create it, if it exists, the program will overwrite it.

In Rust, the TaskStore is consumed slightly differently than in Go because of the impl TaskStore {} differences. In Rust the TaskStore must have mut after let and before the name of the variable in order to indicate to the compiler that the value of store is eligible for changing in the future. Since the TaskStore::new(database) returns with an io::Result<Self>, we can match against it when assigning it to the store variable.

The Go tuple that io::Result<Self> is satisfying is (*TaskStore, error) and in Rust, the first component *TaskStore is represented in by the Ok() invocation. At the end of the various impl TaskStore {} methods, you'll see an Ok(()) at the end of each method ; this invokes the contract and exits the method since we're good. Otherwise, your program will not compile. When assigning a value to store and executing TaskStore::new(database), each component of the output gets matched to the Ok(store) => or Err(e) => {} blocks respectively. When we receive an Ok(()) then that returned store from Ok() is then assigned to the let mut store variable.

Otherwise, if Err(e) satisfies, it means that something went wrong and we'll print the error and exit with status code 1. In Go, we use os.Exit(1) and in Rust we use std::process::exit(1);. In Go, we can use the fmt or slog or verbose packages to write to STDOUT and STDERR, and in Rust we use eprintln!() for STDERR and println!() for STDOUT respectively.

Implementing new TaskStore

We have stubbed out each of our methods, and at this point we need to implement the body of these methods in two locations. We're going to implement the TaskStore now, and that means populating each of the methods in TWO locations. First in the impl TaskStore {} block for Rust and inside of the func (s *TaskStore) ... methods for Go.

Method Go Signature Rust Signature
new func NewTaskStore(filePath string) (*TaskStore, error) fn new(file_path: PathBuf) -> io::Result<Self>
save func (s *TaskStore) Save() error fn save(&self) -> io::Result<()>
list func (s *TaskStore) List() fn list(&self)
complete func (s *TaskStore) Complete(id uint32) error fn complete(&mut self, id: u32) -> Result<(), String>
delete func (s *TaskStore) Delete(id uint32) error fn delete(&mut self, id: u32) -> Result<(), String>
add func (s *TaskStore) Add(title string) fn add(&mut self, title: String)
search func (s *TaskStore) Search(query string) []*Task fn search(&self, query: &str) -> Vec<&Task>

TaskStore's new Implemented

func NewTaskStore(filePath string) (*TaskStore, error) {
    tasks := []Task{}

    data, err := os.ReadFile(filePath)
    if err == nil {
        if jsonErr := json.Unmarshal(data, &tasks); jsonErr != nil {
            tasks = []Task{}
        }
    } else if !errors.Is(err, os.ErrNotExist) {
        return nil, err
    }

    return &TaskStore{tasks: tasks, filePath: filePath}, nil
}
Enter fullscreen mode Exit fullscreen mode
fn new(file_path: PathBuf) -> io::Result<Self> {

    let tasks = if file_path.exists() {
        let data = fs::read_to_string(&file_path)?;
        serde_json::from_str(&data).unwrap_or_else(|_| Vec::new() )
    } else { 
        Vec::new()
    };

    Ok(Self { tasks, file_path })
}
Enter fullscreen mode Exit fullscreen mode

The most notable thing to understand in this code block is the difference between how Go and Rust respectively handle JSON. To be clear up front: both languages parse the actual data at runtime โ€” no compiler can validate a file that doesn't exist until the program runs. The real difference is where the mapping code comes from and how failure is surfaced. In Go, json.Unmarshal uses reflection at runtime to inspect your struct's fields and figure out how to map the data onto them, and it reports problems as an error value you check with jsonErr != nil โ€” a check the compiler will let you skip entirely. It's also forgiving by default: unknown fields are ignored and missing fields silently become zero values, so irregular JSON can flow through your program without any error at all. In Rust, the #[derive(Deserialize)] macro generates the mapping code at compile time โ€” no reflection โ€” and the build fails outright if any field's type can't be deserialized. At runtime, serde_json::from_str returns a Result, and the type system won't let you touch the parsed value until you've decided what happens on failure โ€” with ?, a match, or a fallback like .unwrap_or_else(|_| Vec::new()). Mistyped or missing fields are loud errors by default instead of silent zero values, and the generated code is typically faster than reflection-based decoding as a bonus. Ultimately, Rust doesn't validate your JSON before compilation โ€” it forces you to handle irregular JSON before your code will compile, which is what actually takes the sting out of the most painful part of Go development: interacting with irregular JSON files.

Literally, modeling an irregular JSON file in Rust can take you noticeably longer to get right than in Go โ€” every optional field, inconsistent type, and quirk has to be spelled out with Option<T>, #[serde(default)], or a custom deserializer before the compiler lets you move on. The investment of time in this, on the surface, feels like it's a waste and unnecessary โ€” but years down the road, the payoff arrives at runtime, not at recompile time: when that feed inevitably changes shape, the Rust program fails loudly at the parse boundary, while the Go program may keep running on silent zero values. What neither language will do is pull the rug out from under old code. Go's compatibility promise explicitly guarantees that programs written for Go 1 keep compiling and behaving the same โ€” encoding/json included โ€” and even the new encoding/json/v2 ships as a separate, opt-in package rather than a change to the old one. Rust makes the same commitment through its edition system: new editions are opt-in per crate, and old ones keep compiling on new compilers (rare soundness fixes aside). So who actually gives you write once, compile 25 years later with zero effort? C mostly does, though modern compilers now reject old habits like implicit int. C++ mostly does, though the standard really does remove things โ€” auto_ptr, register, pre-standard headers โ€” so "zero effort" is optimistic. Rust hasn't existed long enough to prove it (1.0 shipped in 2015), but its stability policy is built for exactly that. And Go, far from merely trying, has one of the strictest backward-compatibility records in the industry. As for Python, PHP and Ruby โ€” their major-version breaks (Python 2โ†’3 most famously) are stewardship choices, not something true by definition and design of high-level languages; and "compile 25 years later" isn't quite the right test for interpreted languages anyway.

We consumed the new method in the main() func when we established our let mut store (rust) and store, err := NewTaskStore("tasks.json") (go).

TaskStore's list Implemented

func (s *TaskStore) List() {
    if len(s.tasks) == 0 {
        fmt.Println("No tasks yet. Add one with: taskr add \"your task\"")
        return
    }

    for _, task := range s.tasks {
        marker := "[ ]"
        if task.Status == StatusDone {
            marker = "[X]"
        }
        fmt.Printf("%s %d - %s\n", marker, task.ID, task.Title)
    }
}
Enter fullscreen mode Exit fullscreen mode
fn list(&self) {
    if self.tasks.is_empty() {
        println!("No tasks yet. Add one with: taskr add \"your task\"");
        return;
    }

    for task in &self.tasks {
        let market = match task.status {
            Status::Todo => "[ ]",
            Status::Done => "[X]",
        };
        println!("{} {} - {}", market, task.id, task.title);
    }
}
Enter fullscreen mode Exit fullscreen mode

Respectively, these two methods look pretty much the same. Specifically in Rust we're leveraging the borrower for &self.tasks and in Go we just range over them without needing to indicate memory usage. Without the Debug derived decorator attacked to the Task struct, the println!() methods that use {} wouldn't compile.

TaskStore's add Implemented

func (s *TaskStore) Add(title string) {
    var maxID uint32
    for _, t := range s.tasks {
        if t.ID > maxID {
            maxID = t.ID
        }
    }

    task := Task{
        ID:     maxID + 1,
        Title:  title,
        Status: StatusTodo,
    }

    fmt.Printf("Added task %d: %s\n", task.ID, task.Title)
    s.tasks = append(s.tasks, task)
}
Enter fullscreen mode Exit fullscreen mode
fn add(&mut self, title: String) {
    let id = self.tasks.iter() // use the iterator
        .map(|t| t.id)         // use only the id field
        .max()                 // select the maximum value in the set
        .unwrap_or(0) + 1;     // if no err, use # else use 0 + 1

    let task = Task {
        id,                    // short for `id: id,`
        title,                 // short for `title: title,`
        status: Status::Todo,
    };

    println!("Added task {}: {}", task.id, task.title);

    self.tasks.push(task);
}
Enter fullscreen mode Exit fullscreen mode

Most notably between this Go and Rust code you can see the manner for acquiring the next ID in the set is radically different. In Rust we are using the .iter() in order to .map() then .max() with a final check of .unwrap_or(0) in order to get an iterable list of IDs via the .map() extraction of just the id field, followed by the .max() choice. This quickly returns the largest integer of the result and then attempts to + 1 at the end of it. In Rust this expression is a simple statement and in Go its multiple lines using maxID inside the method.

In Rust without the &mut on the self argument of the method, the self.tasks.push(task); would fail because the compiler is missing the &mut on self in order to actually write via .push(). In Go, we simply use append(...).

Both languages now need to take this impl TaskStore { "add" => {} } and func (s *TaskStore) Add(title string) and consume them in the main() func.

func main() {
    // ... existing after \`switch args[1]\`

    case "add":
        if len(args) < 3 {
            fmt.Fprintln(os.Stderr, "Usage: taskr add <title>")
            os.Exit(1)
        }
        store.Add(strings.Join(args[2:], " "))
        mustSave(store)
    }

    // ... existing before \`default:\` case 
}
Enter fullscreen mode Exit fullscreen mode
fn main() {

    // ... existing after `match args[1].to_str() {`

    // 2. taskr add <title>
    "add" => {
        if args.len() < 3 {
            eprintln!("Usage: taskr add <title>");
            std::process::exit(1); // SIGTERM 1
        }
        let title = args[2..].join(" ");
        store.add(title);
        store.save()?;
        Ok(())
    }

    // ... existing before `_ => {}` match
}
Enter fullscreen mode Exit fullscreen mode

Our Go code is consuming the mustSave helper method and our Rust code is calling .add() and .save() endpoints on the TaskStore impl.

TaskStore's delete Implemented

func (s *TaskStore) Delete(id uint32) error {
    for i, t := range s.tasks {
        if t.ID == id {
            fmt.Printf("Removed task %d: %s\n", t.ID, t.Title)
            s.tasks = append(s.tasks[:i], s.tasks[i+1:]...)
            return nil
        }
    }
    return fmt.Errorf("task %d not found", id)
}
Enter fullscreen mode Exit fullscreen mode
fn delete(&mut self, id: u32) -> Result<(), String> {
    let pos = self.tasks.iter()       // get an iterator
        .position(|t| t.id == id)     // find the index of the id
        .ok_or_else(|| 
            format!("Task {} not found", id)
        )?;

    let removed = self.tasks.remove(pos);
    println!("Removed task {}: {}", removed.id, removed.title); 
    Ok(()) 
}
Enter fullscreen mode Exit fullscreen mode

As you can see - these methods look so similar and that's because Go and Rust compliment each other, like C and C++ complimented each other. They are designed to be similar, but one for more lower level work and the other for more higher level data driven work. Either way, when you implement these methods, you see their implementations even look similar.

case "delete":
    if len(args) < 3 {
        fmt.Fprintln(os.Stderr, "Usage: taskr delete <id>")
        os.Exit(1)
    }
    if err := store.Delete(parseID(args[2])); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    mustSave(store)
Enter fullscreen mode Exit fullscreen mode
"delete" => {
    if args.len() < 3 {
        eprintln!("Usage: taskr delete");
        std::process::exit(1);
    }
    let id: u32 = args[2].parse().unwrap_or_else(|_| {
        eprintln!("Invalid task ID: {}", args[2]);
        std::process::exit(1);
    });
    if let Err(e) = store.delete(id) {
        eprintln!("{}", e);
        std::process::exit(1); // SIGTERM 1
    }
    store.save()?;
    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

Most notably in the differences between these two implementations, you can see the Go helper method parseID being used here where Rust just leverages .parse().unwrap_or_else(|_| {} ) instead.

TaskStore's complete | done Implemented

func (s *TaskStore) Complete(id uint32) error {
    for i := range s.tasks {
        if s.tasks[i].ID == id {
            s.tasks[i].Status = StatusDone
            fmt.Printf("Completed task %d: %s\n", s.tasks[i].ID, s.tasks[i].Title)
            return nil
        }
    }
    return fmt.Errorf("task %d not found", id)
}
Enter fullscreen mode Exit fullscreen mode
fn delete(&mut self, id: u32) -> Result<(), String> {
    let pos = self.tasks.iter()    // get an iterator
        .position(|t| t.id == id)  // find the index of the id
        .ok_or_else(|| 
            format!("Task {} not found", id)
        )?;

    let removed = self.tasks.remove(pos);
    println!("Removed task {}: {}", removed.id, removed.title); 
    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

The conventions between Rust and Go become very visibly noticeable in this manner of presentation.

case "done":
    if len(args) < 3 {
        fmt.Fprintln(os.Stderr, "Usage: taskr done <id>")
        os.Exit(1)
    }
    if err := store.Complete(parseID(args[2])); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    mustSave(store)
Enter fullscreen mode Exit fullscreen mode
"done" => {
    if args.len() < 3 {
        eprintln!("Usage: taskr done");
        std::process::exit(1); // SIGTERM 1
    }
    let id: u32 = args[2].parse().unwrap_or_else(|_| {
        eprintln!("Invalid task ID: {}", args[2]);
        std::process::exit(1); // SIGTERM 1
    });
    if let Err(e) = store.complete(id) {
        eprintln!("{}", e);
        std::process::exit(1); // SIGTERM 1
    }
    store.save()?;
    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

TaskStore's search Implemented

func (s *TaskStore) Search(query string) []*Task {
    queryLower := strings.ToLower(query)
    var results []*Task
    for i := range s.tasks {
        if strings.Contains(strings.ToLower(s.tasks[i].Title), queryLower) {
            results = append(results, &s.tasks[i])
        }
    }
    return results
}
Enter fullscreen mode Exit fullscreen mode
fn search(&self, query: &str) -> Vec<&Task> {
    let query_lower = query.to_lowercase();

    self.tasks.iter()                    // get an iterator
        .filter(|task|                   // each task loop
            task.title.to_lowercase()    // get lowercase title
                .contains(&query_lower)  // match with query_lower
        )
        .collect()                       // gather into vector
}
Enter fullscreen mode Exit fullscreen mode

Specifically, the Rust is returning Vec<&Task> which is a referenced object explicitly from the .collect() output.

case "search":
    if len(args) < 3 {
        fmt.Fprintln(os.Stderr, "Usage: taskr search <query>")
        os.Exit(1)
    }
    query := strings.Join(args[2:], " ")
    results := store.Search(query)
    if len(results) == 0 {
        fmt.Printf("No tasks found matching %q\n", query)
    } else {
        fmt.Printf("Found %d tasks matching: %s\n", len(results), query)
        for _, task := range results {
            marker := "[ ]"
            if task.Status == StatusDone {
                marker = "[X]"
            }
            fmt.Printf("%s %d: %s\n", marker, task.ID, task.Title)
        }
    }
Enter fullscreen mode Exit fullscreen mode
"search" => {
    if args.len() < 3 {
        eprintln!("Usage: taskr search <query>");
        std::process::exit(1); // SIGTERM 1
    }

    let query = args[2..].join(" "); 
    let results = store.search(&query);

    if results.is_empty() {
        println!("No tasks found matching \"{}\"", query);
    } else {
        println!("Found {} tasks matching: {}", results.len(), query);
        for task in &results { 
            let marker = match task.status { 
                Status::Todo => "[ ]", // incomplete items
                Status::Done => "[X]", // completed items
            };
            println!("{} {}: {}", marker, task.id, task.title);
        }
    }

    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

You can view the Go and Rust full source code respectively at either link provided.

This tutorial was designed to help bring Go engineers over to Rust by completing a command line task manager application in both languages to see how they are both the similar and different.

Are you coming into Rust from Go like I am? If so, what are some of your favorite resources to use and learn from?

Top comments (0)