Lesson 10/41 · Composite Data Types & Object-Oriented Design Basics m02-l01

Pointers and Memory: &, * and an Intro to Escape Analysis

Module 2: Composite Data Types & Object-Oriented Design | Lesson: m02-l01 | Level: Beginner After this lesson you will be able to: understand pointer semantics and the memory model, master the difference between new and make, and use escape analysis to optimize code performance.

Learning Objectives

  • Use & and * correctly to take addresses and dereference.
  • Accurately distinguish when to use new versus make, and the differences in their return values.
  • Explain the behavioral difference in memory between value passing and pointer passing.
  • Use -gcflags=-m for escape analysis and understand the logic of stack versus heap allocation.

Core Concepts

2.1 Pointer Semantics: & and *

A pointer is the memory address of a variable. In Go, a pointer type is written *T. - Address operator &: gets a variable's memory address. For example, &x returns the address of x, of type *T. - Dereference operator *: accesses or modifies the value through an address. For example, *p accesses the value that p points to. - Nil pointer: a zero-value pointer that points to no valid memory. Dereferencing a nil pointer causes a panic. - Pointer size: occupies one machine word — 8 bytes on a 64-bit platform, 4 bytes on a 32-bit platform.

2.2 The Difference Between new and make

Go has only two built-in functions for allocating memory, but their purposes are very different: - new(T): allocates zeroed memory and returns a pointer of type *T. It does not initialize the internal structure; it only zeroes it. - make(T, args): used only for the three built-in reference types slice, map and chan. It allocates and initializes the internal data structures, and returns the type T itself (not a pointer).

Important fact: it is not that new cannot be used for slices/maps, but that it should not be: it only allocates a zero value (giving a *[]int pointing at a nil slice, or a *map[K]V pointing at a nil map) which you must dereference before use. To create a usable reference type, use make.

2.3 Value Passing vs Pointer Passing

Go function arguments are passed by value by default. - Value passing: copies the variable's value. For int, struct and the like, modifying the copy does not affect the original. - Pointer passing: copies the address. Modifying what *p points to affects the original variable. - Slices are value types: the slice header (pointer, length, capacity) is passed by value, but the underlying array is shared. Therefore modifying slice elements affects the original slice, but reassigning it (e.g. s = append(s, 1)) does not change the original header.

2.4 Stack and Heap: Escape Analysis

  • Stack: automatically allocated and freed with function calls; fast, but limited in size.
  • Heap: managed by the GC; longer lifetime, large space, but allocation and reclamation have a cost.
  • Escape analysis: the compiler's process of deciding whether a variable lives on the stack or the heap. If a variable is still referenced after the function returns, it "escapes" to the heap.
  • Tool: use go build -gcflags=-m or go run -gcflags=-m to inspect the escape analysis results.

Example Code

Example 1: Pointer Basics and a new/make Comparison

Demonstrates taking addresses and dereferencing, and the difference between creating slices and maps with new and make.

package main

import (
    "fmt"
)

func main() {
    // 1. Pointer basics
    var x int = 10
    p := &x // take the address; p has type *int
    fmt.Printf("x value: %d, address: %p\n", x, p)
    *p = 20 // modify through dereference
    fmt.Printf("x after modification: %d\n", x)

    // 2. Using new
    // new returns a pointer to a zeroed value of the type
    pInt := new(int)
    *pInt = 30
    fmt.Printf("new-allocated int value: %d\n", *pInt)

    // 3. Using make
    // make is used for slice, map, chan, and returns an initialized instance
    s := make([]int, 0, 5) // length 0, capacity 5
    s = append(s, 1)
    fmt.Printf("make-created slice: %v\n", s)

    m := make(map[string]int, 10) // the hint is just an initial-capacity suggestion; maps have no cap
    m["key"] = 100
    fmt.Printf("make-created map: %v\n", m)
}

Expected output:

x value: 10, address: 0xc000012110 (varies on each run)
x after modification: 20
new-allocated int value: 30
make-created slice: [1]
make-created map: map[key:100]

Example 2: An Introduction to Escape Analysis

Demonstrates when a variable escapes to the heap, using -gcflags=-m to observe the compiler's behavior.

package main

import "fmt"

// escapeExample demonstrates an escape
func escapeExample() *int {
    x := 10
    // x is returned after the function returns, so it escapes to the heap
    return &x
}

func main() {
    p := escapeExample()
    fmt.Println(*p)
}

Expected output:

10

(Note: run go run -gcflags=-m example2.go to see the escape information. The actual output contains ./example2.go:9:2: moved to heap: x, showing that x escapes to the heap. You will also see a escapes to heap / b escapes to heap; those come from boxing of the ...any arguments to fmt.Printf, which is a different matter from the return-value escape. Line numbers change with the file contents, and inlining-related hints may also appear on each compilation.)

Common Mistakes & Pitfalls

  1. Misusing new to create a slice/map and then operating on it directly

    • Wrong code: s := new([]int); s = append(s, 1)
    • Why: new([]int) returns *[]int, pointing at a nil slice, which is legal in itself; but the first argument to append must be a slice, so writing append(s, 1) fails to compile (first argument to append must be a slice). To use it you must explicitly dereference: s := new([]int); *s = append(*s, 1). In practice it is still better to use make([]int, 0) or var s []int.
    • Correct approach: use make to create slices or maps.
  2. Believing that passing a slice passes a copy

    • Wrong assumption: after calling func modifySlice(s []int) { s[0] = 99 }, the original slice is unchanged.
    • Why: s[0] = 99 modifies the underlying array, so the original slice sees it (the actual output is [99 2 3]). Passing a *[]int pointer is only necessary when you want to change the slice header itself (length/capacity/reassignment). A slice is "descriptor passed by value + underlying array shared".
    • Correct approach: understand the value-passing nature of the slice header and the sharing of the underlying array.
  3. Indexing a pointer-typed map

    • Wrong code: m := new(map[string]int); m["key"] = 1
    • Why: m is of type *map[string]int and cannot be indexed directly. The compiler reports: invalid operation: cannot index m (variable of type *map[string]int).
    • Correct approach: (*m)["key"] = 1, or more simply, just use make.

Best Practices

  1. Prefer value passing: for small structs or primitive types, value passing is more intuitive and avoids nil-pointer risk.
  2. Use pointers for large structs or when you need to modify the original: this avoids copy overhead, or when you explicitly need to modify the caller's data.
  3. Watch out for unnecessary small-object escapes on hot paths: check with go build -gcflags=-m; but constructing and returning an object pointer (such as func NewX() *X) is an idiomatic Go pattern, in which case heap allocation is by design and not something to avoid.
  4. Initialize slices with make: unless you specifically need a nil slice, use make to ensure the slice is usable.
  5. Do not rely on pointer address stability: addresses and relative offsets may differ on every run; do not depend on them.

Exercises

  1. Concept question: explain the difference in memory allocation between new([]int) and make([]int, 0), and the difference in their return types.
  2. Coding exercise: write a function swap(a, b *int) that swaps the values of two integers. Test it in main.
  3. Advanced/debugging exercise: can the code below correctly modify a slice element? Explain the memory semantics of passing a slice as an argument, and judge whether using *[]int here is necessary. go func modify(s *[]int) { (*s)[0] = 99 } func main() { s := []int{1, 2, 3} modify(&s) fmt.Println(s) }

Reference Answers

Reference Answers (click to expand / collapse)

Question 1

new([]int) returns *[]int: it allocates a block of memory on the heap or stack to hold the zero value of []int (i.e. a nil slice header), and returns a pointer to that memory. It does not allocate an underlying array. make([]int, 0) returns []int: it allocates and initializes the slice header (pointer, length, capacity); for a zero-length slice it usually does not allocate an underlying array (sharing zerobase). The difference is: new returns a pointer that you must dereference; make returns the value itself, ready to use.

Question 2

package main

import "fmt"

func swap(a, b *int) {
    *a, *b = *b, *a
}

func main() {
    x, y := 10, 20
    swap(&x, &y)
    fmt.Printf("x: %d, y: %d\n", x, y)
}

Explanation: by passing pointers, the function dereferences and swaps the values internally, thereby modifying the original variables.

Question 3

This code is perfectly legal in current Go (*[]int is an ordinary pointer type). Analysis: - A slice is a value type, but its underlying array is shared. - modify takes a *[]int and accesses and modifies the underlying array element through (*s)[0]. - Is *[]int necessary? If you only modify elements, passing []int suffices (because the underlying array is shared). But if you need to modify the slice header itself (such as *s = append(*s, ...) to change length/capacity), you must pass a pointer. - This exercise only modifies elements, so passing []int is more idiomatic, but passing *[]int is also entirely correct and legal.

Lesson Summary

  • Pointers take addresses with & and dereference with *; their zero value is nil.
  • new allocates a zero value and returns a pointer; make initializes reference types and returns the instance.
  • A slice is a value type, but its underlying array is shared, so modifying elements affects the original slice.
  • Escape analysis decides whether a variable lives on the stack or the heap; -gcflags=-m is an important debugging tool.

Next lesson preview: struct definitions and field access — the foundation of Go's object-oriented style.

Feedback

This site is front-end only — no backend, no accounts — so feedback goes to GitHub Issues, Discussions, or email.

Paste it into the issue or email to help track it down — it never includes anything you typed

Also:Browse existing issues · Email feedback (no GitHub account needed):278975598@qq.com