Giao diện
🎯 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.
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:
| Location | Characteristics | Cost |
|---|---|---|
| Stack | Fast alloc/dealloc, auto-cleanup | ~1 CPU cycle |
| Heap | Requires 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: uVisualizing 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!
| Method | Returns | For Types | Zero Value |
|---|---|---|---|
new(T) | *T | Any type | Yes |
make(T, ...) | T | slice, map, channel only | Initialized |
&T{} | *T | Structs, arrays | Customizable |
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/opPattern 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
| Concept | Best Practice |
|---|---|
| Pointer vs Value | Use pointer for mutation, large structs |
| Stack vs Heap | Prefer stack; avoid unnecessary escapes |
| new vs make | new for zero values; make for slice/map/chan |
| Allocation | Pre-allocate slices; use sync.Pool |
| Escape Analysis | Regular 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.