Go Beginners Series: Data Structures, Structs, and Pointers

Go Beginners Series: Data Structures, Structs, and Pointers

When writing medium-large applications in Go, you need to keep things like code organization, memory management, access to data, and code flexibility in mind. To say the least.

For example, you can only do so much with the regular data types. In fact, for most applications, you'll need to define and store multiple values in a single unit for better code organization, readability, and composability. Also, using pointers in your Go applications has many benefits, like efficient memory management, direct data manipulation, creating dynamic data structures, and many more.

In this article, you will learn everything you need to know to start using pointers and struct to build more efficient and reliable Go applications.

Data Structures

In programming, a data structure organizes and stores data in a computer's memory. A data structure can be considered a container that holds a collection of related data items and defines their relationships. Data structures are essential in programming because they allow you to store and access data and perform operations on it efficiently.

There are many types of data structures, each with its strengths and weaknesses, and each suited to particular tasks or use cases.

Common Data Structures

Arrays: A simple data structure that stores a collection of items of the same type in contiguous memory locations. Arrays can be accessed by index and help implement sequential data access algorithms.

Linked Lists: A dynamic data structure that consists of a sequence of nodes, each containing a data element and a pointer to the next node in the series. Linked lists help implement algorithms that require frequent insertion and deletion operations.

Stacks: A data structure that stores elements in a Last-In-First-Out (LIFO) order. Stacks are useful for implementing algorithms that require backtracking or undo operations.

Queues: A data structure that stores elements in a First-In-First-Out (FIFO) order. Lines help implement algorithms that require scheduling or message passing.

Trees: A hierarchical data structure that consists of nodes connected by edges. Trees help represent hierarchical relationships between data elements, such as a file system's structure or a company's organization.

Graphs: A data structure representing objects (vertices or nodes) connected by edges. Charts help model relationships between data elements, such as social and transportation networks.

The choice of data structure depends on the problem being solved and the program's performance requirements. Efficient use of data structures is critical for writing high-performance programs, and understanding the trade-offs between different data structures is an essential skill for any programmer.

Let's take a look at structs and how to use them in go in the next section.

Structs

Go enables you to define related data in a single unit using structs. A struct is a data type that holds a group of fields; for example, if you want to define user information in a single place, you can do that like so:

type User struct {
 name      string
 email     string
 accessLvl string
 age       int
}

The code above defines a User struct with name, email, accessLvl, and age fields. After defining the struct, you can now use the value like so:

 user := User{"John Kenneth", "jken@ml.com", "admin", 40}
 fmt.Printf("Your username is %s, your email is %s, and your access level is %s and your age is %d", user.name, user.email, user.accessLvl, user.age)

The code above defines a user variable which is a type of User with a value for all the fields, and prints the values out with a message like this:

Your username is John Kenneth, your email is jken@ml.com, and your access level is admin, and your age is 40

When defining a struct variable, you do not have to define a value for all the fields. You can work with the User struct like this:

 user := User{
  name:  "John Kenneth",
  email: "jkem@ml.com",
  age:   40,
 }
 user.accessLvl = "admin"

The code above defined parts of the user variables upon declaration and other parts after. The code above will return the same result as before.

Embedded Fields

A struct can be a field inside another struct to reduce code repetition. For example, you can use the User struct inside the Employee struct to avoid repeating the same fields inside it like so:

type Employee struct {
 User
 salary int
 role   string
}

You can now define a variable based on the Employee struct like so:


 employee := Employee{
  User: User{
   name:  "John Kenneth",
   email: "jken@ml.com",
   age:   40,
  },
  salary: 50000,
  role:   "Developer",
 }
 fmt.Printf("Your employee name is %s, your email is %s, and your access level is %s, and your age is %d. While your role is %s and your salary is %d", employee.name, employee.email, employee.accessLvl, employee.age, employee.role, employee.salary)

The code above will return a message based on the employee variable like so:

Your employee name is John Kenneth, your email is jken@ml.com, and your access level is admin, and your age is 40. While your role is Developer and your salary is 50000

Note: Field names must be unique. However, if you need to embed the same struct twice, you'll need to prefix it with a name like this: Other User, and it will be accessed with the name Other.

Anonymous Structs

Anonymous structs in Go are used when you need to define a single instance of the struct. You can define anonymous structs like this:

 car := struct {
  brand string
  model string
  year  int
  price int
 }{
  brand: "Toyota",
  model: "Vios",
  year:  2020,
  price: 1000000,
 }

 fmt.Printf("The brand of the car is %s, the model is %s, the year is %d, and the price is %d", car.brand, car.model, car.year, car.price)

The code above defines an anonymous struct car and prints a message based on its values. The code above will return:

The brand of the car is Toyota, the model is Vios, the year is 2020, and the price is 1000000

Receiver Methods

Go allows you to define functions that only work on instances of structs. For example, if you want to create a function that will describe an employee, you can use a receiver function like so:

func (u *Employee) describe() string {
 desc := fmt.Sprintf("Name: %s, Email: %s, Age: %d, Salary: %d, Role: %s", u.name, u.email, u.age, u.salary, u.role)
 return desc
}

The code above defined a function, describe, that only works on instances of the Employee struct and prints out a message with the employee information. You can now use the function on a variable like this:

fmt.Println(employee.describe())

The code above will return:

Name: John Kenneth, Email: jken@ml.com, Age: 40, Salary: 5000, Role: CEO

Note: The braces that come before the function name contain the receiver, a pointer of the type the function operates on. More on pointers later in the article.

Benefits of Structs in Go

In Go, structs are a powerful composite data type that provides several benefits over other data types like arrays or maps. Some of the key benefits of using structs in Go include the following:

Encapsulation: Structs allow you to encapsulate related data together in a single object, which can help to improve code organization and maintainability. This makes it easier to reason about the data being used in your program and reduces the risk of errors caused by accidentally modifying the wrong data.

Struct methods: Structs in Go can have associated methods, allowing you to define behavior specific to a particular struct type. This can help simplify your code and reduce the boilerplate code you need to write.

Type safety: Go is a statically-typed language, meaning that the variable type is known at compile time. Using structs can help to ensure that the types of your data are consistent and can help to catch errors at compile time.

Efficiency: Structs in Go are designed to be lightweight and efficient, which means they can represent complex data structures without incurring a significant performance penalty.

Flexibility: Structs in Go can be nested within other structs, allowing you to create complex data structures that can be easily manipulated and processed.

Using structs in Go that help to improve code organization, reduce errors, and improve performance as well as make programs more efficient, maintainable, and flexible.

Let's explore pointers and how to use them in the next section.

Pointers

Go allows you to sacrifice immutability for efficiency in your code by using pointers. A pointer in Go is the memory address of a given variable; for example, when you create a variable in Go, a unique memory address that looks like 0xc00000c0c0 is assigned to the variable automatically and can be used to do different things in your code.

An excellent use case for pointers in Go is to modify the value of a variable inside and outside the function scope to avoid creating too many copies of the variable all over the application. For example, when you create a function that takes in and modifies an already defined variable, the variable will not be modified; instead, Go will create a reference of that variable just for the function call. Let's see an example of that with a function:

var age = 40

func updateAge(age int) {
 age = 100
 fmt.Println(age)
}
func main() {
 fmt.Println("Here is the value of age before calling updateAge: ", age)
 updateAge(age)
 fmt.Println("Here is the value of age after calling updateAge: ", age)
}

The code above defined a variable, age, a function, updateAge that takes in a number, assigns it the value of 100, and prints it out. Finally, it prints a message with the value of age before and after calling the updateAge function with the age variable. The code above should return the following result:

Here is the value of age before calling updateAge:  40
100
Here is the value of age after calling updateAge:  40

The updateAge function worked as expected, but it couldn’t change the value of age because of Go's pass-by-value principle. So how can you change the actual value of the age variable? First, let's see how to print out a variable's memory address.

Memory Addresses

As mentioned earlier, every variable in Go has a memory address assigned to it upon creation. You can access the address using the following syntax:

fmt.Println("age pointer is: ", &age) // age pointer is:  0x6bd320

The code above will print out a message with the age variable's memory address like so:

age pointer is:  0x6bd320

There's little use for the memory address itself, but you can do whatever you want with it.

Dereferencing

You can solve the problem you saw earlier with the updateAge function with dereferencing. This is a way to pass the value of a variable to a function so it can modify the variable directly. Edit the following parts of the previous code block like this:

func updateAge(age *int) {
 *age = 100
 fmt.Println(*age)
}

// function call
 updateAge(&age)

The code above rewrites the updateAge function to accept a pointer as a parameter instead of a variable. It then uses dereferencing by prefixing the parameter name with * and calls the updateAge function with a pointer by prefixing the age variable with &. The code above should now return:

Here is the age before calling updateAge:  40
100
Here is the age after calling updateAge:  100

To better understand the memory address and dereferencing syntax, consider the following code:

 fmt.Println("age pointer is: ", &age)
 fmt.Println("age value is: ", age)
 fmt.Println("You can also access the value using with: ", (*&age))

The code above will return the following:

age pointer is:  0x8ae320
age value is:  40
You can also access the value using with:  40

Benefits of Using Pointers in Go

Using pointers in your Go code allows you to manipulate and modify data more efficiently and has many benefits. Let's explore some of them in this section.

Modifying Variables

When you want to modify the value of a variable inside a function - If you pass a variable to a function by value, any modifications made to the variable inside the function will not affect the original variable. By passing a pointer to the variable instead, the function can modify the original value directly.

Avoid Unnecessary Copying

When you want to avoid copying large data structures - In Go, passing large data structures by value can be inefficient, as it requires creating a new copy of the entire structure. By passing a pointer to the structure instead, you can avoid this unnecessary copying and improve performance.

Sharing Data Between Functions

When you want to share data between functions - In some cases, you may want to share data between multiple functions or packages. Passing a pointer to the data allows multiple functions to access and modify the same data.

Data Structures Implementation

When you want to implement data structures like linked lists or trees - Data structures like linked lists and trees require storing pointers to other nodes in the structure. Using pointers, you can easily create and manipulate these complex data structures.

It's important to use pointers carefully and ensure you handle memory correctly to avoid bugs and memory leaks. However, pointers can be a powerful tool for efficient and flexible programming in Go when used correctly.

Conclusion

Structs and pointers are significant features of Go and can be pretty complex to understand. However, I hope this article can teach you what you need to know to get started with structs and pointers in Go. You learned about pointers, dereferencing, structs, embedded structs, anonymous structs, and the benefits of using them in your Go applications.

Please leave a comment if you have any questions, suggestions, or corrections; I'll make sure I reply to all of them.