Let's explore structs in Go. If you know Go's basic types like string, int, and bool, structs let you handle more complex data.
Say you're building a store app. You need a product's name, price, and in-stock status. Using separate variables quickly becomes messy.
Structs can simplify this. Picture structures are like a blueprint. It makes it possible to package all the related information in one neat and organized bundle.
Now let's dive into creating and using custom data types in Go.
Declaring Structs
Before using a struct, we first need to describe what it looks like. The result of defining a struct is not a piece of data, but the blueprint of data we want to store later on.
Let's take a look at the declaration of the Product struct:
package main
import "fmt"
// We use the 'type' keyword to define a new custom type.
// We name our type 'Product' and specify that it is a 'struct'.
type Product struct {
Name string // Field for the product's name
Price float64 // Field for the price
InStock bool // Field to check if it's available
}
Creating Struct Instances
Using the struct we just created, let's create a product. Using the struct blueprint to create a piece of data is referred to as creating an instance.
package main
import "fmt"
type Product struct {
Name string
Price float64
InStock bool
}
func main() {
// Creating an instance of Product using a "struct literal."
item1 := Product{
Name: "Wireless Mouse",
Price: 25.99,
InStock: true,
}
// Creating an instance but leaving fields blank
item2 := Product{
Name: "Mousepad",
}
fmt.Println(item1)
fmt.Println(item2)
}
Expected Output:
{Wireless Mouse 25.99 true}
{Mousepad 0 false}
You might be thinking Why does item2 show 0 and false when we didn’t set them?
This is one of Go’s most important behaviours: zero values. In Go, when you declare a variable but don’t give it a value, Go doesn’t leave it empty or undefined (which causes bugs in many other languages). Instead, Go automatically fills it with the zero value for that type.
Since we gave item2 a name, Go automatically sets the price to 0 and in stock to false . This is one of the features in Go that make it exceptional.
Accessing and Modifying Fields
When you want to modify or read information from a struct, you use a dot notation. Consider the dot to be opening up a particular drawer of your filing cabinet to view a folder.
package main
import "fmt"
type Product struct {
Name string
Price float64
InStock bool
}
func main() {
item := Product{
Name: "Keyboard",
Price: 45.00,
InStock: true,
}
// Reading a field
fmt.Println("Original Price:", item.Price)
// Modifying a field
item.Price = 39.99
fmt.Println("Discounted Price:", item.Price)
}
Expected Output:
Original Price: 45
Discounted Price: 39.99
Anonymous Structs
Sometimes we want to bundle up some information for one-time use. In such a case, we don’t want to have to go through the trouble of formally defining a whole blueprint. They are particularly useful when holding configuration settings, grouping test inputs, or temporarily organising data for a single function. We use anonymous structs in this way:
package main
import "fmt"
func main() {
// Declaring and initializing a struct at the exact same time
config := struct {
Port string
Env string
}{
Port: "8080",
Env: "Production",
}
fmt.Println("Running on port:", config.Port)
}
Expected Output:
Running on port: 8080
This is a little confusing, if you are new to Go, because you create the blueprint, then use it to create an instance. Let's look at the code snippet again.
config := struct { <- this is the blueprint opening brace
Port string
Env string
}{ <- this is the closing brace for the blueprint, followed by an opening brace.
Port: "8080",
Env: "Production",
} <- this is the closing
Think of this like defining a shape, then filling it. Keep in mind that this is a non-reusable type.
Nested (Embedded) Structs
Unlike other programming languages, Go has no “classes” or “inheritance”. Instead, Go employs a technique of packing smaller structs into larger structs, similar to Russian nesting dolls.
package main
import "fmt"
// The smaller struct
type Address struct {
City string
Country string
}
// The larger struct
type User struct {
Name string
Contact Address // Embedding the Address struct here
}
func main() {
player := User{
Name: "Alice",
Contact: Address{
City: "Nairobi",
Country: "Kenya",
},
}
// Accessing the nested data
fmt.Println(player.Name, "lives in", player.Contact.City)
}
Expected Output:
Alice lives in Nairobi.
Struct Comparison
A question might pop into your brain asking, “Are you able to compare two structs to determine whether they are equal or not?” Yes! Go supports the normal == operator, but only if all the fields within the struct are comparable.
package main
import "fmt"
type Product struct {
Name string
Price float64
}
func main() {
item1 := Product{Name: "Mug", Price: 12.50}
item2 := Product{Name: "Mug", Price: 12.50}
item3 := Product{Name: "Mug", Price: 15.00}
fmt.Println("Are item1 and item2 identical?", item1 == item2)
fmt.Println("Are item1 and item3 identical?", item1 == item3)
}
Expected Output:
Are item1 and item2 identical? true
Are item1 and item3 identical? false
Go looks at every single field. If everything matches perfectly, the structs are considered equal.
The Photocopy vs. The Original
This is the most essential concept for beginners in Go. Go’s default behavior is to send a struct by value into a function.
If you were to give a friend a copy of your notes, how would they look? If they add a mustache to the photocopied version, you are fine! This is the default behavior of Go. To change an original struct, the function has to be passed a pointer to it (that is, the original notebook).
Let’s observe both of them together:
package main
import "fmt"
type Product struct {
Name string
Price float64
}
// Pass by Value (The Photocopy)
func failedDiscount(p Product) {
p.Price = p.Price - 5.00
}
// Pass by Pointer (The Original)
func successfulDiscount(p *Product) {
p.Price = p.Price - 5.00
}
func main() {
item := Product{Name: "Book", Price: 20.00}
failedDiscount(item) // Trying to discount the photocopy
fmt.Println("After failed discount:", item.Price)
successfulDiscount(&item) // Giving the function the original via '&'
fmt.Println("After successful discount:", item.Price)
}
Expected Output:
After failed discount: 20
After successful discount: 15
Best Practices & Common Beginner Mistakes
- In Go, names of fields that begin with a capital letter are “exported” and available to other packages (as opposed to names that start with a lowercase letter, which are “private”). If it begins with a lower-case letter (e.g., name), it belongs only to the package it is in. If in any doubt, make them capitalized so that you can use them freely.
- Pointers for Modification: Keep in mind the photocopy rule. If your function will call to change or update a struct, then you must pass a pointer (*).
Summary & Your Mini-Challenge
You’ve made it! You’ve learned to structure unsorted variables into orderly, sensible structures. You know how to construct blueprints, make instances, navigate nested data, and update nested data correctly with pointers.
Top comments (0)