Go Zero to Hero - Bài 19: Các hàm thao tác trên Slice: append, copy
Slice trong Golang không chỉ là một danh sách động thông thường mà còn đi kèm với các hàm tích hợp sẵn cực kỳ mạnh mẽ để quản lý bộ nhớ và biến đổi dữ liệu.
1. Hàm append: Nghệ thuật "nhồi nhét" và gộp dữ liệu
Hàm append có nhiệm vụ thêm một hoặc nhiều phần tử vào cuối một Slice. Tuy nhiên, đằng sau cú pháp đơn giản của nó là những nguyên tắc cực kỳ chặt chẽ.
Nguyên tắc Vàng: Luôn phải gán lại giá trị (Re-assignment)
Cú pháp bắt buộc của append là: slice = append(slice, element).
Tại sao không thể chỉ gọi append(slice, element) như cách gọi slice.push(element) trong JavaScript?
Hãy nhớ lại các bài trước: Khi sức chứa (Capacity) bị đầy, Go sẽ tạo ra một vùng nhớ hoàn toàn mới và chuyển con trỏ sang đó. Hàm append sẽ trả về một Slice Header mới (chứa con trỏ mới, len mới, cap mới). Nếu bạn không gán kết quả trả về này đè lên biến cũ, biến của bạn vẫn sẽ trỏ vào vùng nhớ cũ vô dụng!
package main
import "fmt"
func main() {
users := make([]string, 0, 5)
// BẮT BUỘC PHẢI GÁN LẠI
users = append(users, "Hoàng")
users = append(users, "Nam", "Hải") // Có thể append nhiều phần tử cùng lúc
fmt.Println(users) // [Hoàng Nam Hải]
}
Tuyệt kỹ: Gộp 2 Slice bằng toán tử Spread (...)
Làm sao để nối (concat) hai mảng dữ liệu lại với nhau? Ví dụ bạn có 2 batch giao dịch AFC cần gộp lại để gửi lên Data Warehouse.
Trong Go, hàm append chỉ nhận đối số thứ hai là các phần tử rời rạc. Để nhét nguyên một Slice khác vào, bạn dùng dấu ... (Spread Operator) để "bung" Slice đó ra thành các phần tử lẻ.
func main() {
batch1 := []int{101, 102, 103}
batch2 := []int{201, 202}
// Nối batch2 vào đuôi batch1
// Dấu ... sẽ "giải nén" batch2 thành các số 201, 202
mergedBatch := append(batch1, batch2...)
fmt.Println(mergedBatch) // [101 102 103 201 202]
}
2. Dùng append để Xóa phần tử (Delete element)
Go không có hàm remove(). Đây là cú sốc với nhiều anh em newbie. Để xóa một phần tử khỏi Slice ở vị trí index bất kỳ, chúng ta phải tận dụng khả năng cắt mảng (Slicing) và ghép mảng (append).
Công thức Xóa: Cắt lấy khúc đầu (từ 0 đến index) và ghép với khúc đuôi (từ index+1 đến hết).
func main() {
// Giả lập danh sách mã lỗi, ta cần xóa mã lỗi ở vị trí index = 2 (tức là mã 403)
errorCodes := []int{400, 401, 403, 404, 500}
indexToRemove := 2
// Bước 1: Cắt khúc đầu errorCodes[:2] -> [400, 401]
// Bước 2: Cắt khúc đuôi errorCodes[3:] -> [404, 500]
// Bước 3: Gộp chúng lại
errorCodes = append(errorCodes[:indexToRemove], errorCodes[indexToRemove+1:]...)
fmt.Println(errorCodes) // Kết quả: [400 401 404 500]
}
⚠️ Lưu ý cho hệ thống High-Performance: Việc xóa phần tử bằng cách này bắt buộc Go phải dịch chuyển toàn bộ các phần tử ở khúc đuôi lên trước. Nếu bạn có 1 triệu bản ghi và muốn xóa bản ghi ở vị trí số 1, Go sẽ phải copy 999,999 phần tử còn lại dồn lên. Hãy cân nhắc về hiệu năng nếu làm việc này liên tục trong vòng lặp lớn!
3. Hàm copy: Kẻ đóng thế an toàn
Ở các bài trước, chúng ta đã cảnh báo về lỗi Memory Leak khi bạn cắt một mẩu nhỏ từ một Slice khổng lồ. Để mảng khổng lồ kia được dọn dẹp, bạn phải copy dữ liệu ra một vùng nhớ độc lập.
Cú pháp chuẩn: copy(destination, source). Hàm này trả về số lượng phần tử đã được copy thành công.
Cái bẫy "Trắng tay" kinh điển của Newbie
Khi dùng copy, rất nhiều lập trình viên mới gặp tình trạng code chạy không báo lỗi, nhưng mảng đích (destination) lại trống trơn (rỗng).
Lý do: Hàm copy của Go chỉ copy số lượng phần tử bằng đúng phần tử nhỏ nhất giữa len(destination) và len(source). Nếu bạn khởi tạo mảng đích với len = 0, hàm copy sẽ copy... 0 phần tử!
// CODE LỖI (Thường gặp)
func badCopy() {
src := []int{1, 2, 3}
// Khởi tạo len = 0, cap = 5
dst := make([]int, 0, 5)
copy(dst, src)
fmt.Println("Bad Copy:", dst) // KẾT QUẢ: [] (Rỗng tuếch!)
}
// CODE CHUẨN GO
func goodCopy() {
src := []int{1, 2, 3}
// BẮT BUỘC phải khởi tạo len bằng với len của mảng gốc
dst := make([]int, len(src))
copy(dst, src)
fmt.Println("Good Copy:", dst) // KẾT QUẢ: [1 2 3]
}
Tổng kết
Bạn đã nắm trọn bộ công cụ để thao túng danh sách trong Go:
append: Vũ khí tối thượng để thêm phần tử. Luôn nhớ phải gán lại (=) để tránh mất con trỏ vùng nhớ. Dùng toán tử...để gộp 2 Slice.- Xóa phần tử: Kết hợp kỹ thuật cắt lát (Slicing) và
append(head, tail...). copy: Công cụ tách bạch vùng nhớ. Hãy luôn nhớ khởi tạo Slice đích (Destination) có Length (len) đủ chứa dữ liệu nguồn, nếu không hàmcopysẽ không có tác dụng.
All Rights Reserved