Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Events
Videos
Audiobooks
Packt Hub
Free Learning
Arrow right icon
timer SALE ENDS IN
0 Days
:
00 Hours
:
00 Minutes
:
00 Seconds
Rust Web Programming
Rust Web Programming

Rust Web Programming: A hands-on guide to Rust for modern web development, with microservices and nanoservices , Third Edition

Arrow left icon
Profile Icon Maxwell Flitton
Arrow right icon
€26.99 €29.99
Full star icon Full star icon Full star icon Full star icon Half star icon 4.3 (3 Ratings)
eBook Jan 2026 674 pages 3rd Edition
eBook
€26.99 €29.99
Paperback
€37.99
eBook + Subscription
€24.99 Monthly
Arrow left icon
Profile Icon Maxwell Flitton
Arrow right icon
€26.99 €29.99
Full star icon Full star icon Full star icon Full star icon Half star icon 4.3 (3 Ratings)
eBook Jan 2026 674 pages 3rd Edition
eBook
€26.99 €29.99
Paperback
€37.99
eBook + Subscription
€24.99 Monthly
eBook
€26.99 €29.99
Paperback
€37.99
eBook + Subscription
€24.99 Monthly

What do you get with eBook?

Product feature icon Instant access to your Digital eBook purchase
Product feature icon Download this book in EPUB and PDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
Product feature icon AI Assistant (beta) to help accelerate your learning
Modal Close icon
Payment Processing...
tick Completed

Billing Address

Table of content icon View table of contents Preview book icon Preview Book

Rust Web Programming

A Quick Introduction to Rust

Rust is growing in popularity, but it has a reputation for having a steep learning curve. However, if taught correctly, this learning curve can be reduced. By covering the basic rules around Rust, as well as learning how to manipulate a range of data types and variables, we will be able to write simple programs in the same fashion as dynamically typed languages, using a similar number of lines of code.

Understanding the basics is the key to effective web programming in Rust. I have maintained entire Kubernetes clusters where all the servers are written in Rust. Because I utilized traits, error handling, and generics effectively, it made reusing code in Rust extremely effective. In my experience, it takes fewer lines of code to build out Rust servers than it does Python servers if the basics of Rust are well utilized.

The goal of this chapter is to cover the main differences between Rust and generic dynamic languages and to provide you with a quick understanding of how to utilize Rust.

In this chapter, we will cover the following topics:

  • What is Rust?
  • Reviewing data types and variables in Rust
  • Controlling variable ownership
  • Building structs

Once we have covered the main concepts in this chapter, you will be able to code basic programs in Rust that will run. You will also be able to debug your programs and understand the error messages that are thrown by the Rust compiler. As a result, you will have the foundations to be productive in Rust. You will also be able to move on to structuring Rust code over multiple files.

In this chapter and the following chapter, we will cover enough Rust to be productive in web programming. However, we will not be able to do the entire Rust programming language justice in two chapters. If you want to dive deeper into the Rust programming language after reading this chapter, then you can explore the further reading list at the end of the chapter.

Your purchase includes a free PDF copy + code bundle

Your purchase includes a DRM-free PDF copy of this book, the code bundle, and additional exclusive extras. See the Free benefits with your book section in the Preface to unlock them instantly and maximize your learning.

Technical requirements

For this chapter, we only need access to the internet, as we will use the online Rust playground to implement the code. The code examples provided can be run in the online Rust playground at https://play.rust-lang.org/.

You can download the example project and code for this book by following the instructions in the Download the example code files section in the Preface of this book.

This chapter’s code files are included in the downloadable code bundle.

What is Rust?

Rust is a cutting-edge systems programming language that has been making waves since Mozilla Research introduced it in 2010. With a focus on safety, concurrency, and performance, Rust is a formidable alternative to traditional languages like C and C++. Its most notable feature, the ownership system, enforces rigorous memory safety rules at compile time. This approach effectively eradicates common pitfalls like null pointer dereferencing and buffer overflows, all without needing a garbage collector.

Designed for high performance, Rust provides granular control over hardware and memory, making it perfect for developing operating systems, game engines, and other performance-critical applications. Its syntax is both modern and expressive, offering features typically seen in higher-level languages, such as pattern matching and algebraic data types, while retaining the efficiency required for system-level programming. Consequently, Rust has rapidly attracted a strong and active community, bolstered by excellent documentation and a burgeoning ecosystem of libraries and tools.

Rust’s adoption has been nothing short of remarkable, particularly in tech sectors where safety and performance are crucial. Major players like Microsoft, Amazon, and Dropbox have integrated Rust into their technology stacks to capitalize on its reliability and efficiency. Rust’s consistent ranking since 2016 as the most loved programming language in Stack Overflow’s annual developer surveys is a testament to its appeal. Developers laud Rust for its unique blend of performance and safety, alongside its supportive and vibrant community.

Moreover, Rust’s ecosystem, highlighted by its package manager, Cargo, offers a seamless development experience. Cargo simplifies dependency management, project building, and testing, streamlining workflows that can be cumbersome in other systems programming languages. This robust combination of features positions Rust as a unique and powerful tool for developers aiming to create reliable, high-performance software.

Why is Rust revolutionary?

With programming, there is usually a trade-off between speed and resources and development speed and safety. Low-level languages such as C/C++ can give a developer fine-grained control over a computer, with fast code execution and minimal resource consumption. However, this is not free. Languages such as C/C++ need manual memory management, which can introduce bugs and security vulnerabilities. A simple example of this is a buffer overflow attack.

A buffer overflow attack occurs when the programmer does not allocate enough memory. For instance, if the buffer only has a size of 15 bytes, and 20 bytes are sent, then the excess 5 bytes might be written past the boundary. An attacker can exploit this by passing in more bytes than the buffer can handle. This can potentially overwrite areas that hold executable code with their own code.

There are other ways to exploit a program that does not have correctly managed memory. On top of increased vulnerabilities, it takes more code and time to solve a problem in a low-level language. As a result of this, C++ web frameworks do not take up a large share of web development. Instead, it usually makes sense to go for high-level languages such as Python, Ruby, and JavaScript. Using such languages will generally result in a developer solving problems safely and quickly.

However, it must be noted that this memory safety comes at a cost. These high-level languages generally keep track of all the variables defined and their references to a memory address. When there are no more variables pointing to a memory address, the data in that memory address gets deleted. This process is called garbage collection and consumes extra resources and time, as a program must be stopped to clean up the variables.

With Rust, memory safety is ensured without the costly garbage collection process. Rust ensures memory safety through a set of ownership rules, checked at compile time with a borrow checker. Because of this, Rust enables rapid, safe problem-solving with truly performant code, thus breaking the speed/safety trade-off.

Memory safety is the property of programs having memory pointers that always point to valid memory.

With more data processing, traffic, and complex tasks lifted into the web stack, Rust, with its growing number of web frameworks and libraries, has now become a viable choice for web development. This has led to some truly amazing results in the web space for Rust. In 2020, Shimul Chowdhury ran a series of tests against servers with the same specs but different languages and frameworks. The results can be seen in the following figure (note that the Rust frameworks comprise Actix Web and Rocket):

Table

Description automatically generated

Figure 1.1 – Results of different frameworks and languages by Shimul Chowdhury (found at https://www.shimul.dev/en/blog/2020/benchmarking-flask-falcon-actix-web-rocket-nestjs/)

In the preceding figure, we can see that there are some variations in the languages and frameworks. These Rust servers are in a completely different league when it comes to total requests handled and data transferred. Other languages, such as Golang, have come onto the scene, but the lack of garbage collection in Rust has managed to outshine Golang. This was demonstrated in Jesse Howarth’s 2020 blog post Why Discord is switching from Go to Rust (https://discord.com/blog/why-discord-is-switching-from-go-to-rust). In this post, it was clear that Golang servers were producing latency spikes, whereas the Rust servers were not.

The garbage collection that Golang was implementing to keep the memory safe resulted in two-minute spikes. But it does not stop there. In 2022, AWS produced a report called “Sustainability with Rust,” wherein they created a ratio of energy consumption, resulting in the following table:

Language

Energy Rating

C

1.00

Rust

1.03

JavaScript

4.45

PHP

29.30

Ruby

69.91

Python

75.88

Table 1.1 – Energy Rating of Languages from an AWS report (found at https://aws.amazon.com/blogs/opensource/sustainability-with-rust/)

Rust was second only to C in energy consumption and startup time. AWS is not the only fan of Rust. In 2024, the Whitehouse recommended Rust over C and C++ for future projects in the “Back to the Building Blocks: A Pat Toward Secure and Measurable Software” report. Finally, the compatibility of Rust with other systems has created an inflection point where we can integrate it. For instance, PostgreSQL and Redis now support modules that can be written in Rust and uploaded. Cloudflare has written a battle-tested load balancer in Rust that serves more than 40 million internet requests per second.

I personally think that Rust is the future of backend web programming. Like C pushed forward hardware, where all developers agreed on standards and rowed in the same direction, you will hopefully appreciate after finishing this book that Rust could push forward a backend standard, where the developer has a memory-safe language that can interact directly with other components, such as load balancers and databases, in a way that garbage-collected languages just cannot.

Why Rust? cheatsheet

  • Balance: Rust balances speed, resource efficiency, development speed, and safety without compromising any aspect.
  • Low-level control: Like C/C++, Rust provides fine-grained control over the computer, leading to fast code execution and minimal resource consumption.
  • Rust’s memory safety: Languages like Python, Ruby, and JavaScript offer memory safety via garbage collection, but at the cost of additional resources and potential performance overhead. Rust ensures memory safety through compile-time ownership rules and a borrow checker, eliminating the need for garbage collection.
  • Web development: Rust’s growing number of web frameworks and libraries make it a strong candidate for web development, offering both safety and performance.
  • Integration: Rust’s compatibility with systems like PostgreSQL and Redis, as well as its use in high-performance components like Cloudflare’s load balancer, demonstrate its versatile integration capabilities.

Now that we understand why we want to code in Rust, we can move on to reviewing data types in the next section.

Reviewing data types and variables in Rust

If you have coded in another language before, you will have used variables and handled different data types. However, Rust does have some quirks that can put off developers. This is especially true if the developer has come from a dynamic language, as these quirks mainly revolve around memory management and reference to variables. These can be intimidating initially, but when you get to understand them, you will learn to appreciate them.

Some people might hear about these quirks and wonder why they should bother with the language at all. This is understandable, but these quirks are why Rust is such a paradigm-shifting language. Working with borrow checking and wrestling with concepts such as lifetimes and references gives us the high-level memory safety of a dynamic language, such as Python. However, we can also get memory-safe, low-level resources, such as those delivered by C and C++.

This means that we do not have to worry about dangling pointers, buffer overflows, null pointers, segmentation faults, data races, and other issues when coding in Rust. Issues such as null pointers and data races can be hard to debug. The borrow-checking rules enforced are a good trade-off, as we must learn Rust’s quirks to get the speed and control of non-memory-safe languages, but we do not get the headaches these non-memory-safe languages introduce.

Before we do any web development, we need to run our first program. We can do this in the Rust playground at https://play.rust-lang.org/.

If you have never visited the Rust playground before, you will see the following layout once you are there:

fn main() {
    println!("hello world");
}

The preceding code will look like the following screenshot in the online Rust playground, after we have pressed the RUN button on the top left-hand side of the screen:

A screenshot of a computer

Description automatically generated

Figure 1.2 – View of the online Rust playground

In our hello world code, what we have is a main function, which is our entry point. This function fires when we run our program. All programs have entry points. If you have not heard of the concept before, due to coming from a dynamic language, the entry point is the script file that you point your interpreter at. For Python, a closer analogy would be the main block that runs if the file is directly run by the interpreter, denoted as follows:

if __name__ == " __main__":
    print("Hello, World!")

If you were to code in Python, you would probably see this used in a Flask application.

Right now, we have not done anything new. This is a standard Hello, World! example with a little change in syntax; however, even with this example, the string that we print is not all that it seems. For instance, let us write our own function that accepts a string and prints it out with the following code:

fn print(message: str) {
    println!("{}", message);
}
fn main() {
    let message = "hello world";
    print(message);
}

This code should work in other interpreted languages, such as Python or JavaScript. We pass it into our function and print it. However, if we do print it, we get the following printout:

10 |     print(message);
   |           ^^^^^^^ doesn't have a size known at compile-time
   |
   = help: the trait `Sized` is not implemented for `str`
   = note: all function arguments must have a statically known size

This is not very straightforward, but it brings us to the first area we must understand if we are to code in Rust, which is strings. Don’t worry; strings are the quirkiest variables that you need to get your head around to write functional Rust code.

Using strings in Rust

Before we explore the error in the previous section, let us rectify it so that we know what to work toward. We can get the print function to work without any errors with the following code:

fn print(message: String) {
    println!("{}", message);
}
fn main() {
    let message = String::from("hello world");
    print(message);
}

What we did was create a String from "hello world" and pass it into the print function. This time, the compiler did not throw an error because we always know the size of a String, so we can keep the right amount of memory free for it. This may sound counterintuitive because strings are usually of different lengths; it would not be a very flexible programming language if we were only allowed to use the same length of letters for every string in our code. We can make all strings have the same size of memory by passing round pointers, implemented as a vector of bytes, which in Rust is denoted as Vec<u8>. This holds a reference to the string content (str, also known as a string slice) in the heap memory, as seen in the following figure:

A diagram of a stack memory

Description automatically generated

Figure 1.3 – A string’s relationship to str “one”

We can see in Figure 1.3 that a string is a vector of three numbers. One is the actual memory address of the str it references. The second number is the size of the memory allocated, and the third is the length of the string content. Therefore, we can access string literals in our code without having to pass variables of various sizes around our code. We know that String has a set size and, therefore, can allocate this size in the print function parameter. Note that String is on the stack memory while our string literal is on the heap memory.

Considering that we know that String has a set size while our string literal varies, we can deduce that the stack memory is used for predictable memory sizes and is allocated ahead of time when the program runs. The stack memory allocation order is decided on compilation and optimized by the compiler. Our heap memory is dynamic, and therefore, memory is allocated when it is needed.

Now that we know the basics of strings, we can use the different ways in which they are created, as seen in the following code:

    let string_one = "hello world".to_owned();
    let string_two = "hello world".to_string();
    let string_three = string_two.clone();

Note that creating string_three is costly, as we must copy the underlying data in the heap, and heap operations are expensive. This is not a unique quirk of Rust. In our example, we are just experiencing what happens under the hood. For instance, if we alter strings in Python, we will have different outcomes:

# slower method
data = ["one", "two", "three", "four"]
string = ""
for i in data:
    string += i  
# faster method
"".join(data)

Looping through and adding the strings is slower because Python must allocate new memory and copy an entire string to that new memory address. The join method is faster because Python can allocate the memory of all the data of the list and then copy over the strings in the array, meaning the string must only be copied once. This shows us that although high-level languages like Python may not force you to think about the memory allocation of strings, you will still end up paying the price if you don’t acknowledge it.

We can pass a string literal into the print function by borrowing it, as seen in the following Rust code:

fn print(message: &str) {
    println!("{}", message);
}
fn main() {
    let message: &str = "hello world";
    print(message);
}

The : &str is a type annotation of the type of variable that is being assigned to message. We do not need the type annotation; the compiler will work out the type automatically. However, for our exercise, it does illustrate what is happening. The borrow is denoted by &. We will cover borrowing later in the chapter. For now, however, we can deduce that the borrow is only a fixed-size reference to a variable-sized string slice. If the borrow was not a fixed size, we would not be able to pass it into the print function because we would not know the size.

At this point, we can comfortably use strings in Rust productively. The next concept that we must understand before we start writing Rust programs is integers and floats.

Using integers and floats

In most high-level web programming languages, we just assign a float or integer to a variable name and move on with the program. However, from what we have been exposed to in the previous section on strings, we now understand that we must worry about memory size when using strings in Rust. This is no different with integers and floats. We know that integers and floats have a range of sizes. Therefore, we must tell Rust what we pass around our code.

Rust supports signed integers, which are denoted by i, and unsigned integers, which are denoted by u. Both unsigned and signed integers consist of 8, 16, 32, 64, or 128 bits. We will cover what unsigned and signed integers are in this section.

8-byte integers

Exploring the math behind numbers being represented in binary is not relevant for this book; however, we do need to understand the range of numbers that can be represented with several bits, as this will help us understand what the different types of floats and integers in Rust denote.

Because binary is either a 0 or a 1, we can calculate the range of unsigned integers that can be represented by the bits, by raising 2 to the power of the number of bits we have. For example, if we have an integer that is represented by 8 bits, 2 to the power of 8 equates to 256. We must remember that 0 is also represented. Considering this, an integer of 8 bits has a range of 0 to 255. We can test this calculation with the following code:

let number: u8 = 256;

This is one higher than the range that we calculated. As a result, we should not be surprised to see the following overflow error:

the literal `256` does not fit into the type
`u8` whose range is `0..=255`

So we can deduce that if we lower the unsigned integer to 255, it will pass. However, let’s say we change the unsigned integer into a signed integer with the following code:

let number: i8 = 255;

We will see that we get a helpful error message, as follows:

the literal `255` does not fit into the type
`i8` whose range is `-128..=127`

With this helpful error message, we can see that a signed integer considers negative numbers, so the absolute value that a signed integer can take is roughly half.

16-byte integers

We can increase the range by assigning the number as a 16-bit signed integer with the following code:

let number: i16 = 255;

This would work. However, let us add our 16-bit integer with our 8-bit integer, using the following code:

let number = 255i16;
let number_two = 5i8;
let result = number + number_two;

The previous code might look a little different to you. All we have done in the preceding code is define the data type with a suffix instead. So number has a value of 255 and a type of i16, and number_two has a value of 5 and a type of i8. If we run the previous code, we get the following error:

11 |     let result = number + number_two;
   |                         ^ no implementation for `i16 + i8`
   |
   = help: the trait `Add<i8>` is not implemented for `i16`

We will cover traits later in this chapter. For now, all we must understand is that we cannot add the two different integers. If they were both the same type, then we could.

We can change the integer type through casting, using as, as seen in the following line of code:

let result = number + number_two as i16;

This means that number_two is now a 16-bit integer, and result will be 260. However, we must be careful with casting because if we were to do it the wrong way, we could end up with a silent bug, which is unusual for Rust.

Embrace the errors when learning Rust

Rust’s strict compilation process can seem daunting, but it’s a key feature that prevents runtime failures common in other languages. Rust refuses to compile code with errors, ensuring safety and reliability. By encountering and understanding these errors early, you can learn Rust more effectively.

Trying out code variants without error warnings can lead to confusion and frustration later. Teaching Rust by showing errors upfront and explaining them helps us to grasp the language’s principles, making our learning process smoother and more engaging.

If we cast number as i8 instead of casting number_two as i16, then result would equate to 4, which does not make sense because 255 + 5 equals 260. This is because i8 is smaller than i16. So, if we cast an i16 integer as an i8 integer, we are essentially chopping off some of the data, by just taking the lower bits of the number and disregarding the upper bits. Therefore, number ends up being a -1 if we cast it to an i8 integer. To be safer, we can use the i8::from function, as seen in the following code:

let result = i8::from(number) + number_two;

Running this will give us the following error:

let result = i8::from(number) + number_two;
|                  ^^^^^^^^ the trait `From<i16>` is not
                            implemented for `i8`

Again, we will go over traits later on in the chapter, but we can see in the preceding code that because the trait From<i16> is not implemented for an i8 integer, we cannot cast an i8 integer into an i16 integer. With this understanding, we are free to work with integers safely and productively.

Type system to the rescue

Type errors for multiplying two different numerical types prevent silent bugs. If the type system did not flag the differences, then we could have bugs that get thrown when the program is running or, even worse, silent bugs where we don’t know that there is a bug.

Previously, I worked on a financial loss calculation engine. If the number was not loaded to the correct precision of the spec, then the calculations at the end of the financial loss model would be wildly different. The type system here ensures that we do not multiply a number that is a different precision to the spec defined in the code.

Introducing floats

One last point about integer sizes in Rust is that they are not continuous. The supported sizes are shown in the following table:

Bits

Calculation

Size

8

2^8

256

16

2^16

65536

32

2^32

4294967296

64

2^64

1.8446744e+19

128

2^128

3.4028237e+38

Table 1.2 – Size of integer types

When it comes to floats, Rust accommodates f32 and f64 floating point numbers. Both these floating-point types support negative and positive values. Declaring a floating-point variable requires the same syntax as integers, as seen in the following code:

let float: f32 = 2.6;

With this, we can comfortably work with integers and floats in our Rust code. However, we know as developers that just declaring floats and integers is not very useful. We want to be able to contain and loop through them. In the next section, we will do just that with vectors and arrays.

Storing data in arrays

In Rust, we can store our floats, integers, and strings in arrays and vectors. First, we will focus on arrays.

Arrays are stored in stack memory. Knowing this, and remembering what we learned about strings, we can deduce that arrays are of a fixed size. This is because, as we remember, if a variable is stored on the stack, then the memory is allocated on compilation and loaded into the stack when the program starts. We can define an array of integers, loop through it, print each integer, and then access an integer by index with the following code:

fn main() {
    let int_array: [i32; 3] = [1, 2, 3];
    for i in int_array {
        println!("{}", i);
    }
    println!("{}", int_array[1]);
}

With the previous code, we define the type and size by wrapping them in square brackets. For instance, if we were going to create an array of floats with a length of 4, we would use int_array: [f32; 4] = [1.1, 2.2, 3.3, 4.4]. Running the preceding code will give you the following printout:

1
2
3
2

In the preceding printout, we can see that the loop works and we can access the second integer with square brackets. Although the memory size of the array is fixed, we can still change it. This is where mutability comes in.

When we define a variable as mutable, it means that we can mutate it. In other words, we can alter the value of the variable after it has been defined if it is mutable. If you tried to update any of the variables in the code that we have written in this chapter, you will have realized that you can’t. This is because all variables in Rust are immutable by default. However, we can make any variable in Rust mutable by putting a mut tag in front of the variable name.

Going back to the fixed array, we cannot change the size of it, meaning we cannot append/push new integers to it due to it being stored in stack memory. However, if we define a mutable array, we can update parts of it with other integers that are the same memory size. An example of this is the following code:

fn main() {
    let mut mutable_array: [i32; 3] = [1, 2, 0];
    mutable_array[2] = 3;
    println!("{:?}", mutable_array);
    println!("{}", mutable_array.len());
}

In the preceding code, we can see that the last integer in our array is updated to 3. We then print out the full array and then the length. You may have also noted that the first print statement of the preceding code now employs {:?}. This calls the Debug trait. If Debug is implemented for the thing that we are trying to print, then the full representation of the thing we print is displayed in the console. You can also see that we print out the result of the length of the array. Running this code will give the following printout:

[1, 2, 3]
3

With the preceding printout, we can confirm that the array is now updated. We can also access slices with our arrays. To demonstrate this, we can create an array of 100 zeros. We can then take a slice of this and print it out with the following code:

fn main() {
    let slice_array: [i32; 100] = [0; 100];
    println!("length: {}", slice_array.len());
    println!("slice: {:?}", &slice_array[5 .. 8]);
}

Running the preceding code will result in the following printout:

length: 100
slice: [0, 0, 0]

We are now able to be productive with arrays. Arrays can be useful for caching. For instance, if we know the amount that we need to store, then we can use arrays effectively. However, we have only managed to store one type of data in the array. If we tried to store strings and integers in the same array, we would have a problem. How would we define the type? This problem applies to all collections, such as vectors and HashMaps. There are multiple ways to do this, but the most straightforward is using enums.

Storing data in vectors

What we have covered with arrays can be applied to vectors. The only difference is that we do not have to define the length and can increase the size of the vector if needed. This is because vectors put their data onto the heap, meaning that vectors are pointers, pointing to a collection of items in heap memory that are all next to each other, making vectors fast for looping through and reading data. If the allocated memory on the heap runs out, the vector can allocate a bigger section of memory and copy the existing data over to the new memory address. To demonstrate this flexibility, we will create a vector of strings and then add a string to the end, with the following code:

    let mut string_vector: Vec<&str> = vec!["one", "two", "three"];
    println!("{:?}", string_vector);
    string_vector.push("four");
    println!("{:?}", string_vector);

In the preceding code, we can see that we use the vec! macro to create the vector of strings. You may have noticed with macros such as vec! and println! that we can vary the number of inputs. We will cover macros later in the chapter. Running the preceding code will result in the following printout:

["one", "two", "three"]
["one", "two", "three", "four"]

We can also create an empty vector with the new function from the Vec struct, with let _empty_vector: Vec<&str> = Vec::new();.

You may be wondering when to use vectors and when to use arrays. Vectors are more flexible. You may be tempted to opt for arrays for performance gains. At face value, this seems logical, as arrays are stored in the stack. Accessing the stack is going to be quicker because the memory sizes can be computed at compile time, making the allocation and deallocation simpler compared to the heap. However, because it is on the stack, it cannot outlive the scope that it is allocated. Moving a vector around would require moving a pointer around. However, moving an array requires copying the whole array. Therefore, copying fixed-size arrays is more expensive than moving a vector. If you have a small amount of data that you only need in a small scope and you know the size of the data, then reaching for an array does make sense. However, if you’re going to be moving the data around, even if you know the size of the data, using vectors is a better choice.

We must also take note of our initial introduction of vectors. If the heap memory allocated for a vector runs out, the data is copied over to a new address. This copying over is not free. If you know the size of the data you need, you can prevent this copying with a Vec::with_capacity(100) constructor, initially setting aside the memory you need. 100 is just an example of a size of items to be put in a vector; you have the freedom to input whatever value you need. Don’t worry if you exceed this later; the memory reallocation will still work.

Now that we can be productive with basic collections, we can move on to a more advanced collection, a HashMap.

Mapping data with enums

Enums are, well, enums. In dynamic languages such as Python, you may not have had to use them, due to being able to pass any type anywhere you want. However, they are still available. Enum is short for enumerated type and basically defines a type with possible variants. In our case, we want our array to store strings and integers in the same collection. We can do this by initially defining our enum with the following code:

enum SomeValue {
    StringValue(String),
    IntValue(i32)
}

In the preceding code, we can see that we defined an enum with the name of SomeValue. We then denoted that StringValue holds the value of a string and that IntValue holds the value of an integer. We can then define an array with a length of 4, consisting of two strings and two integers, with the following code:

    let multi_array: [SomeValue; 4] = [
                SomeValue::StringValue(String::from("one")),
                SomeValue::IntValue(2),
                SomeValue::StringValue(String::from("three")),
                SomeValue::IntValue(4)
                ];

In the preceding code, we can see that we wrap our strings and integers in our enum. Now, looping through and exporting it is going to be another task. For instance, there are things that we can do to an integer that we cannot do to a string, and vice versa. Considering this, we are going to have to use a match statement when looping through the array, as seen in the following code:

    for i in multi_array {
        match i {
            SomeValue::StringValue(data) => {
                println!("The string is: {}", data);
            },
            SomeValue::IntValue(data) => {
                println!("The int is: {}", data);
            }
        }
    }

In the preceding code, we can see that if i is SomeValue::StringValue, we then assign the data wrapped in SomeValue::StringValue to the variable name data. We then pass data into the inner scope to be printed. We use the same approach with our integer. Even though we are merely printing to demonstrate the concept, we can do anything in these inner scopes to the data variable that the type allows us to. Running the preceding code gives the following printout:

The string is: one
The int is: 2
The string is: three
The int is: 4

Using enums to wrap data and match statements to handle them can be applied to HashMaps and vectors.

Mapping data with HashMaps

In some other languages, HashMaps are referred to as dictionaries. They have a key and a value. We can insert and get values using the key. Now that we have learned about handling collections, we can get a little more adventurous in this section.

We can create a simple profile of a game character. In this character profile, we are going to have a name, age, and a list of items that they have. This means that we need an enum that houses a string, an integer, and a vector that also houses strings. We will want to print out the complete HashMap to see if our code is correct in one glance. To do this, we are going to implement the Debug trait for our enum, as seen in the following code:

#[derive(Debug)]
enum CharacterValue {
    Name(String),
    Age(i32),
    Items(Vec<String>)
}

In the preceding code, we can see that we have annotated our enum with the derive attribute. An attribute is metadata that can be applied to the CharacterValue enum in this case. The derive attribute tells the compiler to provide a basic implementation of a trait. So, in the preceding code, we are telling the compiler to apply the basic implementation of Debug to the CharacterValue enum. With this, we can then create a new HashMap that has keys pointing to the values we defined in the preceding code with the following:

use std::collections::HashMap;
fn main() {
    let mut profile: HashMap<&str, CharacterValue> = HashMap::new();
}

We stated that it is mutable because we are going to insert values with the following code:

profile.insert("name", CharacterValue::Name("Maxwell".to_string()));
profile.insert("age", CharacterValue::Age(34));
profile.insert("items", CharacterValue::Items(vec![
                                        "laptop".to_string(),
                                        "book".to_string(),
                                        "coat".to_string()
                                        ]));
println!("{:?}", profile);

We can see that we have inserted all the data that we need. Running this would give us the following printout:

{"items": Items(["laptop", "book", "coat"]), "age": Age(34),
"name": Name("Maxwell")}

In the preceding output, we can see that our data is correct. Inserting it is one thing; however, we now must get it out again. We can do this with a get associated function.

Associated functions are functions attached to a type, such as an enum, struct, or datatype. They can reference the entity that they are associated with, or even consume them. In some cases, a reference to the type they are associated with isn’t needed at all. Later on in the book, we will associate functions with database handle structs where the functions have no reference to the struct.

The get associated function returns an Option type. The Option type returns either Some or None. So if we were to get name from our HashMap, we would need to do two matches, as seen in the following code:

match profile.get("name") {
    Some(value_data) => {
        match value_data {
            CharacterValue::Name(name) => {
                println!("the name is: {}", name);
            },
            _ => panic!("name should be a string")
        }
    },
    None => {
        println!("name is not present");
    }
}

In the preceding code, we can check to see if there is a name in the keys. If there is not, then we just print out that it was not present. If the name key is present, we then move on to our second check, which prints out the name if it is CharacterValue::Name. However, there is something wrong if the name key does not house CharacterValue::Name. So, we add only one more check to match, which is _. This is a catch, meaning anything else. We are not interested in anything other than CharacterValue::Name. Therefore, the _ catch maps to a panic! macro, which essentially throws an error.

We could make this shorter. If we know that the name key is going to be in the HashMap, we can employ the unwrap function with the following code:

    match profile.get("name").unwrap() {
        CharacterValue::Name(name) => {
            println!("the name is: {}", name);
        },
        _ => panic!("name should be a string")
    }

The unwrap function directly exposes the result. However, if the result is None, then it will directly result in an error terminating the program, which would look like the following printout:

thread 'main' panicked at 'called `Option::unwrap()` on a `None` value'

Using the unwrap function is risky, and we should try and avoid it as much as possible. We can avoid using the unwrap function by handling our results and errors, which we cover in the next section.

Handling results and errors

In the previous section, we learned that directly unwrapping Option, resulting in None, panics a thread. There is another outcome that can also throw an error if unsuccessfully unwrapped, and this is Result. The Result type can return either Ok or Err. To demonstrate this, we can create a basic function that returns a Result type, based on a simple Boolean that we pass into it, with the following code:

fn error_check(check: bool) -> Result<i8, &'static str> {
    if check {
        Err("this is an error")
        }
    else {
        Ok(1)
    }
}

In the preceding code, we can see that we return Result<i8, &'static str>. This means that we return an integer if Result is Ok, or we return an integer if Result is Err. The &'static str variable is basically our error string. We can tell that it’s a reference because of &. The 'static part means that the reference is valid for the entire lifetime of the running program. If this does not make sense now, don’t worry; we will cover lifetimes later in the chapter.

Now that we have created our error-checking function, we can test to see what these outcomes look like with the following code:

fn main() {
    println!("{:?}", error_check(false));
    println!("{:?}", error_check(false).is_err());
    println!("{:?}", error_check(true));
    println!("{:?}", error_check(true).is_err());
}

Running the preceding code gives us the following printout:

Ok(1)
false
Err("this is an error")
true

In the preceding output, we can see that it returned exactly what we wanted. Also note that we can run the is_err() function on Result variable, resulting in false if we return Ok or true if we return Err. We can also directly unwrap but add extra tracing to the stack trace with the following expect function:

let result: i8 = error_check(true).expect("this has been caught");

The preceding function will result in the following printout:

thread 'main' panicked at 'this has been caught: "this is an error"'

Through the preceding example, we can see that we get the message from the expect function first, and then the error message returned in Result. With this understanding, we can throw, handle, and add extra tracing to errors.

We can now throw errors, but how can we handle them? An obvious choice would be a match statement, but we can also use the ? operator. For instance, let us return the same return type for another function that calls our error_check function, as seen in the following code:

fn error_check_two(check: bool) -> Result<i8, &'static str> {
    let outcome: i8 = error_check(check)?;
    Ok(outcome)
}

Here, we can see that the ? operator is used when calling error_check(check)?. What happens here is that if the error_check function returns an error, that error is just directly returned as an error for the error_check_two function. If the error_check function returns an Ok result, then this is automatically unwrapped and assigned to the variable outcome. This will save you a lot of code. Throughout the book, we will map errors so that we can exploit the ? operator, instead of writing matches everywhere. We will even configure our errors to automatically construct an HTTP response. It’s a complete myth that we need to write a lot of Rust code for web programming.

Rust data types and variables cheatsheet

  • Strings: The String type is heap-allocated and its size is known, whereas &str is a fixed-size reference to a string slice. Use String::from or .to_string() to create a String. Use &str to borrow a string slice without ownership transfer.
  • Integers and floats: Rust requires explicit size declarations for integers (e.g., i8, u16) and floats (e.g., f32 and f64). Different integer sizes have specific ranges (e.g., u8 ranges from 0 to 255, and i8 ranges from -128 to 127). Type errors are caught at compile time to prevent runtime bugs.
  • Mutability: Variables are immutable by default. Use mut to make a variable mutable, allowing its value to be changed.
  • Arrays: Fixed-size, stack-allocated collections. Here’s an example: let int_array: [i32; 3] = [1, 2, 3];
  • Vectors: Dynamic-size, heap-allocated collections. Here’s an example: let mut vec = Vec::new(); vec.push(1);
  • Enums: Used to define a type with possible variants. Here’s an example: enum SomeValue { StringValue(String), IntValue(i32) }
  • Result type: Used for functions that can return an error. Here’s an example: fn error_check(check: bool) -> Result<i8, &'static str> { ... }
  • Error handling: Use match, unwrap, or the ? operator to handle results and propagate errors.

However, we are getting more exposed to lifetimes and borrow references as we move forward. Now is the time to address this by understanding variable ownership.

Controlling variable ownership

As we remember from the beginning of the chapter, Rust does not have a garbage collector. However, it has memory safety. It achieves this by having strict rules around variable ownership. These rules are enforced when Rust is being compiled. If you are accustomed to a dynamic language, then this can initially lead to frustration. This is known as fighting the borrow checker. Sadly, this unjustly gives Rust its steep learning curve reputation, as when you are fighting the borrow checker without knowing what is going on, it can seem like an impossible task to write even the most basic programs. However, if we take the time to learn the rules before we try and code anything too complex, the knowledge of the rules and the helpfulness of the compiler will make writing code in Rust fun and rewarding. Again, I take the time to remind you that Rust has been the most favorite language nine years in a row. This is not because it’s impossible to get anything done in it. The people who vote for Rust in these surveys understand the rules around ownership. Rust’s compiling, checking, and enforcing of these rules protect against the following errors:

  • Use-after-frees: This occurs when memory is accessed once it has been freed, which can cause crashes. It can also allow hackers to execute code via this memory address.
  • Dangling pointers: This occurs when a reference points to a memory address that no longer houses the data that the pointer was referencing. Essentially, this pointer now points to null or random data.
  • Double frees: This occurs when allocated memory is freed and then freed again. This can cause a program to crash and increases the risk of sensitive data being revealed. It also enables a hacker to execute arbitrary code.
  • Segmentation faults: This occurs when the program tries to access the memory it’s not allowed to access.
  • Buffer overrun: An example of this error is reading off the end of an array. This can cause a program to crash.

To protect against these errors and thus achieve memory safety, Rust enforces the following rules:

  • Values are owned by the variables assigned to them.
  • As soon as the variable moves out of the scope of where it was defined, it is then deallocated from the memory.
  • Values can be referenced and altered if we adhere to the rules for copying, moving, immutable borrowing, and mutable borrowing.

Knowing the rules is one thing, but to practically work with the rules in Rust code, we need to understand copying, moving, and borrowing in more detail.

Copying variables

Copying occurs when a value is copied. Once it has been copied, the new variable owns the value, while the existing variable also continues to own its value.

Figure 1.4 – Variable copy path

In Figure 1.4, we can see that the path of One is still solid, which denotes that it has not been interrupted and can be handled as if the copy did not happen. Path Two is merely a copy, and there is also no difference in the way in which it can be utilized, as if it were self-defined.

Note that if the variable has a copy trait, then the variable will automatically be copied without us having to write any extra code, as seen in the following code:

let one: i8 = 10;
let two: i8 = one + 5;
println!("{}", one);
println!("{}", two);

Running the preceding code will give us the following printout:

10
15

In the preceding example, we appreciate that the very fact that variables one and two can be printed indicates that one has been copied for two to utilize. To test this, we can test our example with strings using the following code:

let one = "one".to_string();
let two = one;
println!("{}", one);
println!("{}", two);

Running this code will result in the following error:

move occurs because `one` has type `String`, which does not implement the `Copy` trait

Because strings do not implement the Copy trait, the code does not work, as one was moved to two.

It is only natural to wonder why strings do not have the Copy trait. This is because the string is a pointer to a string literal. If we were to copy strings, we would have multiple unconstrained pointers to the same string literal data, which would be dangerous.

However, the code will run if we get rid of println!("{}", one);. This brings us to the next concept that we must understand, moving.

Moving variables

Moving refers to when a value is moved from one variable to another. However, unlike copying, the original variable no longer owns the value.

Figure 1.5 – Variable move path

From what we can see in Figure 1.5, one can no longer be accessed once it’s moved to two. To really establish what is going on here and how strings are affected, we can set up some code designed to fail, as follows:

let one: String = String::from("one");
let two: String = one + " two";
println!("{}", two);
println!("{}", one);

Running the preceding code gives the following error:

let one: String = String::from("one");
    --- move occurs because `one` has type
    `String`, which does not implement the
    `Copy` trait
let two: String = one + " two";
                  ------------ `one` moved due to usage in operator
println!("{}", two);
println!("{}", one);
               ^^^ value borrowed here after move

As we can see, the compiler has been helpful here. It shows us where the string was moved to and where the value of that string is borrowed. So we can make the code run instantly by merely removing the line println!("{}", one);. However, we want to be able to use that print function at the bottom of the preceding code block. We should not have to constrain the functionality of the code due to the rules implemented by Rust. We can solve this by using the to_owned function with the following code:

let two: String = one.to_owned() + " two";

The to_owned function is available because strings implement the ToOwned trait. We will cover traits later in the chapter, so carry on reading even if you do not know what this means yet. We could also have used clone on the string. We must note that to_owned is a generalized implementation of clone. However, it does not really matter which approach we use.

Because of this, we can explore the move concept using strings. If we force our string outside of the scope with a function, we can see how this affects our move. This can be done with the following code:

fn print(value: String) {
    println!("{}", value);
}
fn main() {
    let one = "one".to_string();
    print(one);
    println!("{}", one);
}

If we run the preceding code, we will get an error, stating that the print function moved the one value. As a result, the println!("{}", one); line borrows one after it is moved into the print function. The key part of this message is the word borrow. To understand what is going on, we need to explore the concept of immutable borrowing.

Immutable borrowing of variables

An immutable borrow occurs when a variable can be referenced by another variable without having to clone or copy it. This essentially solves our problem. If the borrowed variable falls out of scope, then it is not deallocated from the memory and the original reference to the value can still be used.

Shape, rectangle

Description automatically generated

Figure 1.6 – Immutable borrow path

We can see in Figure 1.6 that two borrows the value from one. It must be noted that when one is borrowed from, one is locked and cannot be accessed until the borrow is finished.

To perform a borrow operation, we apply a prefix with &. This can be demonstrated with the following code:

fn print(value: &String) {
    println!("{}", value);
}
fn main() {
    let one = "one".to_string();
    print(&one);
    println!("{}", one);
}

In the preceding code, we can see that our immutable borrow enables us to pass a string into the print function and still print it afterward. This can be confirmed with the following printout:

one
one

From what we see in our code, the immutable borrow that we performed can be demonstrated in Figure 1.7.

A diagram of a function

Description automatically generated

Figure 1.7 – Immutable borrow in relation to the print function

In the preceding figure, we can see that one is not available when the print function is running. We can demonstrate this with the following code:

fn print_two(value: &String, value_two: String) {
    println!("{}", value);
    println!("{}", value_two);
}
fn main() {
    let one = "one".to_string();
    print_two(&one, one);
    println!("{}", one);
}

If we run the preceding code, we will get the following error:

Print_two(&one, one);
----- ----  ^^^ move out of `one` occurs here
|     |
|     borrow of `one` occurs here
borrow later used by call

We can see that we cannot utilize one, even though it is utilized in the print function after &one. This is because the lifetime of &one is throughout the entire lifetime of the print function. Thus, we can conclude that Figure 1.7 is correct. However, we can run one more experiment. We can change value_one to a borrow to see what happens with the following code:

fn print_two(value: &String, value_two: &String) {
    println!("{}", value);
    println!("{}", value_two);
}
fn main() {
    let one = "one".to_string();
    print_two(&one, &one);
    println!("{}", one);
}

In the preceding code, we can see that we do two immutable borrows of one, and the code then runs. This highlights an important fact: we can make as many immutable borrows as we like. This is safe because both borrows cannot mutate the value; therefore, we are confident that we know what the borrow points to. However, what happens if the borrow is mutable? To understand, we must explore mutable borrows.

Mutable borrowing of variables

A mutable borrow is essentially the same as an immutable borrow, except that the borrow is mutable and that we cannot have more than one mutable borrow at the same time. To understand this, let’s say that we have a mutable reference, and this mutable reference updates the vector to the point where the memory gets reallocated. Therefore, the immutable reference will point to memory that is no longer allocated to the vector. This can lead to undefined behavior.

With mutable borrows, we can change the borrowed value. To demonstrate this, we can create a print statement that will alter the borrowed value before printing it. We then print it in the main function to establish that the value has been changed with the following code:

fn print(value: &mut i8) {
    *value += 1;
    println!("In function the value is: {}", value);
}
fn main() {
    let mut one: i8 = 5;
    print(&mut one);
    println!("In main the value is: {}", one);
}

Running the preceding code will give us the following printout:

In function the value is: 6
In main the value is: 6

The preceding output proves that one is 6 even after the lifetime of the mutable reference in the print function has expired. We can see that in the print function, we update the value of one using a * operator. This is called a dereference operator. This dereference operator exposes the underlying value so that it can operate. This all seems straightforward, but is it exactly like our immutable references? If we remember, we could have multiple immutable references. We can put this to the test with the following code:

fn print_two(value: &mut i8, value_two: &mut i8) {
    *value += 1;
    println!("In function the value is: {}", value);
    *value_two += 1;
}
fn main() {
    let mut one: i8 = 5;
    print_two(&mut one, &mut one);
    println!("In main the value is: {}", one);
}

In the preceding code, we can see that we make two mutable references and pass them through, just like in the previous section but with immutable references. However, running it gives us the following error:

error[E0499]: cannot borrow `one` as mutable more than once at a time

Through this example, we can confirm that we cannot have more than one mutable reference at a time. This prevents data races and has given Rust the fearless concurrency tag.

Controlling variable ownership cheatsheet

  • Error prevention: Rust prevents dangling pointers, double frees, segmentation faults, and buffer overruns.
  • Copying: Types with the Copy trait can be duplicated without explicit code.
  • Moving: Values are transferred and the original variable loses ownership.
  • Immutable borrowing:
  • Allows you to reference a variable without transferring ownership
  • Multiple immutable borrows are permitted, since they do not mutate the value
  • Mutable borrowing: Only one mutable borrow at a time is allowed, enabling safe value alterations.
  • Dereference operator: Used to access the value referenced by a pointer.

With what we have covered here, we can now be productive when the compiler is combined with the borrow checker. However, we have only touched on the concepts of scope and lifetimes. The use of them has been intuitive, but like the rules around borrowing, we need to dive into scopes and then lifetimes in more detail.

Scopes

To understand scopes, let us go back to how we declare variables. You will have noticed that when we declare a new variable, we use let. When we do, that variable is the only one that owns the resource. Therefore, if the value is moved or reassigned, then the initial variable no longer owns the value. When a variable is moved, it is essentially moved into another scope. Variables declared in an outer scope can be referenced in an inner scope, but a variable declared in an inner scope cannot be accessed in the inner scope once the inner scope has expired. We can break down some code into scopes, as shown in the following diagram:

Figure 1.8 – Basic Rust code broken into scopes

Figure 1.8 shows us that we can create an inner scope by merely using curly brackets. Applying what we just learned about scopes to Figure 1.8, can you work out whether it would compile? If it would crash, how would it?

If you guessed that it would result in a compiler error, then you are correct. Running the code would result in the following error:

println!("{}", two);
               ^^^ not found in this scope

Because one is defined in the inner scope, we will not be able to reference it in the outer scope. We can solve this problem by declaring the variable in the outer scope but assigning the value in the inner scope, with the following code:

fn main() {
    let one = &"one";
    let two: &str;
    {
        println!("{}", one);
        two = &"two";
    }
    println!("{}", one);
    println!("{}", two);
}

In the preceding code, we can see that we do not use let when assigning the value because we have already declared the variable in the outer scope. Running the preceding code gives us the following printout:

one
one
two

We also must remember that if we move a variable into a function, then the variable gets destroyed once the scope of the function finishes. We cannot access the variable after the execution of the function, even though we declared the variable before the execution of the function. This is because once the variable has been moved into the function, it is no longer in the original scope. It has been moved. And because the variable has been moved to that scope, it is then bound to its lifetime. This brings us to our next section, lifetimes.

Running through lifetimes

Understanding lifetimes will wrap up our exploration of borrowing rules and scopes. We can explore the effect of lifetimes with the following code:

fn main() {
    let one: &i8;
    {
        let two: i8 = 2;
        one = &two;
    } // -----------------------> two lifetime stops here
    println!("r: {}", one);
}

With the preceding code, we declare one before the inner scope starts. However, we assign it to have a reference of two. This two only has the lifetime of the inner scope, so the lifetime dies before we try and print it out. This is established with the following error:

one = &two;    }    println!("r: {}", one);}
      ^^^^     -                      --- borrow later used here
      |        |
      |        `two` dropped here while still borrowed
      borrowed value does not live long enough

two is dropped when the lifetime of two has finished. With this, we can state that the lifetimes of one and two are not equal.

While it is great that this is flagged when compiling, Rust does not stop here. This concept also applies to functions. Let’s say that we build a function that references two integers, compares them, and returns the highest integer reference. The function is an isolated piece of code. In this function, we can denote the lifetimes of the two integers. This is done by using the ' prefix, which is a lifetime notation. The names of the notations can be anything you come up with, but it is conventional to use a, b, c, and so on. We can explore this by creating a simple function that takes in two integers and returns the highest one, with the following code:

fn get_highest<'a>(first_number: &'a i8, second_number: &'a
    i8) -> &'a i8 {
    if first_number > second_number {
        first_number
        } else {
        second_number
    }
}
fn main() {
    let one: i8 = 1;
    let outcome: &i8;
    {
        let two: i8 = 2;
        let outcome: &i8 = get_highest(&one, &two);
    }
    println!("{}", outcome);
}

As we can see, the first and second lifetimes have the same notation of a. They both must be present for the duration of the function. Note that the function returns an i8 integer with the lifetime of a. If we were to try and use lifetime notation on function parameters without a borrow, we would get some very confusing errors. In short, it is not possible to use lifetime notation without a borrow. This is because if we do not use a borrow, the value passed into the function is moved into the function. Therefore, its lifetime is the lifetime of the function. This seems straightforward; however, when we run it, we get the following error:

println!("{}", outcome);}
               ^^^^^^^ use of possibly-uninitialized `outcome`

The error occurs because all the lifetimes of the parameters passed into the function and the returned integer are all the same. Therefore, the compiler does not know what could be returned. As a result, two could be returned. If two is returned, then the result of the function will not live long enough to be printed. However, if one is returned, then it will. Therefore, there is a possibility of not having a value to print after the inner scope is executed. In a dynamic language, we would be able to run code that runs the risk of referencing variables that have not been initialized yet. However, with Rust, we can see that if there is a possibility of an error like this, it will not compile.

In the short term, it might seem like Rust takes longer to code, but as the project progresses, this strictness will save a lot of time by preventing silent bugs. In conclusion of our error, there is no way of solving our problem with the exact function and main layout that we have. We would either move our printing of the outcome into the inner scope or clone the integers and pass them into the function.

We can create one more function to explore functions with different lifetime parameters. This time, we will create a filter function. If the first number is lower than the second number, we will then return 0; otherwise, we will return the first number. This can be achieved with the following code:

fn filter<'a, 'b>(first_number: &'a i8, second_number: &'b
    i8) -> &'a i8 {
    if first_number < second_number {
        &0
    } else {
        first_number
    }
}
fn main() {
    let one: i8 = 1;
    let outcome: &i8;
    {
        let two: i8 = 2;
        outcome = filter(&one, &two);
    }
    println!("{}", outcome);
}

The preceding code works because we know the lifetimes are different. The first parameter has the same lifetime as the returned integer. If we were to implement filter(&two, &one) instead, we would get an error stating that the outcome does not live long enough to be printed.

Variable scope and lifetimes cheatsheet

  • Variable ownership: Variables declared with let own their values; reassignment moves ownership.
  • Scope rules: Outer scope variables are accessible in inner scopes, but not vice versa.
  • Function scope: Variables moved into a function are destroyed once the function scope ends.
  • Lifetime mismatch: Variables must outlive the references assigned to them.
  • Function lifetimes: Lifetime annotations ensure that function parameters and return values live long enough.

We have now covered all that we need to know for now to write productive code in Rust without the borrow checker getting in our way. We now need to move on to creating bigger building blocks for our programs, allowing us to focus on tackling the complex problems we want to solve with code. We will start this with a versatile building block of programs, structs.

Building Structs

In modern high-level dynamic languages, objects have been the bedrock for building big applications and solving complex problems, and for good reason. Objects enable us to encapsulate data, functionality, and behavior. In Rust, we do not have objects. However, we do have structs that can hold data in fields. We can then manage the functionality of these structs and group them together with traits. This is a powerful approach, and it gives us the benefits of objects without the high coupling, as highlighted in the following figure:

Diagram

Description automatically generated with medium confidence

Figure 1.9 – Difference between Rust structs and objects

We will start with something basic by creating a Human struct with the following code:

#[derive(Debug)]
struct Human<'a> {
    name: &'a str,
    age: i8,
    current_thought: &'a str
}

In the preceding code, we can see that our string literal fields have the same lifetime as the struct itself. We have also applied the Debug trait to the Human struct, so we can print it out and see everything. We can then create the Human struct and print it out, using the following code:

fn main() {
    let developer = Human{
        name: "Maxwell Flitton",
        age: 34,
        current_thought: "nothing"
    };
    println!("{:?}", developer);
    println!("{}", developer.name);
}

Running the preceding code will give us the following printout:

Human { name: "Maxwell Flitton", age: 34, current_thought: "nothing" }
Maxwell Flitton

We can see that our fields are what we expect. However, we can change our string slice fields to strings to get rid of lifetime parameters. We may also want to add another field where we can reference another Human struct under a friend field. However, we may also have no friends. We can account for this by creating an enum that is either a friend or not and assigning it to a friend field, as seen in the following code:

#[derive(Debug)]
enum Friend {
    HUMAN(Human),
    NIL
}
#[derive(Debug)]
struct Human {
    name: String,
    age: i8,
    current_thought: String,
    friend: Friend
}

We can then define the Human struct initially with no friends, just to see if it works with the following code:

    let developer = Human{
        name: "Maxwell Flitton".to_string(),
        age: 32,
        current_thought: "nothing".to_string(),
        friend: Friend::NIL
    };

However, when we run the compiler, it does not work. I would like to think this is because the compiler cannot believe that I have no friends. But alas, it has to do with the compiler not knowing how much memory to allocate for this declaration. This is shown through the following error code:

enum Friend {    HUMAN(Human),    NIL}#[derive(Debug)]
^^^^^^^^^^^            ----- recursive without indirection
|
recursive type has infinite size

Because of the enum, theoretically, the memory needed to store this variable could be infinite. One Human struct could reference another Human struct as a friend field, which could in turn reference another Human struct, resulting in a potentially infinite number of Human structs being linked together through the friend field. We can solve this problem with pointers. Instead of storing all the data of a Human struct in the friend field, we store a memory address that we know has a maximum value because it’s a standard integer. This memory address points to where another Human struct is stored in the memory. As a result, the program knows exactly how much memory to allocate when it crosses a Human struct, irrespective of whether the Human struct has a friend field or not. This can be achieved by using a Box struct, which is essentially a smart pointer for our enum, with the following code:

#[derive(Debug)]
enum Friend {
    HUMAN(Box<Human>),
    NIL
}

So now, our enum states whether the friend exists or not, and if so, it has a memory address if we need to extract information about this friend. We can achieve this with the following code:

fn main() {
    let another_developer = Human{
        name: "Caroline Morton".to_string(),
        age:30,
        current_thought: "I need to code!!".to_string(),
        friend: Friend::NIL
    };
    let developer = Human{
        name: "Maxwell Flitton".to_string(),
        age: 34,
        current_thought: "nothing".to_string(),
        friend: Friend::HUMAN(Box::new(another_developer))
    };
    match &developer.friend {
        Friend::HUMAN(data) => {
            println!("{}", data.name);
        },
        Friend::NIL => {}
    }
}

In the preceding code, we can see that we have created one Human struct, and then another Human struct with a reference to the first Human struct as a friend field. We then access the second Human struct’s friend through the friend field. Remember, we must handle both possibilities, as the friend field could be a nil value.

While it is exciting that friends can be made, if we take a step back, we can see that there is a lot of code written for each human we create. This is not helpful if we must create a lot of humans in a program. We can reduce this by implementing some functionality for our struct. We will essentially create a constructor for the struct with extra functions, so we can add optional values if we want. We will also make the thought field optional. So a basic struct with a constructor populating only the most essential fields can be achieved with the following code:

#[derive(Debug)]
struct Human {
    name: String,
    age: i8,
    current_thought: Option<String>,
    friend: Friend
}
impl Human {   
    fn new(name: &str, age: i8) -> Human {
        return Human{
            name: name.to_string(),
            age: age,
            current_thought: None,
            friend: Friend::NIL
        }
    }
}

Therefore, creating a new human now only takes the following line of code:

let developer = Human::new("Maxwell Flitton", 34);

This will have the following field values:

  • Name: "Maxwell Flitton"
  • Age: 34
  • Current Thought: None
  • Friend: NIL

We can add more functions to the implement block for adding friends and a current thought with the following code:

    fn with_thought(mut self, thought: &str) -> Human {
        self.current_thought = Some(thought.to_string());
        return self
    }
    fn with_friend(mut self, friend: Box<Human>) -> Human {
        self.friend = Friend::HUMAN(friend);
        return self
    }

In the preceding code, we can see that we pass in a mutable version of the struct that is calling these functions. These functions can be chained because they return the struct that called them. If we want to create a developer with a thought, we can do this with the following code:

let developer = Human::new("Maxwell Flitton", 34).with_thought(
                                             "I love Rust!");

Note that a function that does not require self as a parameter can be called with ::, while a function that does require self as a parameter can be called with a simple dot, .. If we want to create a developer with a friend, it can be done using the following code:

let developer_friend = Human::new("Caroline Morton", 30);
let developer = Human::new("Maxwell Flitton", 34)
                       .with_thought("I love Rust!")
                       .with_friend(Box::new(developer_friend));
println!("{:?}", developer);

Running the code will result in the following parameters for developer:

Name: "Maxwell Flitton"
Age: 34
Current Thought: Some("I love Rust!")
Friend: HUMAN(Human { name: "Caroline Morton", age: 30,
                     current_thought: None, friend: NIL })

We can see that structs combined with enums and functions that have been implemented with these structs can be powerful building blocks. We can define fields and functionality with only a small amount of code if we have defined our structs well. However, writing the same functionality for multiple structs can be time-consuming and result in a lot of repeated code. If you have worked with objects before, you may have utilized inheritance for that.

Rust goes one better. It has traits, which we will explore in the next chapter.

Building structs cheatsheet

  • No objects: Rust uses structs instead of objects to encapsulate data and functionality.
  • Struct definition: Structs hold data in fields (e.g., struct Human { name: String, age: i8 }).
  • String literals in structs: String literals in structs require lifetime parameters.
  • Box for recursive types: Use Box to handle recursive types, preventing infinite memory allocation.
  • Printing structs: Implement the Debug trait to print struct details.
  • Constructors in structs: Implement a new function in the impl block for struct initialization.
  • Function calling: Use :: for static methods and . for instance methods. Add methods to the impl block to chain function calls (e.g., with_thought).
  • Traits for reusability: Use traits to avoid repeated code and enhance functionality across multiple structs.

Summary

With Rust, we have seen that there are some traps when coming from a dynamic programming language background. However, with a little bit of knowledge of referencing and basic memory management, we can avoid common pitfalls and write safe, performant code quickly that can handle errors.

In this chapter, we also covered the concepts of borrowing and referencing in Rust. While adhering to borrowing rules requires more effort than coding in a garbage-collected language, we have a deeper understanding of how variables are placed in memory. This deeper understanding is safer as we know exactly what data we point to. For example, if we were using a language like Python and created an instance of an object when we passed it into two dictionaries (Python’s version of a hash map), then if we updated one instance, the other value in the hash map would also be updated because it is a shared reference to the same memory address. The developer, however, may never know. In Rust, the borrow-checking rules make what is going on explicit. Scopes also make it explicit when a variable is dropped from memory.

We now know enough Rust to get started. In the next chapter, we will cover useful patterns for web programming and metaprogramming.

Questions

  1. What is the difference between a str and a String?
  2. Why can’t string slices be passed into a function (string slice meaning str as opposed to &str)?
  3. How do we access the data belonging to a key in a HashMap?
  4. When a function results in an error, can we handle other processes, or will the error crash the program instantly?
  5. Why does Rust only allow one mutable borrow at a point in time?
  6. When would we need to define two different lifetimes in a function?
  7. How can structs link to the same struct via one of their fields?
  8. How can we add extra functionality to a struct where the functionality can also be implemented by other structs?

Answers

  1. A String is a fixed-size reference stored in the stack that points to string-type data on the heap. A str is an immutable sequence of bytes stored somewhere in memory. Because the size of the str is unknown, it can only be handled by a pointer, &str.
  2. Since we do not know the size of the string slice at compile time, we cannot allocate the correct amount of memory for it. Strings, on the other hand, have a fixed-size reference stored on the stack that points to the string slice on the heap. Because we know this fixed size of the string reference, we can allocate the correct amount of memory and pass it through to a function.
  3. We use the HashMap’s get function. However, we must remember that the get function merely returns an Option struct. If we are confident that there is something there or we want the program to crash if nothing is found, we can directly unwrap it. However, if we don’t want that, we can use a match statement and handle the Some and None output as we wish.
  4. No, results must be unwrapped before exposing the error. A simple match statement can handle unwrapping the result and managing the error as we see fit.
  5. Rust only allows one mutable borrow to prevent memory unsafety. In Goregaokar’s blog (see the Further reading section), the example of an enum is used to illustrate this. If an enum supports two different data types (String and i64), a mutable reference of the string variant of the enum is made, and then another reference is made, the mutable reference can change the data, and then the second reference would still reference the string variant of the enum. The second reference would then try to dereference the string variant of the enum, potentially causing a segmentation fault. Elaboration on this example and others is provided in the Further reading section.
  6. We would need to define two different lifetimes when the result of a function relies on one of the lifetimes and the result of the function is needed outside of the scope of where it is called.
  7. If a struct references itself in one of its fields, the size could be infinite, as it could continue to reference itself continuously. To prevent this, we can wrap the reference to the struct in the field in a Box struct.
  8. We can define a trait that has multiple functions and this trait with these functions can be applied to multiple different structs.

Further reading

  1. Hands-On Functional Programming in Rust (2018) Andrew Johnson: Chapter 1 ,Generics and Structs
  2. Mastering Rust (2019) Rahul Sharma and Vesa Kaihlavirta: Chapter 1, A Tour of the Language
  3. The Problem With Single-threaded Shared Mutability (2015) by Manish Goregaokar: https://manishearth.github.io/blog/2015/05/17/the-problem-with-shared-mutability/
  4. Rust Project Developers, 2024. The Rust Programming Language. [online] Available at: https://doc.rust-lang.org/book/ [Accessed 21 June 2024]
  5. Latimer, N., 2020. Programming Rust. [online] Available at: https://doc.rust-lang.org/stable/rust-by-example/ [Accessed 21 June 2024].

Get this book’s PDF copy, code bundle, and more

Scan the QR code (or go to packtpub.com/unlock). Search for this book by name, confirm the edition, and then follow the steps on the page.

A qr code on a white background

AI-generated content may be incorrect.

A white text on a black background

AI-generated content may be incorrect.

Note: Have your invoice handy. Purchases made directly from the Packt website don’t require an invoice.

Left arrow icon Right arrow icon
Download code icon Download Code

Key benefits

  • Get a comprehensive introduction to Rust for full-stack web development
  • Explore the exciting evolution of Rust in recent years with WebAssembly, Axum, native TLS, and SurrealDB
  • Build code in a scalable way with microservice and nanoservice design patterns

Description

Rust is no longer just for systems programming. This book will show you why this safe and performant language is a crucial up-and-coming option for developing web applications, and get you on your way to building fully functional Rust web apps. You don’t need any experience with Rust to get started, and this new edition also comes with a shallower learning curve. You’ll get hands-on with emerging Rust web frameworks including Actix, Axum, Rocket, and Hyper. You’ll look at injecting Rust into the frontend with WebAssembly and HTTPS configuration with NGINX. Later, you’ll move on to more advanced async topics, exploring TCP and framing, and implementing async systems. As you work through the book, you’ll build a to-do application with authentication using a microservice architecture that compiles into one Rust binary, including the embedding of a frontend JavaScript application in the same binary. The application will have end-to-end atomic testing and a deployment pipeline. By the end of this book, you’ll fully understand the significance of Rust for web development. You’ll also have the confidence to build robust, functional, and scalable Rust web applications from scratch.

Who is this book for?

This book is for web developers who are looking to learn or adopt Rust to build safe and performant web applications. This includes developers familiar with languages such as Python, Ruby, and JavaScript. You don’t need any prior experience in Rust to start this book. However, you’ll need a solid understanding of web development principles, along with basic knowledge of HTML, CSS, and JavaScript to get the most out of it.

What you will learn

  • Build scalable Rust web applications as monoliths or microservices
  • Develop a deeper understanding of async Rust
  • Get to grips with Rust language features such as traits and the borrow checker
  • Manage authentication and databases in Rust web apps
  • Build app infrastructure on AWS using Terraform
  • Learn how to package and deploy Rust servers
  • Build unit tests and end-to-end tests for your Rust web apps with Python

Product Details

Country selected
Publication date, Length, Edition, Language, ISBN-13
Publication date : Jan 30, 2026
Length: 674 pages
Edition : 3rd
Language : English
ISBN-13 : 9781835887776
Languages :

What do you get with eBook?

Product feature icon Instant access to your Digital eBook purchase
Product feature icon Download this book in EPUB and PDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
Product feature icon AI Assistant (beta) to help accelerate your learning
Modal Close icon
Payment Processing...
tick Completed

Billing Address

Product Details

Publication date : Jan 30, 2026
Length: 674 pages
Edition : 3rd
Language : English
ISBN-13 : 9781835887776
Languages :

Packt Subscriptions

See our plans and pricing
Modal Close icon
€18.99 billed monthly
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Simple pricing, no contract
€189.99 billed annually
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Choose a DRM-free eBook or Video every month to keep
Feature tick icon PLUS own as many other DRM-free eBooks or Videos as you like for just €5 each
Feature tick icon Exclusive print discounts
€264.99 billed in 18 months
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Choose a DRM-free eBook or Video every month to keep
Feature tick icon PLUS own as many other DRM-free eBooks or Videos as you like for just €5 each
Feature tick icon Exclusive print discounts

Table of Contents

23 Chapters
A Quick Introduction to Rust Chevron down icon Chevron up icon
Useful Rust Patterns for Web Programming Chevron down icon Chevron up icon
Designing Your Web Application in Rust Chevron down icon Chevron up icon
Async Rust Chevron down icon Chevron up icon
Handling HTTP Requests Chevron down icon Chevron up icon
Processing HTTP Requests Chevron down icon Chevron up icon
Displaying Content in the Browser Chevron down icon Chevron up icon
Injecting Rust in the Frontend with WebAssembly Chevron down icon Chevron up icon
Data Persistence with PostgreSQL Chevron down icon Chevron up icon
Managing User Sessions Chevron down icon Chevron up icon
Communicating Between Servers Chevron down icon Chevron up icon
Caching Auth Sessions Chevron down icon Chevron up icon
Observability Through Logging Chevron down icon Chevron up icon
Unit Testing Chevron down icon Chevron up icon
End-to-End Testing Chevron down icon Chevron up icon
Deploying Our Application on AWS Chevron down icon Chevron up icon
Configuring HTTPS with NGINX on AWS Chevron down icon Chevron up icon
Practicalities of Using Microservices and Nanoservices Chevron down icon Chevron up icon
Low-Level Networking Chevron down icon Chevron up icon
Distributed Computing Chevron down icon Chevron up icon
Unlock Your Exclusive Benefits Chevron down icon Chevron up icon
Other Books You May Enjoy Chevron down icon Chevron up icon
Index Chevron down icon Chevron up icon

Customer reviews

Rating distribution
Full star icon Full star icon Full star icon Full star icon Half star icon 4.3
(3 Ratings)
5 star 33.3%
4 star 66.7%
3 star 0%
2 star 0%
1 star 0%
Ricardo Lima Mar 05, 2026
Full star icon Full star icon Full star icon Full star icon Full star icon 5
Feefo Verified review Feefo
Daniel Sep 24, 2024
Full star icon Full star icon Full star icon Full star icon Empty star icon 4
The graph in chapter 1 "what is rust) that display the bench mark of different frameworks has some numbers format that need to be reviewed. There should be an easy way to provide feedback on the book for early access titles (easy to spot erratas for you guys)
Subscriber review Packt
N/A Mar 26, 2026
Full star icon Full star icon Full star icon Full star icon Empty star icon 4
It's a good starting point to learn RUST.
Feefo Verified review Feefo
Get free access to Packt library with over 7500+ books and video courses for 7 days!
Start Free Trial

FAQs

How do I buy and download an eBook? Chevron down icon Chevron up icon

Where there is an eBook version of a title available, you can buy it from the book details for that title. Add either the standalone eBook or the eBook and print book bundle to your shopping cart. Your eBook will show in your cart as a product on its own. After completing checkout and payment in the normal way, you will receive your receipt on the screen containing a link to a personalised PDF download file. This link will remain active for 30 days. You can download backup copies of the file by logging in to your account at any time.

If you already have Adobe reader installed, then clicking on the link will download and open the PDF file directly. If you don't, then save the PDF file on your machine and download the Reader to view it.

Please Note: Packt eBooks are non-returnable and non-refundable.

Packt eBook and Licensing When you buy an eBook from Packt Publishing, completing your purchase means you accept the terms of our licence agreement. Please read the full text of the agreement. In it we have tried to balance the need for the ebook to be usable for you the reader with our needs to protect the rights of us as Publishers and of our authors. In summary, the agreement says:

  • You may make copies of your eBook for your own use onto any machine
  • You may not pass copies of the eBook on to anyone else
How can I make a purchase on your website? Chevron down icon Chevron up icon

If you want to purchase a video course, eBook or Bundle (Print+eBook) please follow below steps:

  1. Register on our website using your email address and the password.
  2. Search for the title by name or ISBN using the search option.
  3. Select the title you want to purchase.
  4. Choose the format you wish to purchase the title in; if you order the Print Book, you get a free eBook copy of the same title. 
  5. Proceed with the checkout process (payment to be made using Credit Card, Debit Cart, or PayPal)
Where can I access support around an eBook? Chevron down icon Chevron up icon
  • If you experience a problem with using or installing Adobe Reader, the contact Adobe directly.
  • To view the errata for the book, see www.packtpub.com/support and view the pages for the title you have.
  • To view your account details or to download a new copy of the book go to www.packtpub.com/account
  • To contact us directly if a problem is not resolved, use www.packtpub.com/contact-us
What eBook formats do Packt support? Chevron down icon Chevron up icon

Our eBooks are currently available in a variety of formats such as PDF and ePubs. In the future, this may well change with trends and development in technology, but please note that our PDFs are not Adobe eBook Reader format, which has greater restrictions on security.

You will need to use Adobe Reader v9 or later in order to read Packt's PDF eBooks.

What are the benefits of eBooks? Chevron down icon Chevron up icon
  • You can get the information you need immediately
  • You can easily take them with you on a laptop
  • You can download them an unlimited number of times
  • You can print them out
  • They are copy-paste enabled
  • They are searchable
  • There is no password protection
  • They are lower price than print
  • They save resources and space
What is an eBook? Chevron down icon Chevron up icon

Packt eBooks are a complete electronic version of the print edition, available in PDF and ePub formats. Every piece of content down to the page numbering is the same. Because we save the costs of printing and shipping the book to you, we are able to offer eBooks at a lower cost than print editions.

When you have purchased an eBook, simply login to your account and click on the link in Your Download Area. We recommend you saving the file to your hard drive before opening it.

For optimal viewing of our eBooks, we recommend you download and install the free Adobe Reader version 9.

Modal Close icon
Modal Close icon