Understand Go pointers: address-of operator, dereferencing, nil pointers, pointer to structs, and when to use pointers vs values.
Time
25 minutes
Prerequisites
Lab 04 (Structs & Methods)
Tools
Docker image: zchencow/innozverse-go:latest
Lab Instructions
Step 1: Pointer Basics
dockerrun--rmzchencow/innozverse-go:latestgorun-<<'EOF'package mainimport "fmt"func main() { x := 42 p := &x // p is a pointer to x (*int) fmt.Printf("x = %d\n", x) fmt.Printf("&x = %p (address of x)\n", &x) fmt.Printf("p = %p (value of p = address)\n", p) fmt.Printf("*p = %d (dereference p = value at address)\n", *p) *p = 99 // modify x via pointer fmt.Printf("After *p=99, x = %d\n", x) // new() — allocates zeroed memory, returns pointer q := new(int) fmt.Printf("new(int): %p → %d\n", q, *q) *q = 100 fmt.Printf("After *q=100: %d\n", *q) // Nil pointer — zero value of any pointer type var ptr *int fmt.Printf("nil pointer: %v, isNil: %v\n", ptr, ptr == nil) // *ptr would panic! Always check for nil // Pointer to pointer pp := &p fmt.Printf("**pp = %d\n", **pp)}EOF
💡 &x gives the address, *p dereferences it. In Go, you never do pointer arithmetic (no ptr + 1 like C). The garbage collector tracks all pointers and moves/frees memory automatically. Dereferencing a nil pointer causes a panic — always check ptr != nil before dereferencing.
📸 Verified Output:
Step 2: Pointers to Structs
💡 Go automatically dereferences struct pointers — p.Field and (*p).Field are identical. This is why &Config{} is so natural. When you use a pointer receiver method func (c *Config) Set(...), calling myConfig.Set(...) works even if myConfig is not a pointer — Go takes its address automatically.
x = 42
&x = 0xc0000b4008 (address of x)
p = 0xc0000b4008 (value of p = address)
*p = 42 (dereference p = value at address)
After *p=99, x = 99
new(int): 0xc0000b4010 → 0
After *q=100: 100
nil pointer: <nil>, isNil: true
**pp = 99
docker run --rm zchencow/innozverse-go:latest go run - << 'EOF'
package main
import "fmt"
type Node struct {
Value int
Next *Node // pointer to same type — enables linked list
}
type LinkedList struct {
Head *Node
Size int
}
func (l *LinkedList) Push(val int) {
l.Head = &Node{Value: val, Next: l.Head}
l.Size++
}
func (l *LinkedList) Pop() (int, bool) {
if l.Head == nil { return 0, false }
val := l.Head.Value
l.Head = l.Head.Next
l.Size--
return val, true
}
func (l *LinkedList) String() string {
result := ""
for cur := l.Head; cur != nil; cur = cur.Next {
if result != "" { result += " → " }
result += fmt.Sprintf("%d", cur.Value)
}
return "[" + result + "]"
}
// Demonstrate value vs pointer semantics
type Config struct {
Debug bool
Timeout int
Host string
}
// Value: receives a COPY — original unchanged
func applyDefaultsByValue(c Config) Config {
if c.Timeout == 0 { c.Timeout = 30 }
if c.Host == "" { c.Host = "localhost" }
return c
}
// Pointer: modifies the ORIGINAL
func applyDefaultsByPointer(c *Config) {
if c.Timeout == 0 { c.Timeout = 30 }
if c.Host == "" { c.Host = "localhost" }
}
func main() {
list := &LinkedList{}
for _, v := range []int{1, 2, 3, 4, 5} { list.Push(v) }
fmt.Println("List:", list)
fmt.Println("Size:", list.Size)
for i := 0; i < 3; i++ {
val, ok := list.Pop()
fmt.Printf("Pop: %d (ok=%v)\n", val, ok)
}
fmt.Println("After pops:", list)
// Value vs pointer
original := Config{Debug: true}
updated := applyDefaultsByValue(original)
fmt.Printf("Original: %+v\n", original) // unchanged
fmt.Printf("Updated: %+v\n", updated)
applyDefaultsByPointer(&original)
fmt.Printf("After pointer call: %+v\n", original) // modified!
// Struct pointer syntax sugar — (*p).Field == p.Field
p := &Config{Debug: true}
(*p).Timeout = 60 // explicit dereference
p.Host = "example.com" // Go auto-dereferences (syntactic sugar)
fmt.Printf("Config: %+v\n", *p)
}
EOF