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:

  1. To let a function modify a variable from the outside
  2. 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 where x is stored
  • p — 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:

  1. type User struct { ... } declares a new struct type called User with three fields.
  2. User{ Name: "...", Email: "...", Age: 30 } creates an instance of that struct.
  3. u.Name uses 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:

  1. u := &User{...}& in front of a struct literal creates the struct AND returns a pointer to it. u is type *User.
  2. u.Age++ inside haveBirthday — Go is friendly here. Even though u is 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 *User see 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.

Toggle theme (T)