Slice
Trong Go, mảng và slice trông gần như giống hệt nhau, nhưng chức năng có sự khác biệt không nhỏ, mảng là cấu trúc dữ liệu có độ dài cố định, sau khi độ dài được chỉ định thì không thể thay đổi, còn slice là không cố định độ dài, slice sẽ tự động mở rộng khi dung lượng không đủ.
Mảng
Nếu biết trước độ dài dữ liệu cần lưu trữ, và sau đó không có nhu cầu mở rộng, có thể cân nhắc sử dụng mảng, mảng trong Go là kiểu giá trị, chứ không phải tham chiếu, không phải là con trỏ trỏ đến phần tử đầu tiên.
TIP
Mảng là kiểu giá trị, khi truyền mảng làm tham số cho hàm, do Go truyền tham số theo giá trị, nên sẽ sao chép toàn bộ mảng.
Khởi tạo
Độ dài của mảng khi khai báo chỉ có thể là một biểu thức hằng, không thể là biến, bạn không thể khai báo một biến rồi dùng biến đó làm giá trị độ dài của mảng
// Ví dụ đúng
var a [5]int
// Ví dụ sai
l := 1
var b [l]intTrước hết khởi tạo một mảng số nguyên độ dài 5
var nums [5]intCũng có thể dùng phần tử để khởi tạo
nums := [5]int{1, 2, 3}Có thể để trình biên dịch tự động suy luận độ dài
nums := [...]int{1, 2, 3, 4, 5} // Tương đương nums := [5]int{1, 2, 3, 4, 5}, dấu ba chấm phải tồn tại, nếu không sẽ sinh ra slice, không phải mảngCũng có thể thông qua hàm new để nhận một con trỏ
nums := new([5]int)Trên đây là một vài cách đều sẽ phân bổ một vùng bộ nhớ có kích thước cố định cho nums, điểm khác biệt chỉ là cách cuối cùng nhận được giá trị là con trỏ.
Khi khởi tạo mảng, cần lưu ý, độ dài phải là một biểu thức hằng, nếu không sẽ không thể biên dịch, biểu thức hằng tức là kết quả cuối cùng của biểu thức là một hằng số, ví dụ sai như sau:
length := 5 // Đây là một biến
var nums [length]intlength là một biến, nên không thể dùng để khởi tạo độ dài mảng, dưới đây là ví dụ đúng:
const length = 5
var nums [length]int // Hằng số
var nums2 [length + 1]int // Biểu thức hằng
var nums3 [(1 + 2 + 3) * 5]int // Biểu thức hằng
var nums4 [5]int // Thường dùng nhấtSử dụng
Chỉ cần có tên mảng và chỉ số, là có thể truy cập phần tử tương ứng trong mảng.
fmt.Println(nums[0])Tương tự cũng có thể sửa đổi phần tử mảng
nums[0] = 1Cũng có thể thông qua hàm tích hợp len để truy cập số lượng phần tử của mảng
len(nums)Hàm tích hợp cap để truy cập dung lượng của mảng, dung lượng của mảng bằng độ dài mảng, dung lượng đối với slice mới có ý nghĩa.
cap(nums)Cắt
Định dạng cắt mảng là arr[startIndex:endIndex], khoảng cắt là trái đóng phải mở. Và sau khi cắt mảng, sẽ trở thành kiểu slice. Ví dụ như sau:
nums := [5]int{1, 2, 3, 4, 5}
nums[:] // Phạm vi slice con [0,5) -> [1 2 3 4 5]
nums[1:] // Phạm vi slice con [1,5) -> [2 3 4 5]
nums[:5] // Phạm vi slice con [0,5) -> [1 2 3 4 5]
nums[2:3] // Phạm vi slice con [2,3) -> [3]
nums[1:3] // Phạm vi slice con [1,3) -> [2 3]func main() {
arr := [5]int{1, 2, 3, 4, 5}
fmt.Printf("%T\n", arr)
fmt.Printf("%T\n", arr[1:2])
}Xuất ra
[5]int
[]intNếu muốn chuyển đổi mảng thành kiểu slice, chỉ cần cắt không tham số là được, slice sau khi chuyển đổi và mảng ban đầu trỏ đến cùng một vùng bộ nhớ, việc sửa đổi slice sẽ dẫn đến thay đổi nội dung của mảng ban đầu
func main() {
arr := [5]int{1, 2, 3, 4, 5}
slice := arr[:]
slice[0] = 0
fmt.Printf("array: %v\n", arr)
fmt.Printf("slice: %v\n", slice)
}Xuất ra
array: [0 2 3 4 5]
slice: [0 2 3 4 5]Nếu muốn sửa đổi slice sau khi chuyển đổi, khuyến nghị sử dụng cách sau để chuyển đổi
func main() {
arr := [5]int{1, 2, 3, 4, 5}
slice := slices.Clone(arr[:])
slice[0] = 0
fmt.Printf("array: %v\n", arr)
fmt.Printf("slice: %v\n", slice)
}Xuất ra
array: [1 2 3 4 5]
slice: [0 2 3 4 5]Slice
Phạm vi ứng dụng của slice trong Go rộng hơn nhiều so với mảng, nó dùng để lưu trữ dữ liệu không biết độ dài, và trong quá trình sử dụng sau này có thể thường xuyên chèn và xóa phần tử.
Khởi tạo
Cách khởi tạo slice có một vài loại sau
var nums []int // Giá trị
nums := []int{1, 2, 3} // Giá trị
nums := make([]int, 0, 0) // Giá trị
nums := new([]int) // Con trỏCó thể thấy điểm khác biệt về hình dáng giữa slice và mảng, chỉ là thiếu một độ dài khởi tạo. Thông thường, khuyến nghị sử dụng make để tạo một slice rỗng, chỉ là đối với slice而言, hàm make nhận ba tham số: kiểu, độ dài, dung lượng. Lấy một ví dụ để giải thích sự khác biệt giữa độ dài và dung lượng, giả sử có một桶 nước, nước không đầy, chiều cao của桶 là dung lượng của桶, đại diện cho tổng cộng có thể chứa được bao nhiêu chiều cao nước, còn chiều cao nước trong桶 là đại diện cho độ dài, chiều cao nước nhất định nhỏ hơn hoặc bằng chiều cao của桶, nếu không nước sẽ tràn ra ngoài. Nên, độ dài của slice đại diện cho số lượng phần tử trong slice, dung lượng của slice đại diện cho slice tổng cộng có thể chứa được bao nhiêu phần tử, điểm khác biệt lớn nhất giữa slice và mảng là dung lượng của slice sẽ tự động mở rộng, còn mảng thì không, chi tiết hơn đến Sổ tay tham khảo - Độ dài và dung lượng.
TIP
Việc thực hiện cơ bản của slice vẫn là mảng, là kiểu tham chiếu, có thể đơn giản hiểu là con trỏ trỏ đến mảng cơ sở (về bản chất slice trong Go là một struct, bao gồm con trỏ trỏ đến mảng cơ sở, giá trị độ dài, giá trị dung lượng). Nên khi slice được truyền làm tham số hàm sẽ không sao chép mảng cơ sở, việc sửa đổi slice truyền vào trong hàm sẽ phản ánh vào slice ban đầu.
Slice được khai báo thông qua cách var nums []int, giá trị mặc định là nil, nên sẽ không phân bổ bộ nhớ cho nó, và khi sử dụng make để khởi tạo, khuyến nghị phân bổ trước một dung lượng đủ, có thể hiệu quả giảm tiêu hao bộ nhớ mở rộng sau này.
Sử dụng
Việc sử dụng cơ bản của slice hoàn toàn giống với mảng, điểm khác biệt chỉ là slice có thể biến hóa độ dài động, dưới đây xem một vài ví dụ.
Slice có thể thông qua hàm append để thực hiện nhiều thao tác, chữ ký hàm như sau, slice là slice mục tiêu cần thêm phần tử, elems là phần tử cần thêm, giá trị trả về là slice sau khi thêm.
func append(slice []Type, elems ...Type) []TypeTrước hết tạo một slice rỗng độ dài 0, dung lượng 0, sau đó chèn một số phần tử vào cuối, cuối cùng xuất độ dài và dung lượng.
nums := make([]int, 0, 0)
nums = append(nums, 1, 2, 3, 4, 5, 6, 7)
fmt.Println(len(nums), cap(nums)) // 7 8 Có thể thấy độ dài và dung lượng không giống nhau.Kích thước buffer mà slice mới dự trữ có quy luật nhất định. Trước khi cập nhật phiên bản golang1.18, hầu hết các bài viết trên mạng đều mô tả chiến lược mở rộng của slice như sau: Khi dung lượng slice ban đầu nhỏ hơn 1024, dung lượng slice mới biến thành 2 lần ban đầu; dung lượng slice ban đầu vượt quá 1024, dung lượng slice mới biến thành 1.25 lần ban đầu. Sau khi cập nhật phiên bản 1.18, chiến lược mở rộng của slice biến thành: Khi dung lượng slice ban đầu (oldcap) nhỏ hơn 256, dung lượng slice mới (newcap) là 2 lần ban đầu; dung lượng slice ban đầu vượt quá 256, dung lượng slice mới newcap = oldcap+(oldcap+3*256)/4
Chèn phần tử
Việc chèn phần tử của slice cũng cần kết hợp với hàm append để sử dụng, hiện có slice như sau,
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}Chèn phần tử từ đầu
nums = append([]int{-1, 0}, nums...)
fmt.Println(nums) // [-1 0 1 2 3 4 5 6 7 8 9 10]Chèn phần tử từ chỉ số giữa i
nums = append(nums[:i+1], append([]int{999, 999}, nums[i+1:]...)...)
fmt.Println(nums) // i=3, [1 2 3 4 999 999 5 6 7 8 9 10]Chèn phần tử từ cuối, là cách dùng nguyên thủy nhất của append
nums = append(nums, 99, 100)
fmt.Println(nums) // [1 2 3 4 5 6 7 8 9 10 99 100]Xóa phần tử
Việc xóa phần tử của slice cần kết hợp với hàm append để sử dụng, hiện có slice như sau
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}Xóa n phần tử từ đầu
nums = nums[n:]
fmt.Println(nums) //n=3 [4 5 6 7 8 9 10]Xóa n phần tử từ cuối
nums = nums[:len(nums)-n]
fmt.Println(nums) //n=3 [1 2 3 4 5 6 7]Xóa n phần tử bắt đầu từ vị trí chỉ số i ở giữa
nums = append(nums[:i], nums[i+n:]...)
fmt.Println(nums)// i=2, n=3, [1 2 6 7 8 9 10]Xóa tất cả phần tử
nums = nums[:0]
fmt.Println(nums) // []Sao chép
Khi sao chép slice cần đảm bảo slice mục tiêu có độ dài đủ, ví dụ
func main() {
dest := make([]int, 0)
src := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
fmt.Println(src, dest)
fmt.Println(copy(dest, src))
fmt.Println(src, dest)
}[1 2 3 4 5 6 7 8 9] []
0
[1 2 3 4 5 6 7 8 9] []Sửa độ dài thành 10, xuất ra như sau
[1 2 3 4 5 6 7 8 9] [0 0 0 0 0 0 0 0 0 0]
9
[1 2 3 4 5 6 7 8 9] [1 2 3 4 5 6 7 8 9 0]Duyệt
Việc duyệt slice hoàn toàn giống với mảng, vòng lặp for
func main() {
slice := []int{1, 2, 3, 4, 5, 7, 8, 9}
for i := 0; i < len(slice); i++ {
fmt.Println(slice[i])
}
}Vòng lặp for range
func main() {
slice := []int{1, 2, 3, 4, 5, 7, 8, 9}
for index, val := range slice {
fmt.Println(index, val)
}
}Slice nhiều chiều
Trước hết xem một ví dụ dưới đây, trong tài liệu chính thức cũng có giải thích: Effective Go - Slice hai chiều
var nums [5][5]int
for _, num := range nums {
fmt.Println(num)
}
fmt.Println()
slices := make([][]int, 5)
for _, slice := range slices {
fmt.Println(slice)
}Kết quả xuất ra là
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[]
[]
[]
[]
[]Có thể thấy, cùng là mảng và slice hai chiều, nhưng cấu trúc bên trong của chúng khác nhau. Mảng khi khởi tạo, độ dài một chiều và hai chiều đã cố định, còn độ dài của slice là không cố định, mỗi slice trong slice đều có thể có độ dài khác nhau, nên phải khởi tạo riêng, phần khởi tạo slice sửa thành mã sau là được.
slices := make([][]int, 5)
for i := 0; i < len(slices); i++ {
slices[i] = make([]int, 5)
}Kết quả xuất ra cuối cùng là
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]Biểu thức mở rộng
TIP
Chỉ có slice mới có thể sử dụng biểu thức mở rộng
Slice và mảng đều có thể sử dụng biểu thức đơn giản để cắt, nhưng biểu thức mở rộng chỉ có slice mới có thể sử dụng, tính năng này được thêm vào phiên bản Go1.2, chủ yếu là để giải quyết vấn đề đọc ghi chia sẻ mảng cơ sở của slice, định dạng chủ yếu như sau, cần thỏa mãn quan hệ low<= high <= max <= cap, dung lượng của slice được cắt bằng biểu thức mở rộng là max-low
slice[low:high:max]low và high vẫn là ý nghĩa ban đầu không đổi, còn max thêm ra thì chỉ là dung lượng tối đa, ví dụ trong ví dụ dưới đây bỏ qua max, thì dung lượng của s2 là cap(s1)-low
s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} // cap = 9
s2 := s1[3:4] // cap = 9 - 3 = 6Như vậy sẽ có một vấn đề rõ ràng, s1 và s2 là chia sẻ cùng một mảng cơ sở, khi đọc ghi s2, có thể ảnh hưởng đến dữ liệu của s1, đoạn mã sau thuộc về trường hợp này
s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} // cap = 9
s2 := s1[3:4] // cap = 9 - 3 = 6
s2 = append(s2, 1) // Thêm phần tử mới, do dung lượng là 6, nên không mở rộng, trực tiếp sửa đổi mảng cơ sở
fmt.Println(s2)
fmt.Println(s1)Kết quả xuất ra cuối cùng là
[4 1]
[1 2 3 4 1 6 7 8 9]Có thể thấy rõ ràng là thêm phần tử vào s2, nhưng lại sửa đổi cả s1, biểu thức mở rộng là để giải quyết loại vấn đề này mà sinh ra, chỉ cần sửa đổi một chút là có thể giải quyết vấn đề này
func main() {
s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} // cap = 9
s2 := s1[3:4:4] // cap = 4 - 3 = 1
s2 = append(s2, 1) // Dung lượng không đủ, phân bổ mảng cơ sở mới
fmt.Println(s2)
fmt.Println(s1)
}Kết quả nhận được lần này là bình thường
[4 1]
[1 2 3 4 5 6 7 8 9]clear
Trong go1.21 đã thêm hàm tích hợp clear, clear sẽ đặt tất cả giá trị trong slice thành giá trị 0,
package main
import (
"fmt"
)
func main() {
s := []int{1, 2, 3, 4}
clear(s)
fmt.Println(s)
}Xuất ra
[0 0 0 0]Nếu muốn xóa sạch slice, có thể
func main() {
s := []int{1, 2, 3, 4}
s = s[:0:0]
fmt.Println(s)
}Hạn chế dung lượng sau khi cắt, như vậy có thể tránh ghi đè các phần tử sau của slice ban đầu.
