Go versus Rust in 2024: Measuring the Best with 15 Benchmarks for Everyday Tasks

Go versus Rust in 2024: Measuring the Best with 15 Benchmarks for Everyday Tasks
Photo by Roman Synkevych / Unsplash

This post idea was born in last several public and personal discussions with highlighting a lof of technical, political, personal and religious aspects. Both programming languages got success in last decade, but released in different times: Go in 2009 and Rust in 2015.

Go logo, ©Wikipedia

Some people think that Go and Rust are not direct competitors, but this is not quite true: they cross very often: console tools, desktop applications, web services, and more. The only non-crossing area is embedded, but here Rust is not very strong due to static linking and strong competition from C/C++. This means that in many cases you will have to choose between Go and Rust as the main language for your next project.

Rust logo
Rust logo, ©Wikipedia

The tests were not selected by code complexity or level of extravagance, the main pattern is popular tasks. Even in radically different projects like machine learning, networking, or audio processing, you can't escape the main building block: basic math like addition, string concatenation, sorting, hashing, parsing, and more. So let's dive deep and figure out what the code looks like and which is faster. Time is the most important here - the faster the better. To get maximum performance, we need to enable the next optimization for both languages:

  • GOAMD64=v3 for Go
  • RUSTFLAGS=-C target-cpu=native for Rust

The biggest differences between Go and Rust, very briefly:

  • Memory management: Go uses garbage collection and automatic memory management, Rust uses ownership and borrowing, manual memory management with strong guarantees.
  • Concurrency: Go uses goroutines and channels for concurrency, lightweight threads and message passing, Rust uses threads and mutexes for concurrency, supports asynchronous programming with async/await.
  • Data structures: Go has built-in support for slices, maps, and channels. Rust offers a wide range of data structures, including vectors, hash maps, and sets.
  • Generics: Go has limited support for generics, while Rust provides strong support for generics, allowing for code reuse and type safety.
  • Mutability: Go is mutable by default, requiring explicit keywords for immutability. Rust is immutable by default and requires the mut keyword for mutable data.

1. Can't start without pure classic - print Hello World 1000 times

We use ranges in Go instead of the more readable for i := 1; i <= 1000; i++ because this for mode is not available in Rust and it is better to make the code mode similar.

  • Go:
package main
import "fmt"

func main() {

	for i := range [1000]int{} {
		_ = i // ignore the value of i
		fmt.Println("Hello world!")
	}
}
  • Rust:
fn main() {

    for _i in 1..=1000 {
        // _i to suppress unused variable error
        println!("Hello world!");
    }
}
  • Results:
    • Go: 0.037s
    • Rust: 0.022s
Hello world!
...

The Winner is Rust!

2. Basic Math: 1 million digit addition

In Go code the range works up to 999, so 1000 was added to the final number.
Rust code is quite similar with one exception - the sum variable must be mutable.

  • Go:
package main
import "fmt"

func main() {
	sum := 0
	for i := range [1000000]int{} {
		sum = sum + i
	}
	fmt.Println(sum + 1000000)
}
  • Rust:
fn main() {
   let mut sum = 0;
    for i in 1..=1000000 {
        sum = sum + i
    }
    println!("{}", sum);
}
  • Results:
    • Go: 0.044s
    • Rust: 0.031s
500000500000

The Winner is Rust!

3. String concatenation: merge the preamble of the US Constitution into one string

  • Go:
package main
import (
	"fmt"
	"strings"
)

func main() {
	src := []string{"We", "the", "People", "of", "the", "United", "States,", "in", "Order", "to", "form", "a", "more", "perfect", "Union,", "establish", "Justice", "insure", "domestic", "Tranquility,", "provide", "for", "the", "common", "defence,", "promote", "the", "general", "Welfare,", "and", "secure", "the", "Blessings", "of", "Liberty", "to", "ourselves", "and", "our", "Posterity,", "do", "ordain", "and", "establish", "this", "Constitution", "for", "the", "United", "States", "of", "America."}


	preamble := strings.Join(src, " ")
	fmt.Println(preamble)
}
  • Rust:
fn main() {
    let src = [
        "We", "the", "People", "of", "the", "United", "States,",
		"in", "Order", "to", "form", "a", "more", "perfect", "Union,",
		"establish", "Justice", "insure", "domestic", "Tranquility,",
		"provide", "for", "the", "common", "defence,", "promote", "the", "general", "Welfare,",
		"and", "secure", "the", "Blessings", "of", "Liberty", "to", "ourselves", "and", "our", "Posterity,",
		"do", "ordain", "and", "establish", "this", "Constitution", "for", "the", "United", "States", "of", "America."
    ];

    let preamble = src.join(" ");
    println!("{}", preamble);
}
  • Results:
    • Go: 0.026s
    • Rust: 0.012s
We the People of the United States, in Order to form a more perfect Union, establish Justice insure domestic Tranquility, provide for the common defence, promote the general Welfare, and secure the Blessings of Liberty to ourselves and our Posterity, do ordain and establish this Constitution for the United States of America.

The Winner is Rust!

4. Array creation: create the array from the list of US states

  • Go:
package main
import (
	"fmt"
	"strings"
)

func main() {
	list := "Alabama, Alaska, Arizona, Arkansas, California, Colorado, Connecticut, Delaware, Florida, Georgia, Hawaii, Idaho, Illinois, Indiana, Iowa, Kansas, Kentucky, Louisiana, Maine, Maryland, Massachusetts, Michigan, Minnesota, Mississippi, Missouri, Montana, Nebraska, Nevada, New Hampshire, New Jersey, New Mexico, New York, North Carolina, North Dakota, Ohio, Oklahoma, Oregon, Pennsylvania, Rhode Island, South Carolina, South Dakota, Tennessee, Texas, Utah, Vermont, Virginia, Washington, West Virginia, Wisconsin, Wyoming"

	arr := strings.Split(list, " ")
	fmt.Println(arr)
}
  • Rust:
fn main() {

let string = "Alabama, Alaska, Arizona, Arkansas, California, Colorado, Connecticut, Delaware, Florida, Georgia, Hawaii, Idaho, Illinois, Indiana, Iowa, Kansas, Kentucky, Louisiana, Maine, Maryland, Massachusetts, Michigan, Minnesota, Mississippi, Missouri, Montana, Nebraska, Nevada, New Hampshire, New Jersey, New Mexico, New York, North Carolina, North Dakota, Ohio, Oklahoma, Oregon, Pennsylvania, Rhode Island, South Carolina, South Dakota, Tennessee, Texas, Utah, Vermont, Virginia, Washington, West Virginia, Wisconsin, Wyoming";

let states : Vec<&str>  = string.split(',').collect();
println!("{:?}", states);
}
  • Results:
    • Go: 0.033s
    • Rust: 0.026s
["Alabama", " Alaska", " Arizona", " Arkansas", " California",...]

The Winner is Rust!

5. Array length measurement: how many words in the GPL license?

In the Go application we use the bufio package, which implements buffered I/O and is able to do the word detection, then counting is the easiest part. For Rust, word detection isn't available, so we have to detect and filter them manually.

  • Go:
package main
import (
		"os"
	  "fmt"
	  "bufio"
)

func main() {
	 f, err := os.Open("gpl.txt")
		if err != nil {
				fmt.Println("Error opening file:", err)
				return
		}
		defer f.Close()

		words := []string{}
		scanner := bufio.NewScanner(f)
		for scanner.Scan() {
				words = append(words, scanner.Text())
		}
		fmt.Println(len(words))
}
  • Rust:
use std::fs::File;
use std::io::{prelude::*, BufReader};

fn main() {
    let file = File::open("src/gpl.txt").expect("error opening the file");
    let reader = BufReader::new(file);
    let mut word_count: u32 = 0;
    for line in reader.lines() {
        let curr: String = line.expect("error reading content of the file");

        word_count += curr
            .split_whitespace()
            .filter(|word| word.len() > 0)
            .count() as u32
    }
    println!("{}", word_count);
}
  • Results:
    • Go: 0.026s
    • Rust: 0.014s
674

The Winner is Rust!

6. Hash table performance - let's hash the English alphabet

The Rust code looks a bit complicated, but we just copied the hash function from std:hash documentation. Golang code is probably simpler and it's hash documentation too.

  • Go:
package main
import (
	"fmt"
	"hash/fnv"
)

func hash(s string) uint32 {
	h := fnv.New32a()
	h.Write([]byte(s))
	return h.Sum32()
}

func main() {
	for letter := 'a'; letter <= 'z'; letter++ {
		char := string(letter)
		fmt.Println(char, hash(char))
	}
}
  • Rust:
use std::hash::{Hash, Hasher};
use std::collections::hash_map::DefaultHasher;

fn hash<T>(obj: T) -> u64
where
    T: Hash,
{
    let mut hasher = DefaultHasher::new();
    obj.hash(&mut hasher);
    hasher.finish()
}

fn main() {
    for c in 'a'..='z' {
        println!("{} {}", c, hash(c));
    }
}
  • Results:
    • Go: 0.021s
    • Rust: 0.035s
a 7144977204516799495
b 4680303220831814404
c 14854574485056723277
...

The winner is Go!

7. Sort 1000 number array

  • Go:
package main
import (
	"fmt"
	"sort"
)

func main() {
	arr := make([]int, 0, 1000)
	for i := 0; i < 1000; i++ {
		arr = append(arr, 1000-i)
	}
	sort.Ints(arr)
  fmt.Println(arr)

}
  • Rust:
fn main() {
    let mut arr = Vec::new();
    for num in 1..1001 {
        arr.push(1001 - num);
    }

    arr.sort();
    println!("array: {:?}", arr);
}
  • Results:
    • Go: 0.071s
    • Rust: 0.043s
[1,2,3... 1000000]

The Winner is Rust!

8. Filter: exclude even numbers from 1000 long array

  • Go:
package main
import "fmt"

func main() {
	arr := make([]int, 0, 1000)
	for i := 0; i < 1000; i++ {
		arr = append(arr, 1000-i)
	}

	non_even := make([]int, 0, 1000)

	for _, num := range arr {
		if num%2 != 0 {
			non_even = append(non_even, num)
		}
	}

	fmt.Println(non_even)
}
  • Rust:
fn main() {
    let mut arr: [i32; 1000] = [0; 1000];
    for i in 0..1000000 {
        arr[i] = (1000 - i) as i32;
    }

    let non_even: Vec<i32> = arr.iter()
        .copied()
        .filter(|&x| x % 2 != 0)
        .collect();

    println!("result: {:?}", non_even)
}
  • Results:
    • Go: 0.033s
    • Rust: 0.041s
[999, 997, 995...]

The Winner is Go!

9. JSON parsing - parse 5000 photos from jsonplaceholder.typicode.com

JSON API is very popular, so working with it is necessary for every programmer. We saved it as photos.json to remove network loading time from the final result.

For Rust, we are using the popular third-party `serde' crate, so need to add it manually.

  • Go:
package main
import (
	"os"
	"encoding/json"
	"fmt"
)

func main() {
	f, err := os.Open("photos.json")
	if err != nil {
		fmt.Println(err)
		return
	}
	defer f.Close()


	type Photos struct {
		ID   int    `json:"id"`
		Title string `json:"title"`
		Url  string `json:"url"`
	}

	var photos []Photos
	err = json.NewDecoder(f).Decode(&photos)
	if err != nil {
		fmt.Println(err)
		return
	}

	for _, photo := range photos {
		fmt.Println(photo)
	}
}
  • Rust:
use std::fs;

fn main() {
    let data = fs::read_to_string("src/photos.json")
    .expect("Unable to read file");
    let json: serde_json::Value = serde_json::from_str(&data)
    .expect("JSON was not well-formatted");

    for i in 0..5000 {
        println!(
        "{} {} {}",
        json[i].get("id").unwrap().as_u64().unwrap(), json[i].get("title").unwrap(),
        json[i].get("url").unwrap()
        );
    }
}
  • Results:
    • Go: 4.982s
    • Rust: 4.607s
...
5000 "error quasi sunt cupiditate voluptate ea odit beatae" "https://via.placeholder.com/600/6dd9cb"

The Winner is Rust!

10. HTML parsing

For Golang, there doesn't seem to be a package for our needs, so we'll use this gist. Also don't forget to install the html package: go get golang.org/x/net/html.

The Rust application needs to add the dependency html_parser = "0.7.0" to Cargo.toml.

  • Go:
package main

import (
	"encoding/json"
	"fmt"
	"strings"
	"golang.org/x/net/html"
)

func main() {
	// HTML string to parse
	htmlStr := `<!DOCTYPE html>
			<html lang="en">
			<head>
					<title>Nix Sanctuary</title>
					<meta charset="utf-8">
					</head>
			</html>`

	// Parse the HTML string
	doc, err := html.Parse(strings.NewReader(htmlStr))
	if err != nil {
		fmt.Println(err)
		return
	}

	// Traverse the HTML document and create a JSON object
	jsonObj := make(map[string]interface{})
	traverseHTMLNode(doc, jsonObj)

	// Print the JSON object
	jsonBytes, err := json.MarshalIndent(jsonObj, "", "  ")
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(string(jsonBytes))
}

func traverseHTMLNode(n *html.Node, jsonObj map[string]interface{}) {
	if n.Type == html.ElementNode {
		// Add the element name and attributes to the JSON object
		jsonObj[n.Data] = make(map[string]interface{})
		for _, attr := range n.Attr {
			jsonObj[n.Data].(map[string]interface{})[attr.Key] = attr.Val
		}
	} else if n.Type == html.TextNode {
		// Add the text content to the JSON object
		jsonObj["text"] = n.Data
	}

	// Recursively traverse the children of the node
	for c := n.FirstChild; c != nil; c = c.NextSibling {
		traverseHTMLNode(c, jsonObj)
	}
}
  • Rust:
use html_parser::{Dom, Result};
fn main() -> Result<()> {
    let html = r#"
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <title>Nix Sanctuary</title>
            <meta charset="utf-8">
            </head>
        </html>"#;
    let json = Dom::parse(html)?.to_json_pretty()?;
    println!("{}", json);
    Ok(())
}
  • Results:
{
  "name": "title",
  "variant": "normal",
  "children": [
    "Nix Sanctuary"
  ]
  ...
}
  • Results:
    • Go: 0.021s
    • Rust: 0.029s

The Winner is Go!

11. Search: Find a number in a 1000 long array

  • Go:
package main
import (
    "fmt"
    "slices"
)

func main() {
    arr := make([]int, 0, 1000)
    for i := 0; i < 1000; i++ {
        arr = append(arr, 1000-i)
    }
    fmt.Println(slices.Index(arr, 333))
}
  • Rust:
fn main() {
    let mut arr = Vec::new();
    for num in 1..1001 {
        arr.push(1001 - num);
    }
    let index = arr.iter().position(|&e| e == 333).unwrap();
    println!("{}", index);
}
  • Results:
    • Go: 0.026s
    • Rust: 0.068s
667

Winner is Go!

12. Regular Expression Test - How many "God" words in the Bible?

For this most interesting challenge, we need to download the Bible in text format here, this is King James Version: Pure Cambridge Edition. Let's save it as "Bible.txt".

Surprisingly, to make regular expressions work, you need to add the regex crate! That's very strange, because regexs are the must-have tool and should surely be available out of the box.

  • Go:
package main
import (
    "fmt"
    "log"
    "os"
    "regexp"
)

func countWordInFile(filename, word string) (int, error) {
    data, err := os.ReadFile(filename)
    if err != nil {
        return 0, err
    }
    content := string(data)
    re := regexp.MustCompile(`\b` + word + `\b`)
    matches := re.FindAllString(content, -1)
    return len(matches), nil
}

func main() {
    word := "God"
    filename := "Bible.txt"
    count, err := countWordInFile(filename, word)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(count)
}
  • Rust:
use regex::Regex;
use std::fs::File;
use std::io::Read;

fn main() {
    let word = r"\bGod\b";
    let mut file = File::open("src/Bible.txt").unwrap();
    let mut contents = String::new();
    file.read_to_string(&mut contents).unwrap();

    let re = Regex::new(word).unwrap();
    let count = re.captures_iter(&contents).count();

    println!("{}", count);
}
  • Results:
    • Go: 0.178s
    • Rust: 0.030s
4106

The Winner is Rust!

13. Random number generation: create 1000 of them & write to file

In both applications we will generate a random number between 0 and 100.
For Rust we use the rand module, so do not forget to run cargo add rand, in Go - math/rand.

  • Go:
package main
import (
    "fmt"
    "math/rand"
    "os"
)

func main() {
    arr := make([]int, 1000)
    for i := range arr {
        arr[i] = rand.Intn(100)
    }

    file, err := os.Create("random_numbers.txt")
    if err != nil {
        fmt.Println("Error creating file:", err)
        return
    }
    defer file.Close()

    for _, randomNumber := range arr {
        fmt.Fprintln(file, randomNumber)
    }
}
  • Rust:
use rand::{thread_rng, Rng};
use std::fs::File;
use std::io::{BufWriter, Write};

fn main() {
    let array = [(); 1000].map(|_| thread_rng().gen_range(0..100));
    let file = File::create("random_numbers.txt").expect("Error creating file");
    let mut writer = BufWriter::new(file);

    for number in &array {
        writeln!(writer, "{}", number).expect("Error writing to file");
    }
}
  • Results:
    • Go: 0.026s
    • Rust: 0.019s
random_numbers.txt:
87
61
...

The Winner is Rust!

14. File operations: print the contents of /usr directory

For Rust, we use the walkdir crate and filter out the errors.
The Go version uses the built-in path library with some error checking. There are actually no errors, so these checks won't slow down the application.

  • Go:
package main
import (
	"fmt"
	"log"
	"os"
	"path/filepath"
)

func main() {
	err := filepath.Walk("/usr", func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		fmt.Println(path)
		return nil
	})
	if err != nil {
		log.Println(err)
	}
}
  • Rust:
extern crate walkdir;
use walkdir::WalkDir;
fn main() {
  for entry in WalkDir::new("/usr")
             .into_iter()
             .filter_map(|e| e.ok()) {
    println!("{}", entry.path().display());
  }
}
  • Results:
    • Go: 2.733s
    • Rust: 2.673s
/usr
/usr/lib64
/usr/lib64/ld-linux-x86-64.so.2
/usr/bin
...

The Winner is Rust!

15. Native web server: measure time to process 1000 requests

In this cool chapter we have to implement a simple web server that will be able to respond with "Hello World!" to a "GET" request.

To do it with Rust we need to add actix-web = "4.8.0" dependency to Cargo.toml.

To test the code for 1000 requests, there's a simple Bash loop:

./app &
time for ((i=1; i<=1000; i++)); do
  curl 127.0.0.1:8000
done
  • Go:
package main
import (
    "fmt"
    "net/http"
    "log"
)

func helloWorld(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, world!")
}

func main() {
    http.HandleFunc("/", helloWorld)
    log.Println("Listening on port 8000...")
    http.ListenAndServe(":8000", nil)
}
  • Rust:
use actix_web::{web, App, HttpResponse, HttpServer, Responder};

async fn index() -> impl Responder {
    HttpResponse::Ok().body("Hello, world!")
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new().route("/", web::get().to(index))
    })
    .bind("127.0.0.1:8000")?
    .run()
    .await
}
  • Results:
    • Go: 11.404s
    • Rust: 7.642s
Hello, world!
...

The Winner is Rust!

Final score

Go wins in 4 tests, Rust in 11! So the winner is Rust! But do you really want to spend your time with Rust to achieve this performance? That is a personal decision only.

Read more