KHOÁ HỌC Phần 2 — 20% còn lại Kiểu dữ liệu nâng cao

Kiểu dữ liệu nâng cao

Chương này gồm 4 mục — đọc mục nào cần, không phải đọc hết 1 lượt.

A1. Slice — Sâu về cấu trúc & các bẫy thường gặp

🧠 Bản chất

Slice không phải array. Nó là một struct 3 trường bên trong runtime:

type slice struct {
    ptr *element  // Con trỏ đến array gốc
    len int       // Số phần tử đang có
    cap int       // Sức chứa tối đa trước khi phải cấp phát lại
}

💻 Ví dụ minh hoạ

s := make([]int, 3, 5)  // len=3, cap=5
fmt.Println(s, len(s), cap(s)) // [0 0 0] 3 5

s = append(s, 1, 2)      // len=5, cap=5 (chưa vượt cap)
s = append(s, 99)        // len=6, cap tăng gấp đôi → cap=10 (cấp phát mới!)

⚠️ Bẫy #1 — Slice chia sẻ array gốc

a := []int{1, 2, 3, 4, 5}
b := a[1:3]      // b = [2 3], nhưng vẫn dùng chung array với a
b[0] = 999
fmt.Println(a)   // [1 999 3 4 5] — a bị đổi theo!

👉 Cách xử lý an toàn: dùng copy để tách hẳn ra:

b := make([]int, 2)
copy(b, a[1:3])

⚠️ Bẫy #2 — append vào slice truyền vào hàm

func addOne(s []int) { s = append(s, 1) } // Không sửa được slice gốc nếu cap không đổi

👉 Cách đúng: trả về slice mới hoặc dùng con trỏ.

⚠️ Bẫy #3 — Vòng lặp với con trỏ (rất hay gặp)

users := []User{{"A"}, {"B"}, {"C"}}
var ptrs []*User
for _, u := range users {
    ptrs = append(ptrs, &u) // ❌ SAI: u là biến tái sử dụng, tất cả pointer cùng trỏ vào 1 chỗ
}

👉 Từ Go 1.22 đã sửa mặc định, nhưng vẫn nên viết:

for i := range users {
    ptrs = append(ptrs, &users[i]) // ✅ Đúng
}

A2. Map — Dictionary của Go

💻 Cú pháp cơ bản

m := map[string]int{
    "apple":  10,
    "banana": 20,
}

m["cherry"] = 30              // Thêm/sửa
delete(m, "apple")            // Xoá
value, ok := m["banana"]      // ok = true nếu tồn tại

⚠️ Bẫy #1 — Truy cập key không tồn tại

n := m["notexist"] // Trả về zero value (0 cho int, "" cho string), KHÔNG panic

👉 Luôn dùng dạng comma-ok để phân biệt "0 thật" và "không có key":

if v, ok := m["key"]; ok { ... }

⚠️ Bẫy #2 — Không an toàn khi concurrency

Đọc/ghi map từ nhiều goroutine → panic. Giải pháp: dùng sync.Mutex bọc quanh, hoặc sync.Map (xem chương "Concurrency nâng cao").

⚠️ Bẫy #3 — Thứ tự lặp không ổn định

for k, v := range m { ... } // Mỗi lần chạy có thể khác thứ tự

👉 Nếu cần thứ tự: lấy keys ra slice, sort.Strings(keys), rồi lặp qua slice.

⚠️ Bẫy #4 — Không thể &m["key"]

p := &m["apple"] // ❌ Compile error

Vì map có thể cấp phát lại khi lớn lên → pointer sẽ trỏ vào rác.

A3. String, byte, rune

🧠 Bản chất

  • string bất biến (immutable) — không sửa được từng ký tự.
  • byte = uint8 — 1 byte (ASCII).
  • rune = int32 — 1 ký tự Unicode (có thể nhiều byte).

💻 Ví dụ

s := "Xin chào"
fmt.Println(len(s))         // 10 (số BYTE, không phải ký tự)
fmt.Println(len([]rune(s))) // 8 (số ký tự thật)

for i, r := range s { // range trên string tự động decode rune
    fmt.Printf("%d: %c\n", i, r)
}

⚠️ Bẫy

s := "chào"
fmt.Println(s[0]) // 99 (byte 'c'), OK
fmt.Println(s[1]) // 104 (byte 'h'), OK
// Nhưng nếu là "🚀" thì s[0] không phải cả emoji!

🔧 Nối chuỗi hiệu quả

// ❌ Chậm: mỗi + tạo string mới
result := ""
for _, s := range list {
    result += s
}

// ✅ Nhanh: dùng strings.Builder
var b strings.Builder
for _, s := range list {
    b.WriteString(s)
}
result := b.String()

A4. Constants & iota

const Pi = 3.14

// iota = bộ đếm tự tăng từ 0 trong khối const
const (
    Sunday = iota  // 0
    Monday         // 1
    Tuesday        // 2
    Wednesday      // 3
)

// Ứng dụng: enum flag với bit shift
const (
    Read    = 1 << iota // 1 (0001)
    Write               // 2 (0010)
    Execute             // 4 (0100)
)
perm := Read | Write   // 3 (0011)
← Về mục lục