Skip to content

🧬 Arrays & Slices Internals

Slices là "The Soul of Go". Hiểu cách chúng hoạt động under the hood là điều bắt buộc để viết performant, bug-free code.

📐 Arrays: The Foundation

Arrays trong Go có fixed size và là value type:

go
// Declaration - size là PHẦN của type
var arr1 [5]int            // [0, 0, 0, 0, 0]
arr2 := [3]string{"a", "b", "c"}
arr3 := [...]int{1, 2, 3}  // Compiler counts: [3]int

// [5]int và [3]int là KHÁC TYPE!
// var x [5]int = [3]int{1,2,3}  // ❌ Compile error

Value Semantics: The Copying Problem

🔥 Raizo's Pitfall: Array Copying

Arrays are VALUES, không phải references. Passing array = copying entire array!

go
func modifyArray(arr [1000000]int) {
    arr[0] = 999  // Modifies COPY, không phải original!
}

func main() {
    bigArr := [1000000]int{}
    modifyArray(bigArr)  // 💥 Copies 8MB of data!
    fmt.Println(bigArr[0])  // 0 - unchanged
}

Memory Impact:

┌─────────────────────────────────────────────────────────┐
│                    FUNCTION CALL                        │
├─────────────────────────────────────────────────────────┤
│   Caller Stack:                                         │
│   bigArr [1000000]int → 8,000,000 bytes                │
│                                                         │
│   modifyArray Stack:                                    │
│   arr [1000000]int → 8,000,000 bytes (COPY!)           │
│                                                         │
│   Total: 16MB just to call one function!               │
└─────────────────────────────────────────────────────────┘

Đây là lý do chúng ta HIẾM KHI dùng arrays trực tiếp → Use Slices!


🔪 Slices: The Dynamic View

Slice Header Structure

🎓 Professor Tom's Deep Dive: Slice Internals

Một slice không chứa data trực tiếp. Nó là một descriptor (header) trỏ đến underlying array:

go
// runtime/slice.go
type slice struct {
    array unsafe.Pointer  // Pointer to underlying array
    len   int             // Current length
    cap   int             // Capacity (from ptr to end of array)
}

Memory Layout:

┌─────────────────────────────────────────────────────────────┐
│                    SLICE HEADER (24 bytes)                  │
├──────────────┬──────────────┬──────────────────────────────┤
│   array      │     len      │           cap                │
│   *byte      │     int      │           int                │
│   8 bytes    │   8 bytes    │         8 bytes              │
└──────┬───────┴──────────────┴──────────────────────────────┘

       │   Points to element at index 0

┌──────────────────────────────────────────────────────────┐
│                   UNDERLYING ARRAY                        │
│  ┌────┬────┬────┬────┬────┬────┬────┬────┐               │
│  │ 10 │ 20 │ 30 │ 40 │ 50 │  0 │  0 │  0 │               │
│  └────┴────┴────┴────┴────┴────┴────┴────┘               │
│   ◄──────── len=5 ────────►                              │
│   ◄──────────────── cap=8 ──────────────►                │
└──────────────────────────────────────────────────────────┘

Creating Slices

go
// 1. Literal
s1 := []int{1, 2, 3}

// 2. make(type, len, cap)
s2 := make([]int, 5)       // len=5, cap=5
s3 := make([]int, 5, 10)   // len=5, cap=10

// 3. Slicing an array
arr := [5]int{10, 20, 30, 40, 50}
s4 := arr[1:4]  // []int{20, 30, 40}, len=3, cap=4

// 4. Slicing a slice
s5 := s1[1:]    // Shares underlying array with s1!

Slicing: Creating Views

go
arr := [8]int{0, 1, 2, 3, 4, 5, 6, 7}

s1 := arr[2:5]   // [2, 3, 4], len=3, cap=6
s2 := arr[2:5:5] // [2, 3, 4], len=3, cap=3 (3rd arg = cap limit)

Visual:

arr: [ 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 ]
           ↑           ↑
      s1 starts    s1 ends
           |←─ len=3 ─→|
           |←─────── cap=6 ──────→|

🔧 The append Mechanic

How append Works

go
s := make([]int, 0, 4)  // len=0, cap=4

s = append(s, 1)  // len=1, cap=4 - fits
s = append(s, 2)  // len=2, cap=4 - fits
s = append(s, 3)  // len=3, cap=4 - fits
s = append(s, 4)  // len=4, cap=4 - fits
s = append(s, 5)  // len=5, cap=8 - REALLOCATE! (doubled)

🎓 Professor Tom's Deep Dive: Growth Strategy

Go's slice growth algorithm (Go 1.18+):

  1. If cap < 256: Double the capacity
  2. If cap >= 256: Grow by ~1.25x (actually cap + (cap + 3*256) / 4)
go
// Approximate growth pattern:
// cap: 0 → 1 → 2 → 4 → 8 → 16 → 32 → 64 → 128 → 256
// Then: 256 → 512 → 768 → 1024 → 1280 → ... (slower growth)

Tại sao không luôn double?

  • Large slices sẽ waste memory (1GB → 2GB là quá nhiều)
  • 1.25x growth giảm memory overhead ở scale lớn

Memory visualization:

Before append (cap full):
┌────┬────┬────┬────┐
│ 1  │ 2  │ 3  │ 4  │  cap=4, len=4
└────┴────┴────┴────┘
       ↓ append(s, 5)
       
After reallocation:
┌────┬────┬────┬────┬────┬────┬────┬────┐
│ 1  │ 2  │ 3  │ 4  │ 5  │    │    │    │  cap=8, len=5
└────┴────┴────┴────┴────┴────┴────┴────┘
  ↑ NEW array, OLD array becomes garbage

Reallocation Changes Pointer!

🔥 Raizo's Pitfall: Append May Reallocate

Sau reallocation, slice trỏ đến ARRAY MỚI!

go
original := make([]int, 3, 3)  // cap=3
original[0], original[1], original[2] = 1, 2, 3

alias := original  // Cả hai trỏ đến cùng array

// Append triggers reallocation
original = append(original, 4)

// BÂY GIỜ:
// - original trỏ đến array MỚI [1,2,3,4]
// - alias vẫn trỏ đến array CŨ [1,2,3]

original[0] = 999
fmt.Println(alias[0])  // 1 (not 999!) - Different arrays!

Rule: Sau append, luôn dùng slice được return!

go
s = append(s, value)  // ✅ Always reassign
append(s, value)       // ❌ Result ignored

🚨 Memory Leak Trap

The Problem: Large Backing Array

🔥 Critical: Memory Leak via Slicing

go
func getHeader(data []byte) []byte {
    return data[:100]  // Chỉ cần 100 bytes đầu
}

func main() {
    hugeFile, _ := os.ReadFile("10gb_file.log")  // 10GB
    header := getHeader(hugeFile)
    
    // header chỉ có len=100, nhưng...
    // Underlying array vẫn là 10GB!
    // GC KHÔNG THỂ thu hồi 10GB vì header vẫn reference nó!
    
    // Giữ header trong memory = giữ 10GB
    cache[key] = header  // 💥 Memory leak!
}

Memory Layout:

hugeFile slice header → [ 10GB array .......................... ]

header slice header ─────┘
  (len=100, cap=10GB)

GC sees: "header references 10GB array, cannot free"

The Fix: Use copy

go
func getHeaderSafe(data []byte) []byte {
    header := make([]byte, 100)  // Allocate new, small array
    copy(header, data[:100])      // Copy only needed data
    return header
}

func main() {
    hugeFile, _ := os.ReadFile("10gb_file.log")
    header := getHeaderSafe(hugeFile)
    
    // header có underlying array riêng, chỉ 100 bytes
    // hugeFile có thể được GC sau khi hết scope
    
    hugeFile = nil  // Hint cho GC
    cache[key] = header  // ✅ Only 100 bytes cached
}

🧩 Slice Operations Cheatsheet

Delete Element (Maintain Order)

go
// Delete element at index i, maintaining order
func deleteOrdered(s []int, i int) []int {
    return append(s[:i], s[i+1:]...)
}

// Complexity: O(n) - phải shift tất cả elements sau i

Visual (delete index 2):

Before: [ A | B | C | D | E ]
              ↑ delete this

Step 1: s[:2] = [ A | B ]
Step 2: s[3:] = [ D | E ]
Step 3: append(s[:2], s[3:]...) = [ A | B | D | E ]

Delete Element (No Order - Fast)

go
// Delete without maintaining order - O(1)
func deleteUnordered(s []int, i int) []int {
    s[i] = s[len(s)-1]     // Move last element to deleted position
    return s[:len(s)-1]    // Shrink slice
}

Insert Element

go
// Insert value at index i
func insert(s []int, i int, val int) []int {
    s = append(s, 0)       // Extend slice
    copy(s[i+1:], s[i:])   // Shift elements right
    s[i] = val             // Insert value
    return s
}

🧠 Parsons Problem: Slice Deletion Logic

🧩 Coding Challenge: Đúng thứ tự các bước

Task: Xóa phần tử tại index i từ slice s và giữ nguyên thứ tự các phần tử còn lại.

Sắp xếp các bước sau theo đúng thứ tự:

📝 Các bước (chưa sắp xếp)
  • A. return append(s[:i], s[i+1:]...)
  • B. Kiểm tra i >= 0 && i < len(s) để đảm bảo index hợp lệ
  • C. Lấy phần slice trước index i: s[:i]
  • D. Lấy phần slice sau index i: s[i+1:]
  • E. Nối hai phần lại với nhau bằng append
Đáp án đúng

Thứ tự đúng: B → C → D → E → A

go
func deleteAt(s []int, i int) []int {
    // B: Validate index
    if i < 0 || i >= len(s) {
        return s
    }
    
    // C + D + E: Combine parts, skipping index i
    // A: Return the result
    return append(s[:i], s[i+1:]...)
}

Giải thích:

  1. Validate first - Tránh panic nếu index ngoài range
  2. s[:i] - Tất cả elements TRƯỚC index i
  3. s[i+1:] - Tất cả elements SAU index i (skip i)
  4. append - Nối lại, element tại i bị bỏ qua
  5. return - Trả về slice mới (có thể cùng backing array)

Performance: Slice vs container/list

go
// Benchmark: Append 1 million elements

// Native Slice: ~15ms, contiguous memory, cache-friendly
// container/list: ~150ms, scattered nodes, cache misses

// Rule: Dùng slice cho hầu hết cases
// Chỉ dùng list khi cần frequent insertions ở giữa
OperationSlicecontainer/list
Append at endO(1) amortizedO(1)
Insert at middleO(n)O(1)*
Random accessO(1)O(n)
Memory localityExcellentPoor
GC pressureLowHigh

*Nếu đã có pointer đến node


💝 Support HPN Development

Mua Raizo một ly cà phê?

Mỗi module như thế này mất 2-3 giờ để research, viết, và test. Nếu bạn thấy content này hữu ích, hãy cân nhắc support để Raizo tiếp tục sản xuất thêm!

👑 Xem Premium Options

🚀 HPN Ultimate Starter Kit

Muốn setup VS Code chuẩn để code Go mượt như Raizo?

Bao gồm:

  • VS Code settings tối ưu cho Go
  • Extensions pack (gopls, delve, etc.)
  • Snippets và keybindings
  • Debug configurations

Mua ngay HPN Ultimate Starter Kit →


📊 Summary

ConceptKey Point
ArraysFixed size, value type, copying on pass
Slice Header{Pointer, Length, Capacity} - 24 bytes
SlicingCreates VIEW into backing array
appendMay reallocate, always reassign
Memory LeakSlice small from large → use copy

➡️ Tiếp theo

Slices nắm vững rồi! Tiếp theo: Maps Internals - Hash table structure, buckets, và concurrent access.