Skip to content

🎯 Pointers & Memory Management

"Understanding memory is understanding performance."
Go manages memory automatically, nhưng hiểu cách nó hoạt động là key để viết high-performance code.

📌 Pointer Basics

Pointer là gì?

Pointer là biến chứa địa chỉ bộ nhớ của một biến khác:

go
func main() {
    x := 42
    p := &x          // p là pointer đến x
    
    fmt.Println(x)   // 42 - value
    fmt.Println(&x)  // 0xc000018030 - address
    fmt.Println(p)   // 0xc000018030 - pointer value (same address)
    fmt.Println(*p)  // 42 - dereferenced value
    
    *p = 100         // Modify through pointer
    fmt.Println(x)   // 100 - x changed!
}

Memory Layout

Stack Frame:
┌─────────────────────────────────────────┐
│  Variable x                             │
│  Address: 0xc000018030                  │
│  Value: 42                              │
├─────────────────────────────────────────┤
│  Variable p (*int)                      │
│  Address: 0xc000018038                  │
│  Value: 0xc000018030 (points to x)      │
└─────────────────────────────────────────┘

🏗️ Stack vs Heap

🎓 Professor Tom's Deep Dive: Memory Allocation

Go runtime quyết định allocation location dựa trên escape analysis:

LocationCharacteristicsCost
StackFast alloc/dealloc, auto-cleanup~1 CPU cycle
HeapRequires GC, survives function~100+ CPU cycles

Stack được ưu tiên khi:

  • Variable không escape khỏi function
  • Size được biết tại compile-time
  • Không có pointer đến variable được return

Escape Analysis in Action

go
// ❌ Escapes to Heap - pointer returned
func createUserBad() *User {
    u := User{Name: "Raizo"}  // Allocated on HEAP
    return &u                  // Escapes!
}

// ✅ Stack allocation - no escape
func processUser() {
    u := User{Name: "Raizo"}  // Stack - freed when function returns
    fmt.Println(u.Name)
}

// Kiểm tra escape analysis:
// $ go build -gcflags="-m" ./...
// ./main.go:5:2: moved to heap: u

Visualizing Escape

createUserBad():
┌─────────────────────────────────────────┐
│ Stack Frame                             │
│ ┌─────────────────────────────────────┐ │
│ │ return value: *User ────────────────┼─┼──┐
│ └─────────────────────────────────────┘ │  │
└─────────────────────────────────────────┘  │

Heap:                                        │
┌─────────────────────────────────────────┐  │
│ User{Name: "Raizo"}  ◄─────────────────────┘
│ Must survive function return            │
│ GC will eventually clean this           │
└─────────────────────────────────────────┘

🔧 new() vs make() vs Literal

🔥 HPN's Pitfall: Chọn đúng constructor!

MethodReturnsFor TypesZero Value
new(T)*TAny typeYes
make(T, ...)Tslice, map, channel onlyInitialized
&T{}*TStructs, arraysCustomizable
go
// new() - returns pointer to zero value
p := new(int)           // *int, points to 0
u := new(User)          // *User, all fields zero

// make() - for built-in reference types
s := make([]int, 5)     // []int with len=5, cap=5
m := make(map[string]int) // initialized empty map
ch := make(chan int, 10) // buffered channel

// Literal - most common for structs
user := &User{          // *User with initialized fields
    Name: "Raizo",
    Age:  30,
}

// ❌ Common mistake: new with maps
m := new(map[string]int)  // *map[string]int pointing to nil map!
// (*m)["key"] = 1        // PANIC: assignment to nil map

// ✅ Correct
m := make(map[string]int)
m["key"] = 1

📦 Pointer Receivers vs Value Receivers

Decision Tree

Nên dùng Pointer Receiver?

├── Method cần modify receiver? 
│   └── YES → *T (pointer receiver)

├── Struct lớn (>64 bytes)? 
│   └── YES → *T (avoid copying)

├── Có method khác dùng pointer receiver?
│   └── YES → *T (consistency)

└── NO to all → T (value receiver)

Engineering Example

go
// User struct - moderate size
type User struct {
    ID        int64
    Name      string
    Email     string
    CreatedAt time.Time
    Metadata  map[string]string  // Makes copying expensive
}

// ✅ Pointer receiver - modifies state
func (u *User) SetEmail(email string) {
    u.Email = email
    u.Metadata["email_updated"] = time.Now().String()
}

// ✅ Pointer receiver - struct is large (has map)
func (u *User) Validate() error {
    if u.Name == "" {
        return errors.New("name required")
    }
    return nil
}

// ✅ Value receiver OK - small, read-only
func (u User) String() string {
    return fmt.Sprintf("User(%d: %s)", u.ID, u.Name)
}

// Consistency: nếu một method dùng pointer, tất cả nên dùng pointer
// Trừ khi có lý do cụ thể (như implementing fmt.Stringer)

Memory Optimization Patterns

Pattern 1: Pre-allocation

go
// ❌ BAD: Multiple allocations during append
func processItemsBad(items []Item) []Result {
    var results []Result  // len=0, cap=0
    for _, item := range items {
        results = append(results, process(item))  // May reallocate!
    }
    return results
}

// ✅ GOOD: Single allocation
func processItemsGood(items []Item) []Result {
    results := make([]Result, 0, len(items))  // Pre-allocate
    for _, item := range items {
        results = append(results, process(item))  // No reallocation
    }
    return results
}

// Benchmark difference:
// Bad:  ~500ns/op, 5 allocs/op
// Good: ~150ns/op, 1 allocs/op

Pattern 2: sync.Pool for Temporary Objects

go
var bufferPool = sync.Pool{
    New: func() interface{} {
        return new(bytes.Buffer)
    },
}

func processRequest(data []byte) []byte {
    // Get buffer from pool (may reuse existing)
    buf := bufferPool.Get().(*bytes.Buffer)
    buf.Reset()  // Clear previous content
    
    // Use buffer
    buf.Write(data)
    buf.WriteString("-processed")
    result := make([]byte, buf.Len())
    copy(result, buf.Bytes())
    
    // Return to pool for reuse
    bufferPool.Put(buf)
    
    return result
}

Pattern 3: Avoid Pointers in Hot Paths

go
// For small, frequently-accessed data, values are faster

// ❌ Pointer - cache miss, GC pressure
type PointPtr struct {
    X, Y *float64
}

// ✅ Value - cache-friendly, no GC
type PointVal struct {
    X, Y float64
}

// Benchmark with 1M points:
// PointPtr iteration: 15ms (cache misses)
// PointVal iteration: 3ms (contiguous memory)

💻 Engineering Example: Memory-Efficient Data Pipeline

go
package pipeline

import (
    "bytes"
    "encoding/json"
    "sync"
)

// Object pools for reuse
var (
    recordPool = sync.Pool{
        New: func() interface{} {
            return &Record{}
        },
    }
    bufferPool = sync.Pool{
        New: func() interface{} {
            return bytes.NewBuffer(make([]byte, 0, 4096))
        },
    }
)

// Record represents a data record
type Record struct {
    ID        int64             `json:"id"`
    Timestamp int64             `json:"ts"`
    Data      map[string]string `json:"data"`
}

// Reset clears Record for reuse
func (r *Record) Reset() {
    r.ID = 0
    r.Timestamp = 0
    // Reuse map if exists, otherwise create
    if r.Data == nil {
        r.Data = make(map[string]string, 8)
    } else {
        for k := range r.Data {
            delete(r.Data, k)
        }
    }
}

// ProcessBatch processes records with minimal allocations
func ProcessBatch(input [][]byte) ([][]byte, error) {
    // Pre-allocate output slice
    output := make([][]byte, 0, len(input))
    
    for _, raw := range input {
        // Get pooled record
        record := recordPool.Get().(*Record)
        record.Reset()
        
        // Parse
        if err := json.Unmarshal(raw, record); err != nil {
            recordPool.Put(record)
            continue
        }
        
        // Transform
        record.Data["processed"] = "true"
        record.Timestamp = time.Now().UnixNano()
        
        // Serialize with pooled buffer
        buf := bufferPool.Get().(*bytes.Buffer)
        buf.Reset()
        
        if err := json.NewEncoder(buf).Encode(record); err != nil {
            bufferPool.Put(buf)
            recordPool.Put(record)
            continue
        }
        
        // Copy result (buffer will be reused)
        result := make([]byte, buf.Len())
        copy(result, buf.Bytes())
        output = append(output, result)
        
        // Return to pools
        bufferPool.Put(buf)
        recordPool.Put(record)
    }
    
    return output, nil
}

Ship-to-Prod Checklist

Memory Management

  • [ ] Escape Analysis: Run go build -gcflags="-m" để check allocations
  • [ ] Pre-allocation: Dùng make([]T, 0, expectedCap) cho known sizes
  • [ ] sync.Pool: Implement cho frequently allocated objects
  • [ ] Pointer Receivers: Consistent usage across type methods
  • [ ] Benchmarks: Profile với -benchmem để track allocations

Code Review

  • [ ] Không có unnecessary pointer indirection
  • [ ] Large structs passed by pointer
  • [ ] No pointer to loop variable (pre-Go 1.22)
  • [ ] Maps và slices properly initialized (không phải nil)

Production Monitoring

  • [ ] runtime.ReadMemStats() cho memory metrics
  • [ ] pprof heap profile enabled
  • [ ] GC tuning nếu cần (GOGC, GOMEMLIMIT)

📊 Summary

ConceptBest Practice
Pointer vs ValueUse pointer for mutation, large structs
Stack vs HeapPrefer stack; avoid unnecessary escapes
new vs makenew for zero values; make for slice/map/chan
AllocationPre-allocate slices; use sync.Pool
Escape AnalysisRegular checks with -gcflags="-m"

➡️ Tiếp theo

Memory nắm vững rồi! Tiếp theo: Packages & Modules - Go module system và dependency management.