Back to blog

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

GoBest PracticesArraysSlicesData Structures
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ăngArraySlice
Kích thướcCố định, là phần của typeĐộng, không phải phần của type
Type[N]Type[]Type
TruyềnCopy theo giá trịTham chiếu đến underlying array
Có thể resizeKhôngCó (với append)
Standard libHỗ trợ hạn chếHỗ trợ đầy đủ
Bộ nhớValue typeReference 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))  // 0

Trong 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.