Go Zero to Hero - Bài 16: Cấu trúc dữ liệu Mảng (Array) và những hạn chế
Trong Go, Array là một tập hợp các phần tử có cùng kiểu dữ liệu và kích thước được cố định ngay từ lúc khởi tạo.
1. Mảng (Array) trong Go là gì?
Bạn có thể khai báo Array theo nhiều cách, nhưng điểm chung là luôn phải có con số định hình kích thước nằm trong dấu ngoặc vuông [].
package main
import "fmt"
func main() {
// Cách 1: Khai báo tường minh với kích thước 5, kiểu int
// Lúc này các phần tử sẽ mang giá trị Zero Value của int là 0
var serverPorts [5]int
serverPorts[0] = 8080
serverPorts[1] = 3306
// Cách 2: Vừa khai báo vừa gán giá trị (Dùng nhiều nhất)
// Hệ thống máy chủ trạm (Station Servers)
stationIPs := [3]string{"10.0.0.1", "10.0.0.2", "10.0.0.3"}
// Cách 3: Nhờ Go tự đếm số lượng phần tử bằng dấu [...]
// Dấu [...] nghĩa là "Compiler hãy tự đếm xem tôi gán bao nhiêu giá trị nhé"
errorCodes := [...]int{400, 401, 403, 404, 500}
fmt.Println(stationIPs)
fmt.Printf("Số lượng mã lỗi: %d\n", len(errorCodes)) // Hàm len() dùng để lấy độ dài mảng
}
Ưu điểm tàng hình: CPU Cache Friendly
Tuy bị ghẻ lạnh, nhưng Array có một sức mạnh tuyệt đối về mặt hiệu năng ở tầng hệ thống (Low-level).
Khi bạn khai báo một mảng [5]int, Go sẽ yêu cầu hệ điều hành cấp phát một dải bộ nhớ nằm liên tiếp nhau (Contiguous Memory). Điều này giúp CPU có thể nạp toàn bộ mảng vào bộ đệm (CPU Cache) trong một lần đọc, khiến tốc độ truy xuất các phần tử trong Array nhanh như chớp.
2. Những hạn chế "chí mạng" của Array
Dù có tốc độ truy xuất cực nhanh, nhưng đối với các bài toán nghiệp vụ Backend biến đổi không ngừng, Array lộ ra 3 điểm yếu khiến nó mất điểm trầm trọng.
Hạn chế 1: Kích thước bị đóng đinh (Rigid Size)
Một khi bạn đã khai báo [3]string, mảng đó vĩnh viễn chỉ có 3 phần tử.
Bạn không thể dùng bất kỳ hàm nào (như append hay push) để nhét thêm phần tử thứ 4 vào. Nếu số lượng giao dịch đẩy về từ các trạm vé (TVM) đột ngột tăng lên, một mảng cố định sẽ lập tức tràn (Index out of bounds) và gây Panic sập hệ thống.
Hạn chế 2: Kích thước là một phần của "Kiểu dữ liệu"
Trong Go, [3]int và [5]int được coi là hai kiểu dữ liệu hoàn toàn khác nhau, giống như việc con chó và con mèo là hai loài khác nhau vậy.
Điều này dẫn đến một sự bất tiện khủng khiếp khi viết các hàm dùng chung (Reusable functions):
// Hàm này CHỈ nhận mảng CÓ ĐÚNG 3 PHẦN TỬ
func processLogs(logs [3]string) {
fmt.Println("Đang xử lý log...")
}
func main() {
logsStationA := [3]string{"OK", "OK", "FAIL"}
logsStationB := [5]string{"OK", "OK", "OK", "FAIL", "OK"}
processLogs(logsStationA) // HỢP LỆ
// LỖI BIÊN DỊCH! Cannot use logsStationB (type [5]string) as type [3]string
// processLogs(logsStationB)
}
Để viết một hàm xử lý linh hoạt cho mọi kích thước mảng, Array hoàn toàn bất lực.
Hạn chế 3: Truyền tham trị (Pass-by-value) - Cơn ác mộng RAM
Đây là điểm khác biệt lớn nhất của Go so với C++ hay Java.
Trong Java, khi bạn truyền một mảng vào hàm, bạn đang truyền một "con trỏ" trỏ tới mảng đó - rất nhẹ nhàng!
Nhưng trong Go, Array được xử lý theo dạng Tham trị (Value Type). Nghĩa là khi bạn gán mảng A cho mảng B, hoặc truyền mảng A vào một hàm, Go sẽ copy toàn bộ dữ liệu của mảng A ra một vùng nhớ mới tinh.
Hãy tưởng tượng bạn có một mảng chứa 100,000 bản ghi lịch sử quẹt thẻ (Ticket logs). Nếu bạn truyền mảng đó qua 3 hàm khác nhau (hàm Validate, hàm Format, hàm SaveToDB), Go sẽ âm thầm nhân bản cái mảng khổng lồ đó lên 3 lần trong RAM. Ứng dụng của bạn sẽ bị vắt kiệt bộ nhớ ngay lập tức!
func modifyArray(arr [3]int) {
arr[0] = 999 // Chỉ thay đổi trên bản COPY
}
func main() {
original := [3]int{1, 2, 3}
modifyArray(original)
// Mảng gốc không hề bị suy xuyển!
fmt.Println(original) // Kết quả vẫn là: [1 2 3]
}
Tổng kết
Array là khối gạch nguyên thủy, đóng vai trò cấp phát bộ nhớ liên tục cho hệ thống. Tuy nhiên, sự cứng nhắc về kích thước và cơ chế Copy đắt đỏ (Pass-by-value) khiến nó không phù hợp để làm việc trực tiếp với dữ liệu thay đổi liên tục trong các hệ thống Web hay Microservices.
All rights reserved