blog

Rust as a Case Study: How to Choose a Programming Language for your Project

Written by Nome Completo do Autor | Apr 6, 2026 7:34:23 PM

Rust is a programming language that has grown in popularity over the last few years. According to the 2025 Stackoverflow survey, even though it is not the most used language for "extensive development work" — the longer, heavier projects (Rust sits at fourteenth place) — it is still the language most people want to work with.

In the past, it was known for having the features of a language that could become useful across many domains, but never quite getting there; a language to keep an eye on, but "not there yet" (as the "are we Rust yet?" repository shows across the many domains where that was true). During that time, it was at least usable for a simple experimental project. We won't go into the details; we'll just focus on describing that first experience with Rust.

This article uses our experience with Rust as a concrete example of how to evaluate and choose a technology for a project. The process we followed — defining clear requirements, comparing alternatives based on them, and validating the choice with real experiments — can be applied to any technology decision. Each team will work better with different tools, and each project has its own requirements that we cannot address or predict here. Do not base your decision solely on our initial experience.

 

The Project and the Selection Criteria

The year was 2017. The goal was to create a "simple" implementation of a new statistical model that was not widely known at the time but was gaining attention, as it had interesting properties when applied to problems where other statistical models (and machine learning models) had been used. If it had been any other, more established model, we could have relied on a highly optimized implementation in Sklearn or another machine learning library, but nothing of the sort existed yet. In the end, we chose Python for the prototype, but not for the final implementation.

In short, we needed a better, faster, and more memory-efficient implementation. The first step was to define clear criteria before evaluating any language. This brought us to the ever-present problem of choosing the right programming language for the project. It had to be:

  • Fast. For both development speed and execution speed. We prioritized development speed, since the differences in execution speed among the most efficient programming languages were small. But it still had to be fast enough, as training these models takes a long time. Support for concurrency and parallelism was also crucial.
  • Readable. The language should be easy to understand, especially for team members with less programming experience. Complex syntax can confuse the reader and waste time. Reading the code should be enough to understand it.
  • Reliable. Not just in the formal sense of reliability (running according to specifications). It should also prevent programmers from introducing problems caused by, for example, lack of attention. Reliable and readable code empowers the programmer to do what they need.

Defining these criteria explicitly — even if only in hindsight — is what makes a technology choice defensible. We did not list them formally before choosing the language, but we later realized they had become the most important reasons for choosing Rust. The other candidates were the most common choices for being fast and modern: C, C++, and Go (with Julia appearing later, after the project had already started, and ending up as an interesting alternative).

The second step was to eliminate candidates based on previously documented experience. C and C++ were ruled out early due to past experiences. Specifically, problems with mutable pointers, not knowing what caused those problems, and spending several days tracking down a small mistake made long ago by someone else. We wanted to avoid that kind of problem at all costs.

With Go, there was an unusual advantage: a partially developed library, at the time, had enough to start working with that statistical model. But having a library available is not a sufficient reason to choose a technology. While discussing with the team, it was difficult to explain parts of the code that seemed unnecessarily complex in Go. For that reason, our evaluation will focus on the readability of these languages.

 

A small example

 

Go did provide good and useful features for concurrency, which was necessary for our project, but so did Rust. For example, Go can easily make a function concurrent by using Goroutines:

 

func say(s string) {
	for i := 0; i < 5; i++ {
		time.Sleep(100 * time.Millisecond)
		fmt.Println(s)
	}
}
func main() {
	go say("world")
	say("hello")
}

 

This is great if you want to avoid explicitly creating threads, managing interrupts, and so on. A similar piece of code in Rust would look something like:

 

fn say(s: &str) {
    for _ in 0..5 {
        thread::sleep(Duration::from_millis(100));
        println!("{}", s);
    }
}
fn main() {
    thread::spawn(|| {
        say("world");
    });
    say("hello");
}

 

Pretty similar, right? Also simple. And good enough to start our comparisons and describe why the experience with Rust was more desirable.

Here is an important point in the evaluation: does the Go code create a thread? Not always. It creates a lightweight thread, and we trust Go's runtime scheduler to handle it. Does the Rust code create a thread? Yes. We explicitly create a thread, and from reading the code, we always know whether we did. It may be slightly slower or use a little more memory, but that level of control — explicit and readable — was an important aspect for the project. This illustrates how the same criterion — readability — can manifest differently depending on the language, and why comparing real code examples is more useful than comparing abstract descriptions.

The loop in the Go example uses an index, while the Rust one uses a Range and an iterator, which means we have Ranges in this language, and an iterator that traverses that Range — both easy to read and understand.

We also have Strings in both, but the Rust function receives a value of type &str. What is that? Is it readable? A type called String is understandable, but this is different. To be precise, &str is a "string slice", and we will use that to better describe this example and pattern of readability.

 

Reading the simple things

Both Go and Rust have a pass-by-value mechanism. In our case, this means that when you have a String as a function argument, the value of the String is copied, and the function receives its own, new String to do whatever it needs. But the Rust code does not use a String, does it? If it did, we would have something like:

 

fn say(s: String) {

 

Which is valid Rust code. But what is happening here? When we call this function, we are sending a String as an argument, and the value is being copied, just as in Go. Recall that Strings are made up of a pointer to some bytes and some additional metadata, which is not much to copy. The function receives a new, copied String and can do almost anything with it... except some things, like modifying it.

Immutability is a useful feature to have when you have experienced problems where a value was modified when it should not have been (especially when that value is a pointer). Variables and parameters in Rust are immutable by default. Strings are immutable in Go, which means the function cannot change the String either way. Do you think this is a String-specific feature in Go, or does it apply to other structures as well? What about this:

 

fn say(mut s: String) {

 

We have mut, a keyword for mutable variables and parameters in Rust. Do you think this String is immutable? Do you think this feature applies to other structures, or only to Strings? In Rust, it is clear that this is a language feature that can be applied to any parameter or variable. If not explicitly written, the variable or parameter is constant (immutable) — the opposite of Go, where you must declare constants with the keyword const... except for Strings. Can you see the difference?

Now, what about the string slice we saw earlier? A "slice" is a reference to a sequence of elements. In the example, it was a reference to a sequence of characters. It is also not mutable (there is no mut keyword), and that is enough for the function, since it does not need the other data of a full String and does not modify it either.

But what if we wanted our own, mutable String instead of a slice? An owned String can be created with:

 

let mut the_string = String::from("This is the literal string");

 

This is an explicit and direct initialization of a String, using a string literal as an argument. With Rust, these initializations are not implicit (at least in this first experience with Rust), which helps you say with confidence what the values you are working with actually are. This is a small difference from what Go does with string literals in the earlier examples, but a large difference from what C and C++ do with functions that accept Strings (or other parameters that can be implicitly converted).

Being explicit about structure initialization, variable mutability, and different but compatible types were the first things Rust did well, and the initial experience was positive — but it required a learning period. It was not simpler than Go, and it gave the impression of being a language that requires prior experience with other languages to be understood on its own terms; to show that there is a reason these features exist.

Rust earned some points as our choice, but the next feature is the most well-known aspect of Rust, and the one that sealed our decision.

 

Checking your borrows

Remember the pointer problems mentioned earlier with C/C++? Both Rust and Go have their own ways of solving them. In Go, the compiler performs static analysis and allocates variables on the stack or the heap as needed. If there is, for example, a function that returns a pointer to a local variable:

 

func dangling() *int{
  local_int := 20
  return &local_int
}

 

It will be valid, thanks to one of the Go compiler's mechanisms called "Escape Analysis". It is a pointer to an integer that will not be removed by the garbage collector until necessary. However, the equivalent is not valid in Rust:

 

fn dangling() -> &i32 {
  let local_int = 20;
  return &local_int;
}

 

And it is important to understand that this is not merely a difference in features, but a difference in code structure and organization. In Rust, this feature is called "Ownership", and it is the compiler's way of preventing not only dangling pointer problems, but also use-after-free errors. According to the Rust book, it is a set of rules that governs how a program manages its memory, without sacrificing execution speed to a garbage collector.

The book explains it with three simple rules:

  • Each value in Rust has an owner
  • There can only be one owner at a time
  • When the owner goes out of scope, the value is dropped

Simple enough to understand. For example, if you declare a value using let x =, you are the owner of that value. If it is passed to a function as an argument, the function is now the owner of the value. When the function ends, the value is dropped.

As long as it is not a copy of a value, we can understand when ownership changes. It would be good — though tedious — to always have it explicitly stated whether (and when) a value is copied, but Rust has a convenient way to discern this: if the type of a value implements the Trait "Copy", it will be implicitly copied. For some types in the standard library, such as the String type from the earlier examples, this is easy to verify, and if we need it in our own structures, we will have to implement the trait ourselves. And since some structures can be too large to copy without impacting runtime performance, that flexibility was welcome.

Additionally, shared references (which in Rust are only the immutable ones) can be copied. For example, we saw that using a large structure as a function argument will transfer ownership to the function, but we can keep ownership of that value by using a reference as the function parameter, as we did with the string slice in the earlier examples. And to avoid all the problems that can arise when using references, Rust has a few simple rules:

  • At any given time, you can have either one mutable reference, or any number of immutable references
  • References must always be valid

These rules are easy to understand, but also easy to break by accident. However, receiving an angry message from the compiler rather than spending a long time tracking down the problem sealed our choice. Not only do these rules and restrictions prevent reference and pointer errors, but they also force a different way of thinking about the structure of the problem to avoid similar issues in the future.

You cannot assume that every variable you are dealing with is mutable and can be passed between functions without issue, nor that you can hold a mutable reference in one context and still allow other parts of your program to read and modify it. And best of all, that reference will always be there, valid, and will not be dereferenced (or nullified) without you explicitly doing so and understanding the risks. Data races are prevented, so you are free to create threads and move values where they need to go, keeping only a few mutable references to avoid unintentional changes.

 

Conclusion

This was the first part of our experience with Rust, where we explored what the language had to offer, how easy it was to learn, and how to program the solution we needed at the time. It was harder than expected, and the language — even if simple to explain — has a steep learning curve for those unfamiliar with memory-safe programming and concurrency.

What this example illustrates about technology selection goes beyond Rust. The process we followed — even if not entirely conscious at the time — involved defining concrete criteria tied to the project, eliminating candidates based on real experience rather than reputation alone, comparing alternatives with actual code, and validating the choice based on the features that mattered most to the team. Having a ready-made library in Go could have been argument enough for a hasty decision, but the readability and reliability criteria revealed that that initial convenience would have come at a cost later on.

The Rust book's preface emphasizes that Rust is fundamentally about empowerment. Even when dealing with a complex codebase, references, threads, and performance bottlenecks, the language should make you feel empowered to build whatever you want. You can inspect the implementation, see explicit conversions and copies (and the implicit ones you chose to leave that way), and try new approaches to your problem without fear of running into pointer issues, reference problems, or concurrency conflicts.

For our experiment, the conclusion of this first step was positive, and, at least for now, it seems that other programmers have also enjoyed (and want more of) their experience with Rust. And regardless of which technology you choose for your next project, what matters most is that the choice is guided by clear criteria — not by familiarity, convenience, or hype.