Học Go: Không Bao Giờ Sử Dụng Arrays

Giới Thiệu
Nếu bạn mới với Go, bạn có thể ngạc nhiên khi biết rằng arrays hiếm khi được sử dụng trong code Go idiomatic. Mặc dù là một cấu trúc dữ liệu cơ bản trong hầu hết các ngôn ngữ lập trình, Go arrays có những hạn chế đáng kể khiến chúng không thực tế cho hầu hết các tình huống thực tế.
Trong bài viết này, chúng ta sẽ khám phá tại sao arrays gần như không bao giờ nên được sử dụng trong Go, hiểu sự khác biệt quan trọng giữa arrays và slices, và học khi nào slices là lựa chọn tốt hơn (đó là hầu như luôn luôn).
Arrays Trong Go Là Gì?
Trong Go, một array là một chuỗi các phần tử có kích thước cố định, được đánh số của một type duy nhất. Kích thước là một phần của type của array, dẫn đến một số hạn chế đáng ngạc nhiên.
package main
import "fmt"
func main() {
// Khai báo array - kích thước là một phần của type
var numbers [5]int
numbers[0] = 10
numbers[1] = 20
// Array literal
fruits := [3]string{"apple", "banana", "orange"}
fmt.Println(numbers) // [10 20 0 0 0]
fmt.Println(fruits) // [apple banana orange]
}Có vẻ đơn giản, phải không? Hãy xem tại sao điều này trở thành vấn đề.
Vấn Đề Cơ Bản: Kích Thước Là Một Phần Của Type
Vấn đề quan trọng nhất với Go arrays là kích thước là một phần của định nghĩa type. Điều này có nghĩa là [3]int và [5]int là các types hoàn toàn khác nhau, không tương thích.
package main
import "fmt"
func printArray(arr [3]int) {
fmt.Println(arr)
}
func main() {
arr1 := [3]int{1, 2, 3}
arr2 := [5]int{1, 2, 3, 4, 5}
printArray(arr1) // ✅ Hoạt động
// printArray(arr2) // ❌ Compile error: cannot use arr2 (type [5]int) as type [3]int
}Vấn đề: Bạn không thể viết các functions generic hoạt động với arrays có kích thước khác nhau. Bạn sẽ cần một function riêng cho mỗi kích thước array!
Vấn Đề #1: Arrays Được Truyền Theo Giá Trị
Trong Go, arrays là value types, không phải reference types. Khi bạn truyền một array cho một function, toàn bộ array được copy.
package main
import "fmt"
func modifyArray(arr [5]int) {
arr[0] = 999
fmt.Println("Inside function:", arr)
}
func main() {
numbers := [5]int{1, 2, 3, 4, 5}
modifyArray(numbers)
fmt.Println("Original array:", numbers)
}Output:
Inside function: [999 2 3 4 5]
Original array: [1 2 3 4 5]Vấn đề: Sửa đổi không ảnh hưởng đến array gốc vì một bản copy đầy đủ đã được tạo. Đối với arrays lớn, điều này vừa không hiệu quả (lãng phí bộ nhớ) vừa gây nhầm lẫn (hành vi không mong đợi).
Workaround: Truyền Pointers (Vẫn Không Tốt)
func modifyArray(arr *[5]int) {
arr[0] = 999
}
func main() {
numbers := [5]int{1, 2, 3, 4, 5}
modifyArray(&numbers)
fmt.Println(numbers) // [999 2 3 4 5]
}Điều này hoạt động, nhưng giờ bạn lại bị kẹt với vấn đề kích thước cố định. Bạn cần các functions khác nhau cho *[5]int, *[10]int, v.v.
Vấn Đề #2: Không Có Khả Năng Tăng Trưởng Built-in
Arrays có kích thước cố định. Bạn không thể append các phần tử hoặc resize chúng.
func main() {
arr := [3]int{1, 2, 3}
// arr = append(arr, 4) // ❌ Compile error: first argument to append must be slice
// Để "thêm" một phần tử, bạn cần tạo một array mới
newArr := [4]int{arr[0], arr[1], arr[2], 4}
fmt.Println(newArr)
}Vấn đề: Điều này rườm rà, dễ gây lỗi và không hiệu quả.
Vấn Đề #3: Không Thể Trả Về Các Kích Thước Khác Nhau
Vì kích thước array là một phần của type, các functions phải trả về một kích thước cụ thể.
// Function này CHỈ có thể trả về arrays kích thước 3
func getNumbers() [3]int {
return [3]int{1, 2, 3}
}
// Bạn không thể viết một function trả về arrays có kích thước động
// func getDynamicArray(size int) [size]int { // ❌ Invalid syntax
// ...
// }Vấn đề: Bạn không thể viết các functions linh hoạt trả về các collections có kích thước khác nhau.
Vấn Đề #4: Không Tương Thích Với Standard Library
Hầu hết các functions của standard library của Go hoạt động với slices, không phải arrays.
import (
"fmt"
"sort"
)
func main() {
arr := [5]int{5, 2, 8, 1, 9}
// sort.Ints(arr) // ❌ Compile error: cannot use arr (type [5]int) as type []int
// Bạn cần chuyển đổi thành slice
sort.Ints(arr[:])
fmt.Println(arr)
}Vấn đề: Arrays không tích hợp tốt với hệ sinh thái Go.
Giải Pháp: Sử Dụng Slices
Slices là câu trả lời cho tất cả những vấn đề này. Một slice là một view linh hoạt, động vào một array.
package main
import "fmt"
func main() {
// Khai báo slice (không chỉ định kích thước)
var numbers []int
// Slice literal
fruits := []string{"apple", "banana", "orange"}
// Append vào slice
numbers = append(numbers, 1, 2, 3)
fmt.Println(numbers) // [1 2 3]
fmt.Println(fruits) // [apple banana orange]
}Slices vs Arrays: Sự Khác Biệt Chính
| Tính năng | Array | Slice |
|---|---|---|
| Kích thước | Cố định, là phần của type | Động, không phải phần của type |
| Type | [N]Type | []Type |
| Truyền | Copy theo giá trị | Tham chiếu đến underlying array |
| Có thể resize | Không | Có (với append) |
| Standard lib | Hỗ trợ hạn chế | Hỗ trợ đầy đủ |
| Bộ nhớ | Value type | Reference type (header + array) |
Tại Sao Slices Tốt Hơn
1. Generic Functions
package main
import "fmt"
// Hoạt động với slices có bất kỳ độ dài nào
func printSlice(s []int) {
fmt.Println(s)
}
func main() {
slice1 := []int{1, 2, 3}
slice2 := []int{1, 2, 3, 4, 5, 6, 7, 8}
printSlice(slice1) // ✅ Hoạt động
printSlice(slice2) // ✅ Hoạt động
}2. Pass by Reference (Nhẹ)
package main
import "fmt"
func modifySlice(s []int) {
s[0] = 999
fmt.Println("Inside function:", s)
}
func main() {
numbers := []int{1, 2, 3, 4, 5}
modifySlice(numbers)
fmt.Println("Original slice:", numbers) // [999 2 3 4 5]
}Output:
Inside function: [999 2 3 4 5]
Original slice: [999 2 3 4 5]Slice được truyền by reference (thực tế, một copy của slice header), vì vậy các sửa đổi ảnh hưởng đến bản gốc.
3. Tăng Trưởng Động Với append
package main
import "fmt"
func main() {
slice := []int{1, 2, 3}
fmt.Printf("Length: %d, Capacity: %d\n", len(slice), cap(slice))
slice = append(slice, 4, 5)
fmt.Printf("Length: %d, Capacity: %d\n", len(slice), cap(slice))
fmt.Println(slice) // [1 2 3 4 5]
}Output:
Length: 3, Capacity: 3
Length: 5, Capacity: 6
[1 2 3 4 5]Slices tự động tăng trưởng khi cần!
4. Built-in Functions và Standard Library
package main
import (
"fmt"
"sort"
"strings"
)
func main() {
// Sorting
numbers := []int{5, 2, 8, 1, 9}
sort.Ints(numbers)
fmt.Println(numbers) // [1 2 5 8 9]
// Strings
words := []string{"banana", "apple", "cherry"}
sort.Strings(words)
fmt.Println(words) // [apple banana cherry]
// Joining
result := strings.Join(words, ", ")
fmt.Println(result) // apple, banana, cherry
}5. Return Types Linh Hoạt
package main
import "fmt"
func getNumbers(count int) []int {
result := make([]int, count)
for i := 0; i < count; i++ {
result[i] = i + 1
}
return result
}
func main() {
fmt.Println(getNumbers(3)) // [1 2 3]
fmt.Println(getNumbers(7)) // [1 2 3 4 5 6 7]
}Hiểu Slice Internals
Một slice là một cấu trúc nhẹ với ba thành phần:
type slice struct {
ptr *Type // Pointer đến underlying array
len int // Độ dài hiện tại
cap int // Capacity (kích thước của underlying array)
}package main
import "fmt"
func main() {
// Tạo slice từ array
arr := [5]int{1, 2, 3, 4, 5}
slice := arr[1:4] // slice trỏ đến arr[1], arr[2], arr[3]
fmt.Println("Array:", arr)
fmt.Println("Slice:", slice)
fmt.Printf("Len: %d, Cap: %d\n", len(slice), cap(slice))
// Sửa đổi slice ảnh hưởng đến array
slice[0] = 999
fmt.Println("Array after modification:", arr)
}Output:
Array: [1 2 3 4 5]
Slice: [2 3 4]
Len: 3, Cap: 4
Array after modification: [1 999 3 4 5]Khi Nào Thực Sự Sử Dụng Arrays?
Có những trường hợp hiếm khi arrays có ý nghĩa:
1. Cấu Trúc Dữ Liệu Có Kích Thước Cố Định
Khi kích thước thực sự cố định và có ý nghĩa đối với type.
type IPv4 [4]byte // Địa chỉ IP luôn 4 bytes
type SHA256 [32]byte // SHA-256 hash luôn 32 bytes
type Matrix3x3 [3][3]float64 // Ma trận 3x3
func parseIP(ip string) IPv4 {
// ...
}2. Code Performance-Critical Với Kích Thước Đã Biết
Nếu bạn cần tránh heap allocations trong các đường dẫn performance-critical.
func process() {
// Array được cấp phát trên stack (nhanh hơn)
var buffer [1024]byte
// vs slice có thể cấp phát trên heap
// buffer := make([]byte, 1024)
}3. Yêu Cầu Memory Layout
Khi bạn cần memory layout chính xác cho lập trình system-level hoặc CGO.
// Để interface với C code
type CStruct struct {
Values [10]uint32 // Phải chính xác 10 phần tử
}Quy tắc ngón tay cái: Nếu bạn không chắc chắn nên sử dụng array hay slice, hãy sử dụng slice.
Các Pattern Slice Phổ Biến
Tạo Slices
// Nil slice
var s []int
// Empty slice với make
s = make([]int, 0)
// Slice với độ dài ban đầu
s = make([]int, 5) // [0 0 0 0 0]
// Slice với length và capacity
s = make([]int, 5, 10) // len=5, cap=10
// Slice literal
s = []int{1, 2, 3, 4, 5}Append Phần Tử
slice := []int{1, 2, 3}
// Append phần tử đơn
slice = append(slice, 4)
// Append nhiều phần tử
slice = append(slice, 5, 6, 7)
// Append slice khác
other := []int{8, 9}
slice = append(slice, other...) // Lưu ý dấu ...Copy Slices
original := []int{1, 2, 3}
// Tạo bản copy
copied := make([]int, len(original))
copy(copied, original)
copied[0] = 999
fmt.Println("Original:", original) // [1 2 3]
fmt.Println("Copied:", copied) // [999 2 3]Slicing Slices
numbers := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
fmt.Println(numbers[2:5]) // [2 3 4]
fmt.Println(numbers[:3]) // [0 1 2]
fmt.Println(numbers[7:]) // [7 8 9]
fmt.Println(numbers[:]) // [0 1 2 3 4 5 6 7 8 9]Xóa Phần Tử
func remove(slice []int, index int) []int {
return append(slice[:index], slice[index+1:]...)
}
numbers := []int{1, 2, 3, 4, 5}
numbers = remove(numbers, 2) // Xóa index 2 (giá trị 3)
fmt.Println(numbers) // [1 2 4 5]Best Practices
1. Luôn Sử Dụng Slices Cho Function Parameters
// Tốt
func ProcessItems(items []string) {
// ...
}
// Không tốt
func ProcessItems(items [10]string) {
// ...
}2. Pre-allocate Slices Khi Biết Kích Thước
// Hiệu quả
items := make([]int, 0, expectedSize)
for i := 0; i < expectedSize; i++ {
items = append(items, i)
}
// Ít hiệu quả hơn (nhiều reallocations)
var items []int
for i := 0; i < 1000; i++ {
items = append(items, i)
}3. Hiểu Slice Sharing
original := []int{1, 2, 3, 4, 5}
slice1 := original[1:3]
slice2 := original[2:4]
slice1[1] = 999
fmt.Println(original) // [1 2 999 4 5]
fmt.Println(slice1) // [2 999]
fmt.Println(slice2) // [999 4]Cả ba slices đều chia sẻ cùng underlying array!
4. Sử Dụng copy Để Tránh Sharing Không Chủ Ý
original := []int{1, 2, 3, 4, 5}
independent := make([]int, len(original))
copy(independent, original)
independent[0] = 999
fmt.Println(original) // [1 2 3 4 5]
fmt.Println(independent) // [999 2 3 4 5]5. Kiểm Tra nil vs Empty Slices
var nilSlice []int
emptySlice := []int{}
madeSlice := make([]int, 0)
fmt.Println(nilSlice == nil) // true
fmt.Println(emptySlice == nil) // false
fmt.Println(madeSlice == nil) // false
// Nhưng tất cả đều có length 0
fmt.Println(len(nilSlice)) // 0
fmt.Println(len(emptySlice)) // 0
fmt.Println(len(madeSlice)) // 0Trong hầu hết các trường hợp, hãy đối xử với chúng giống nhau. Một số APIs phân biệt giữa nil và empty slices (như JSON marshaling).
Chuyển Đổi Giữa Arrays và Slices
Array Sang Slice
arr := [5]int{1, 2, 3, 4, 5}
// Full array as slice
slice := arr[:]
// Partial array as slice
slice = arr[1:4]Slice Sang Array (Go 1.17+)
slice := []int{1, 2, 3, 4, 5}
// Chuyển đổi thành array pointer
arr := (*[5]int)(slice)
// Hoặc sử dụng type conversion (Go 1.20+)
arr2 := [5]int(slice)Cảnh báo: Điều này panics nếu độ dài slice không khớp với kích thước array!
Cân Nhắc Hiệu Suất
Stack vs Heap Allocation
// Array trên stack (nhanh)
func useArray() {
var arr [1000]int
// Sử dụng arr...
}
// Slice có thể cấp phát trên heap
func useSlice() {
slice := make([]int, 1000)
// Sử dụng slice...
}Tuy nhiên, tính linh hoạt của slices hầu như luôn vượt trội hơn sự khác biệt hiệu suất nhỏ. Chỉ tối ưu nếu profiling cho thấy đó là bottleneck.
Quản Lý Capacity
// Hiệu quả: pre-allocated capacity
slice := make([]int, 0, 1000)
for i := 0; i < 1000; i++ {
slice = append(slice, i) // Không có reallocation
}
// Ít hiệu quả hơn: tăng trưởng động
var slice []int
for i := 0; i < 1000; i++ {
slice = append(slice, i) // Nhiều reallocations
}Các Lỗi Phổ Biến
1. Append Vào Shared Slices
original := []int{1, 2, 3}
slice1 := original
slice2 := original
slice1 = append(slice1, 4)
slice2 = append(slice2, 5)
fmt.Println(original) // Có thể là [1 2 3] hoặc [1 2 3 4] tùy capacity
fmt.Println(slice1) // [1 2 3 4]
fmt.Println(slice2) // [1 2 3 5]Giải pháp: Luôn reassign kết quả của append.
2. Loop Variable Addresses
// Sai
items := []int{1, 2, 3}
var pointers []*int
for _, item := range items {
pointers = append(pointers, &item) // ❌ Tất cả trỏ đến cùng variable!
}
// Tất cả pointers trỏ đến cùng address (giá trị cuối)
for _, p := range pointers {
fmt.Println(*p) // 3, 3, 3
}
// Đúng
for i := range items {
pointers = append(pointers, &items[i]) // ✅ Trỏ đến slice elements
}Kết Luận
Trong Go, arrays gần như không bao giờ nên được sử dụng cho lập trình mục đích chung. Đây là lý do:
- Kích thước là một phần của type: Làm cho code generic không thể
- Pass by value: Không hiệu quả và gây nhầm lẫn
- Không tăng trưởng: Không thể append hoặc resize
- Tích hợp hệ sinh thái kém: Standard library sử dụng slices
Sử dụng slices thay thế vì chúng cung cấp:
- ✅ Tính linh hoạt với sizing động
- ✅ Semantics pass-by-reference hiệu quả
- ✅ Tăng trưởng built-in với
append - ✅ Hỗ trợ standard library đầy đủ
- ✅ Code sạch hơn, dễ bảo trì hơn
Hãy nhớ: Thời gian duy nhất để sử dụng arrays là khi bạn có lý do chính đáng—các cấu trúc dữ liệu có kích thước cố định với ràng buộc kích thước có ý nghĩa, yêu cầu hiệu suất cụ thể, hoặc nhu cầu lập trình system-level. Khi nghi ngờ, luôn sử dụng slices.
Đọc Thêm
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.