Real programs deal with structured data. A customer isn’t just a name — it’s a name, an email, a phone number, an address, a list of orders. Go gives you structs to model this. Once you understand structs, you’ll see them in nearly every real Go program.
We’ll also cover pointers in this lesson, because the two work together. A pointer is just a variable that holds the address of another variable. They sound scarier than they are.
Pointers
Every variable in your program lives at some address in memory. A pointer is a variable whose value is “the address where another variable lives.”
You’ll mostly use pointers for two reasons:
- To let a function modify a variable from the outside
- To avoid copying large values when you pass them around
The basics
Two operators do the work: & (address-of) and * (dereference).
package main
import "fmt"
func main() {
x := 42
p := &x // p holds the address of x
fmt.Println("Value of x: ", x)
fmt.Println("Address of x (p): ", p)
fmt.Println("Value at address p:", *p)
}
Value of x: 42
Address of x (p): 0xc000014098
Value at address p: 42
Reading the code:
&x— the address wherexis storedp— a variable holding that address. Its type is*int(read: “pointer to int”)*p— go to that address and read the value there. This is called dereferencing.
Why pointers matter — modifying through a pointer
Inside a function, parameters are normally copies. Changes you make inside the function don’t affect the caller’s variable:
package main
import "fmt"
func incrementCopy(n int) {
n++
}
func incrementReal(n *int) {
*n++
}
func main() {
x := 10
incrementCopy(x)
fmt.Println("After incrementCopy:", x)
incrementReal(&x)
fmt.Println("After incrementReal:", x)
}
After incrementCopy: 10
After incrementReal: 11
incrementCopy got a copy of x. It modified the copy, threw the copy away, and left x untouched. incrementReal got the address of x, dereferenced it, and modified the real thing.
This is one of the few cases where you’ll touch pointers directly. Most of the time, you’ll use them through structs.
Structs
A struct groups multiple named values into one compound type. Each named value is called a field.
package main
import "fmt"
type User struct {
Name string
Email string
Age int
}
func main() {
u := User{
Name: "Manikandan",
Email: "[email protected]",
Age: 30,
}
fmt.Println(u)
fmt.Println("Name:", u.Name)
fmt.Println("Age: ", u.Age)
}
{Manikandan [email protected] 30}
Name: Manikandan
Age: 30
What’s happening:
type User struct { ... }declares a new struct type calledUserwith three fields.User{ Name: "...", Email: "...", Age: 30 }creates an instance of that struct.u.Nameuses the dot operator to read a field.
You can also create a struct without naming the fields, by giving values in declaration order:
// inside main()
u := User{"Manikandan", "[email protected]", 30}
fmt.Println(u)
Naming the fields is preferred. It’s clearer, and it doesn’t break if someone reorders the struct fields later.
Modifying struct fields
// inside main()
u := User{Name: "Mani", Age: 30}
u.Age = 31
fmt.Println(u)
{Mani 31}
(Email is empty because we didn’t set it — it got the zero value "".)
Zero values for structs
If you declare a struct without initializing it, every field gets its zero value:
// inside main()
var u User
fmt.Println(u)
{ 0}
Empty string for Name and Email, 0 for Age. No “undefined” or “null” — every field has a real value.
Pointers to structs
Here’s where the two concepts come together. Most real Go code uses pointers to structs rather than struct values.
package main
import "fmt"
type User struct {
Name string
Age int
}
func haveBirthday(u *User) {
u.Age++
}
func main() {
u := &User{Name: "Mani", Age: 30}
haveBirthday(u)
haveBirthday(u)
haveBirthday(u)
fmt.Println(u.Name, "is now", u.Age)
}
Mani is now 33
Two things to notice:
u := &User{...}—&in front of a struct literal creates the struct AND returns a pointer to it.uis type*User.u.Age++insidehaveBirthday— Go is friendly here. Even thoughuis a pointer, you can use the dot operator directly. Go automatically dereferences. (You could write(*u).Age++, but no one does.)
When to use struct values vs pointers
A common question. Here are simple rules:
- Use a pointer (
*User) when:- The function should modify the struct
- The struct is large (copying it would be wasteful)
- You want to share state — multiple parts of the program holding the same
*Usersee the same data
- Use a value (
User) when:- The struct is small and you want a copy
- You want to ensure the function can’t modify the original
Real Go code uses pointers most of the time, especially for “objects” that have identity (a user, a connection, a config).
Nested structs
Struct fields can themselves be structs. This is how you model real-world data:
package main
import "fmt"
type Address struct {
Street string
City string
}
type User struct {
Name string
Address Address
}
func main() {
u := User{
Name: "Mani",
Address: Address{
Street: "12 Anna Salai",
City: "Chennai",
},
}
fmt.Println(u.Name, "lives in", u.Address.City)
}
Mani lives in Chennai
Nesting can go as deep as you need. u.Address.City reaches through two levels.
A practical example
Tracking inventory with a slice of structs:
package main
import "fmt"
type Product struct {
Name string
Price float64
Stock int
}
func main() {
inventory := []Product{
{Name: "Notebook", Price: 5.99, Stock: 100},
{Name: "Pen", Price: 1.25, Stock: 500},
{Name: "Eraser", Price: 0.75, Stock: 200},
}
totalValue := 0.0
for _, p := range inventory {
totalValue += p.Price * float64(p.Stock)
}
fmt.Printf("Total inventory value: $%.2f\n", totalValue)
}
Total inventory value: $1374.00
fmt.Printf is like Println but with format specifiers. %.2f means “a floating-point number with 2 decimal places.” \n is a newline.
What’s next
You can model data: lists, lookups, and structured records. The next step is doing things with that data — writing reusable code with functions, attaching behavior to types with methods, and writing flexible code with interfaces. That’s the next section.