Series Go Backend - Arrays, Slices và cơ chế cấp phát bộ nhớ ngầm
Trong Go, quản lý tập hợp dữ liệu là thao tác diễn ra hàng ngày. Tuy nhiên, sự khác biệt giữa Array (mảng tĩnh) và Slice (mảng động) thường gây bối rối cho người mới, đặc biệt là cơ chế quản lý vùng nhớ bên dưới của Slice. Hiểu rõ phần này sẽ giúp bạn tránh được những bug memory rò rỉ rất khó phát hiện sau này.
1. Arrays (Mảng tĩnh)
Array trong Go có kích thước cố định và được xác định ngay lúc biên dịch. Bạn không thể thêm hay bớt phần tử sau khi khởi tạo.
Go
// Khai báo mảng 5 phần tử nguyên (Zero value là 0)
var ids [5]int
// Khai báo và gán giá trị ngay
cities := [3]string{"Hà Nội", "Đà Nẵng", "HCM"}
Đặc điểm cốt lõi của Array:
-
Kích thước là một phần của kiểu dữ liệu: Kiểu
[3]intvà[5]intlà hai kiểu hoàn toàn khác nhau. Bạn không thể truyền biến kiểu[3]intvào một hàm yêu cầu tham số[5]int. -
Pass-by-value: Khi gán một array cho một biến khác hoặc truyền vào hàm, Go sẽ copy toàn bộ dữ liệu sang một vùng nhớ mới. Nếu mảng có 1 triệu phần tử, việc truyền mảng vào hàm sẽ tiến hành sao chép cả 1 triệu phần tử đó, cực kỳ tiêu tốn tài nguyên.
2. Slices (Mảng động) và Cấu trúc bên dưới
Để khắc phục giới hạn của Array, Go cung cấp Slice. Trong 99% các trường hợp làm backend bằng Go, bạn sẽ dùng Slice thay vì Array.
Go
// Cú pháp giống Array nhưng KHÔNG có kích thước
var users []string
users = []string{"Admin", "User"}
Bản chất Slice không trực tiếp chứa dữ liệu. Dưới tầng low-level, Slice là một Struct (Header) đóng vai trò như một wrapper gồm 3 thành phần:
-
Pointer (Con trỏ): Trỏ đến địa chỉ của phần tử đầu tiên trong một Array ngầm (Underlying Array) nằm trên bộ nhớ.
-
Length (Chiều dài): Số lượng phần tử thực tế đang có trong Slice (lấy bằng hàm
len()). -
Capacity (Sức chứa): Kích thước tối đa của Underlying Array tính từ vị trí con trỏ (lấy bằng hàm
cap()).
Go
// Cách tốt nhất và tối ưu nhất để khởi tạo Slice khi biết trước kích thước là dùng make()
// make(kiểu_dữ_liệu, length, capacity)
orders := make([]int, 0, 100)
3. Cơ chế Append và Cấp phát bộ nhớ ngầm
Bạn dùng hàm append() để thêm phần tử vào Slice. Nhờ hiểu về Length và Capacity ở phần trước, bạn sẽ dễ dàng hình dung cách Go cấp phát bộ nhớ:
Go
nums := make([]int, 0, 3) // len=0, cap=3
nums = append(nums, 1) // len=1, cap=3 (Chưa vượt sức chứa)
nums = append(nums, 2, 3) // len=3, cap=3 (Đã đầy)
// Chuyện gì xảy ra ở dòng tiếp theo?
nums = append(nums, 4)
Khi bạn append phần tử thứ 4, Capacity bị vượt giới hạn. Lúc này Go sẽ tự động kích hoạt cơ chế sinh bộ nhớ:
-
Cấp phát một Underlying Array hoàn toàn MỚI với Capacity lớn hơn (thường là gấp đôi đối với các slice nhỏ).
-
Copy dữ liệu từ Array cũ sang Array mới.
-
Thêm số
4vào Array mới. -
Cập nhật Pointer của Slice trỏ sang Array mới. Array cũ sẽ bị Garbage Collector dọn dẹp sau đó.
Bẫy Memory Leak (Rò rỉ bộ nhớ) khi cắt Slice: Bạn có thể tạo Slice mới từ một Slice/Array cũ bằng cú pháp
[start:end].Go
original := []int{10, 20, 30, 40, 50} subSlice := original[1:3] // Chứa giá trị [20, 30]Lúc này
subSlicevàoriginaldùng chung một Underlying Array. Nếu bạn thay đổisubSlice[0] = 99, thìoriginal[1]cũng biến thành99. Hơn nữa, nếuoriginaltrỏ đến một array dữ liệu rất lớn, vàsubSlicechỉ cắt lấy 1 phần tử nhỏ, toàn bộ array khổng lồ kia vẫn bị giữ trong RAM (không bị GC dọn) vì con trỏ củasubSlicevẫn đang neo vào Underlying Array chung đó.
4. Duyệt Slice với Vòng lặp for range
Đây là biến thể thứ 4 của vòng lặp for (như đã nhắc ở bài trước), chuyên dùng để duyệt qua các phần tử của Array, Slice hoặc Map.
Go
products := []string{"Laptop", "Chuột", "Bàn phím"}
// range trả về 2 giá trị: index (vị trí) và value (giá trị tại vị trí đó)
for index, value := range products {
fmt.Printf("Sản phẩm %d: %s\n", index, value)
}
// Nếu không cần dùng index, bắt buộc phải dùng Blank Identifier "_" để bỏ qua, nếu không sẽ bị lỗi compile
for _, value := range products {
fmt.Println(value)
}
Lưu ý quan trọng: Biến value trong vòng lặp range là một bản copy của phần tử thực sự. Việc bạn thay đổi value = "Màn hình" sẽ không làm thay đổi dữ liệu gốc bên trong mảng products. Để update dữ liệu gốc, bạn bắt buộc phải truy cập qua index: products[index] = "Màn hình".
All rights reserved