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 nameOther
.
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.