1. Go Slices là gì?
Ở Go, Slice là một cấu trúc dữ liệu tương tự như mảng nhưng linh hoạt và mạnh mẽ hơn. Cũng giống như mảng, slice được sử dụng để lưu trữ nhiều giá trị cùng loại trong một biến duy nhất. Tuy nhiên, khác với mảng, số phần tử của một slice có thể tăng hoặc giảm tùy ý.
2. Khái niệm về length và capacity.
Đầu tiên hãy làm quen với các khái niệm về length và capacity của slice và array trước.
Length: Đây là số phần tử thực sự có trong mảng hoặc slice. Nó thể hiện số lượng phần tử mà bạn đã thêm vào mảng hoặc slice.
Capacity: Đây là số lượng phần tử tối đa của mảng hoặc slice, tức là số lượng phần tử tối đa mà mảng hoặc slice có thể chứa mà không cần phải cấp phát thêm bộ nhớ. Đối với slice, capacity là số lượng phần tử tối đa mà nó có thể chứa mà không cần phải cấp phát thêm bộ nhớ và nó phụ thuộc vào số lượng phần tử có thể chứa của array mà slice được tạo ra từ.
Lưu ý rằng khi bạn tạo một slice từ một array, capacity của slice sẽ bằng với số lượng phần tử có thể chứa của array mà slice đó được tạo ra. Còn nếu bạn tạo một slice mà không chỉ định một array cụ thể, capacity của slice sẽ bằng với số phần tử của slice (vì nó sẽ tạo một array mới với số phần tử là số phần tử của slice đó).
3. Cách Tạo Slice.
Tạo Slice bằng []datatype{values}.
Để khởi tạo slice khi khai báo, bạn có thể sử dụng []datatype{values}
như sau:
myslice := []int{1, 2, 3}
Để xem thông tin về số phần tử và capacity của slice bạn có thể sử dụng hàm len() và cap().
myslice := []int{1, 2, 3}
length := len(myslice)
capacity := cap(myslice)
fmt.Printf("Số phần tử của slice là %d/%d\n", length, capacity)
Khi bạn in ra, bạn sẽ thấy rằng số phần tử của slice là 3 và capacity cũng là 3. Trong trường hợp này, vì bạn đã khởi tạo slice với một số lượng phần tử cụ thể, nó sẽ có số phần tử và capacity bằng nhau.
Dưới đây là ví dụ minh họa tổng hợp cách tạo slice bằng []datatype{values}
trong Go:
package main
import "fmt"
func main() {
// Tạo slice rỗng
myslice1 := []int{}
fmt.Println(len(myslice1)) // In ra số phần tử của slice (0)
fmt.Println(cap(myslice1)) // In ra số lượng phần tử có thể chứa của slice (0)
fmt.Println(myslice1) // In ra slice (mảng rỗng)
// Tạo slice với các phần tử được chỉ định
myslice2 := []string{"Go", "Slices", "Are", "Powerful"}
fmt.Println(len(myslice2)) // In ra số phần tử của slice (4)
fmt.Println(cap(myslice2)) // In ra capacity của slice (4)
fmt.Println(myslice2) // In ra slice với các phần tử đã được chỉ định
}
Kết quả thực thi:
0
0
[]
4
4
[Go Slices Are Powerful]
Trong ví dụ trên, ta thấy rằng trong slice đầu tiên (myslice1
), các phần tử thực tế không được chỉ định, do đó cả số phần tử và capacity của slice đều là 0. Trong slice thứ hai (myslice2
) các phần tử được chỉ định và cả số phần tử và capacity đều bằng số phần tử đã chỉ định.
Tạo Slice từ Array.
Ví dụ này sẽ tạo một slice mới từ mảng arr
, bắt đầu từ phần tử thứ nhất và kết thúc ở phần tử thứ tư.
arr := [5]int{1, 2, 3, 4, 5}
myslice := arr[1:4]
Nếu bạn muốn tạo một slice từ một array với tất cả các phần tử có trong array đó, bạn có thể sử dụng slicing mà không cần chỉ định chỉ số bắt đầu và chỉ số kết thúc. Điều này sẽ tạo một slice bao gồm tất cả các phần tử của array.
arr := [5]int{1, 2, 3, 4, 5}
myslice := arr[:]
Ở đây, arr[:]
sẽ tạo một slice (myslice
) bao gồm tất cả các phần tử của array arr
. Kết quả là myslice
sẽ chứa các phần tử [1, 2, 3, 4, 5]
.
Nếu bạn không chỉ định chỉ số bắt đầu và kết thúc trong slicing, Go mặc định sẽ sử dụng toàn bộ số phần tử của array để tạo slice.
Dưới đây là một ví dụ tổng hợp về Tạo Slice từ Array.
package main
import "fmt"
func main() {
// Tạo một mảng
arr1 := [6]int{10, 11, 12, 13, 14, 15}
// Tạo một slice từ mảng arr1
myslice := arr1[2:4]
// In ra thông tin về slice
fmt.Printf("myslice = %v\n", myslice) // In ra các phần tử của slice
fmt.Printf("length = %d\n", len(myslice)) // In ra số phần tử của slice (2)
fmt.Printf("capacity = %d\n", cap(myslice)) // In ra số lượng phần tử có thể chứa của slice (4)
}
Kết quả thực thi:
myslice = [12 13]
length = 2
capacity = 4
Trong ví dụ trên, myslice
là một slice có số phần tử là 2, được tạo ra từ mảng arr1
có số phần tử là 6. Slice bắt đầu từ phần tử thứ hai của mảng (giá trị 12) và có thể mở rộng đến cuối mảng. Do đó, số lượng phần tử có thể chứa của slice là 4.
Nếu myslice
bắt đầu từ phần tử đầu tiên của mảng (vị trí 0), số lượng phần tử có thể chứa của slice sẽ bằng số phần tử của mảng (6).
Tạo Slice bằng make()
.
Bạn cũng có thể sử dụng hàm make()
để tạo slice với số phần tử và capacity được chỉ định. Hàm make()
được sử dụng chủ yếu khi bạn muốn tạo một slice với capacity lớn hơn số phần tử ban đầu hoặc khi bạn cần kiểm soát số phần tử và capacity của slice.
Cú pháp của make()
khi tạo một slice là:
slice := make([]T, length, capacity)
Trong đó:
T
là kiểu dữ liệu của phần tử trong slice.length
là số phần tử ban đầu của slice (số phần tử thực sự có trong slice).capacity
là số lượng phần tử có thể chứa ban đầu của slice (số phần tử tối đa mà slice có thể chứa mà không cần phải cấp phát thêm bộ nhớ).
Ví dụ này sẽ sử dụng hàm make()
tạo một slice với số phần tử là 3 và capacity là 5.
myslice := make([]int, 3, 5)
Ở đây, myslice
sẽ là một slice với số phần tử là 3 và capacity là 5. Nếu bạn thêm phần tử vào myslice
, số phần tử có thể tăng lên và nếu số lượng phần tử vượt quá capacity, slice có thể phải cấp phát bộ nhớ mới và capacity có thể tăng lên.
Hàm make()
thường được sử dụng khi bạn cần kiểm soát tổng số lượng phần tử có thể chứa của slice và khi bạn muốn tránh việc thay đổi tổng số lượng phần tử có thể chứa của slice sau mỗi lần thêm phần tử mới.
Dưới đây là một ví dụ khác thể hiện cách tạo Slice bằng hàm make()
:
package main
import "fmt"
func main() {
// Tạo một slice với số phần tử là 5 và tổng số lượng phần tử có thể chứa là 10
myslice1 := make([]int, 5, 10)
fmt.Printf("myslice1 = %v\n", myslice1) // In ra các phần tử của slice
fmt.Printf("length = %d\n", len(myslice1)) // In ra số phần tử của slice (5)
fmt.Printf("capacity = %d\n", cap(myslice1)) // In ra capacity của slice (10)
// Tạo một slice với số phần tử là 5, tổng số lượng phần tử có thể chứa sẽ bằng với số phần tử
myslice2 := make([]int, 5)
fmt.Printf("myslice2 = %v\n", myslice2) // In ra các phần tử của slice
fmt.Printf("length = %d\n", len(myslice2)) // In ra số phần tử của slice (5)
fmt.Printf("capacity = %d\n", cap(myslice2)) // In ra capacity của slice (5)
}
Kết quả thực thi:
myslice1 = [0 0 0 0 0]
length = 5
capacity = 10
myslice2 = [0 0 0 0 0]
length = 5
capacity = 5
Trong ví dụ trên, myslice1
được tạo với số phần tử là 5 và tổng số lượng phần tử có thể chứa là 10, trong khi myslice2
được tạo với số phần tử là 5 và tổng số lượng phần tử có thể chứa tự động bằng với số phần tử của nó.
Duyệt phần tử.
Trong ngôn ngữ lập trình, số phần tử luôn bắt đầu từ 0, điều này có nghĩa là [0] là phần tử đầu tiên, [1] là phần tử thứ hai và cứ như vậy. Dưới đây là một ví dụ:
package main
import "fmt"
func main() {
// Tạo một slice
prices := []int{10, 20, 30}
// Truy cập và in ra giá trị của phần tử đầu tiên (index 0)
fmt.Println(prices[0]) // Kết quả: 10
// Truy cập và in ra giá trị của phần tử thứ ba (index 2)
fmt.Println(prices[2]) // Kết quả: 30
}
Kết quả thực thi:
10
30
Trong ví dụ trên, prices[0]
truy cập và in ra giá trị của phần tử đầu tiên trong slice (10
), và prices[2]
truy cập và in ra giá trị của phần tử thứ ba trong slice (30
).
Thay đổi giá trị của một phần tử.
Bạn có thể thay đổi giá trị của một phần tử cụ thể trong một slice bằng cách sử dụng số thứ tự của phần tử (index). Dưới đây là một ví dụ:
package main
import "fmt"
func main() {
// Tạo một slice
prices := []int{10, 20, 30}
// Thay đổi giá trị của phần tử thứ ba (index 2)
prices[2] = 50
// In ra giá trị của phần tử đầu tiên và phần tử thứ ba sau khi thay đổi
fmt.Println(prices[0]) // Kết quả: 10
fmt.Println(prices[2]) // Kết quả: 50
}
Kết quả thực thi:
10
50
Trong ví dụ trên, prices[2] = 50
thay đổi giá trị của phần tử thứ ba trong slice từ 30
thành 50
. Sau đó chúng ta in ra giá trị của phần tử đầu tiên và phần tử thứ ba để kiểm tra sự thay đổi.
Thêm phần tử vào cuối trong Slice.
Bạn có thể thêm các phần tử vào cuối của một slice bằng cách sử dụng hàm append()
trong Go. Dưới đây là một ví dụ:
package main
import "fmt"
func main() {
// Tạo một slice
myslice1 := []int{1, 2, 3, 4, 5, 6}
// In ra thông tin về slice trước khi thêm phần tử
fmt.Printf("myslice1 = %v\n", myslice1)
fmt.Printf("length = %d\n", len(myslice1))
fmt.Printf("capacity = %d\n", cap(myslice1))
// Thêm phần tử vào cuối slice
myslice1 = append(myslice1, 20, 21)
// In ra thông tin về slice sau khi thêm phần tử
fmt.Printf("myslice1 = %v\n", myslice1)
fmt.Printf("length = %d\n", len(myslice1))
fmt.Printf("capacity = %d\n", cap(myslice1))
}
Kết quả thực thi:
myslice1 = [1 2 3 4 5 6]
length = 6
capacity = 6
myslice1 = [1 2 3 4 5 6 20 21]
length = 8
capacity = 12
Trong ví dụ trên, append(myslice1, 20, 21)
thêm hai phần tử 20
và 21
vào cuối của myslice1
. Sau khi thêm, số phần tử của slice tăng lên từ 6 lên 8 và tổng số lượng phần tử có thể chứa của slice có thể đã được tăng lên dựa trên cơ chế tự động mở rộng của append()
(tổng số lượng phần tử có thể chứa mới có thể lớn hơn tổng số lượng phần tử có thể chứa hiện tại).
Thêm tất cả các phần tử của một slice này vào cuối của một slice khác.
Bạn có thể thêm tất cả các phần tử của một slice vào cuối của một slice khác bằng cách sử dụng hàm append()
trong Go. Dưới đây là một ví dụ:
package main
import "fmt"
func main() {
// Tạo hai slice
myslice1 := []int{1, 2, 3}
myslice2 := []int{4, 5, 6}
// Thêm tất cả phần tử của myslice2 vào cuối của myslice1
myslice3 := append(myslice1, myslice2...)
// In ra thông tin về slice sau khi thêm
fmt.Printf("myslice3=%v\n", myslice3)
fmt.Printf("length=%d\n", len(myslice3))
fmt.Printf("capacity=%d\n", cap(myslice3))
}
Kết quả thực thi:
myslice3=[1 2 3 4 5 6]
length=6
capacity=6
Trong ví dụ trên, append(myslice1, myslice2...)
thêm tất cả các phần tử của myslice2
vào cuối của myslice1
. Khi sử dụng ...
sau myslice2
, nó giúp trải rộng các phần tử của myslice2
thành các đối số đơn lẻ của hàm append()
. Sau khi thêm, myslice3
có số phần tử là 6 và tổng số lượng phần tử có thể chứa là 6 vì nó sử dụng tổng số lượng phần tử có thể chứa của myslice1
mà không cần mở rộng.
Thay đổi số phần tử của slice.
Trong slice của Go, bạn có thể thay đổi số phần tử của slice một cách linh hoạt. Dưới đây là một ví dụ:
package main
import "fmt"
func main() {
// Tạo một mảng
arr1 := [6]int{9, 10, 11, 12, 13, 14}
// Tạo một slice từ mảng
myslice1 := arr1[1:5]
fmt.Printf("myslice1 = %v\n", myslice1)
fmt.Printf("length = %d\n", len(myslice1))
fmt.Printf("capacity = %d\n", cap(myslice1))
// Thay đổi số phần tử của slice bằng cách re-slice mảng
myslice1 = arr1[1:3]
fmt.Printf("myslice1 = %v\n", myslice1)
fmt.Printf("length = %d\n", len(myslice1))
fmt.Printf("capacity = %d\n", cap(myslice1))
// Thay đổi số phần tử của slice bằng cách thêm phần tử
myslice1 = append(myslice1, 20, 21, 22, 23)
fmt.Printf("myslice1 = %v\n", myslice1)
fmt.Printf("length = %d\n", len(myslice1))
fmt.Printf("capacity = %d\n", cap(myslice1))
}
Kết quả thực thi:
myslice1 = [10 11 12 13]
length = 4
capacity = 5
myslice1 = [10 11]
length = 2
capacity = 5
myslice1 = [10 11 20 21 22 23]
length = 6
capacity = 10
Trong ví dụ trên, myslice1
được tạo từ mảng arr1
và có số phần tử ban đầu là 4 và tổng số lượng phần tử có thể chứa là 5. Sau đó, chúng ta thay đổi số phần tử của slice bằng cách re-slice mảng (myslice1 = arr1[1:3]
), giảm số phần tử xuống 2. Cuối cùng, chúng ta thay đổi số phần tử của slice bằng cách thêm các phần tử mới (myslice1 = append(myslice1, 20, 21, 22, 23)
) và số phần tử của slice tăng lên thành 6, tổng số lượng phần tử có thể chứa có thể đã được tăng lên dựa trên cơ chế tự động của append()
.
Thêm phần tử vào bộ nhớ memory.
Khi sử dụng slices, Go tải tất cả các phần tử cơ bản vào bộ nhớ. Nếu mảng lớn và bạn chỉ cần một số phần tử, tốt hơn là sao chép những phần tử đó bằng cách sử dụng hàm copy()
.
Hàm copy()
tạo một mảng cơ sở mới chỉ chứa các phần tử cần thiết cho slice. Điều này sẽ giảm bộ nhớ được sử dụng cho chương trình.
Dưới đây là một ví dụ:
package main
import "fmt"
func main() {
// Tạo một slice ban đầu
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
fmt.Printf("numbers = %v\n", numbers)
fmt.Printf("length = %d\n", len(numbers))
fmt.Printf("capacity = %d\n", cap(numbers))
// Tạo một bản sao chỉ với những số cần thiết
neededNumbers := numbers[:len(numbers)-10]
numbersCopy := make([]int, len(neededNumbers))
copy(numbersCopy, neededNumbers)
fmt.Printf("numbersCopy = %v\n", numbersCopy)
fmt.Printf("length = %d\n", len(numbersCopy))
fmt.Printf("capacity = %d\n", cap(numbersCopy))
}
Kết quả thực thi:
numbers = [1 2 3 4 5 6 7 8 9 10 11 12 13 14 15]
length = 15
capacity = 15
numbersCopy = [1 2 3 4 5]
length = 5
capacity = 5
Trong ví dụ trên, neededNumbers
là một slice mới chỉ chứa những số cần thiết, sau đó chúng ta sử dụng hàm copy()
để tạo một slice numbersCopy
có cùng nội dung nhưng có tổng số lượng phần tử có thể chứa (capacity) mới. Tổng số lượng phần tử có thể chứa của slice mới giảm xuống so với tổng số lượng phần tử có thể chứa của slice ban đầu, vì mảng cơ sở mới nhỏ hơn. Điều này làm tăng hiệu suất và giảm lượng bộ nhớ được sử dụng.
Lấy giá trị cuối cùng trong Slices.
Để lấy phần tử cuối cùng của một mảng hoặc slice trong ngôn ngữ lập trình Go, bạn có thể sử dụng chỉ số cuối cùng của mảng hoặc sử dụng hàm len
để lấy chiều dài của slice và truy cập phần tử cuối cùng.
Dưới đây là một ví dụ:
package main
import "fmt"
func main() {
// Khai báo một slice
numbers := []int{1, 2, 3, 4, 5}
// Lấy chiều dài của slice
length := len(numbers)
// Kiểm tra xem slice có phần tử nào không
if length > 0 {
// Lấy phần tử cuối cùng của slice
lastElement := numbers[length-1]
fmt.Println("Last element of the slice:", lastElement)
} else {
fmt.Println("Slice is empty.")
}
}
Trong ví dụ chúng ta sử dụng len
để lấy chiều dài của slice hoặc array, sau đó sử dụng chỉ số length-1
để truy cập phần tử cuối cùng của chúng. Lưu ý rằng trước khi truy cập phần tử cuối cùng, chúng ta cần kiểm tra xem slice hoặc array có phần tử nào không để tránh lỗi khi chiều dài là 0.
So sánh giữa arrays và slices.
Trong Go, cách khai báo array và slice có điểm tương đồng nhất định, nhưng cũng có sự khác biệt quan trọng. Dưới đây là cách khai báo cơ bản cho array và slice trong Go:
- Array.
var arr [5]int // Khai báo một array có độ dài là 5 với kiểu dữ liệu int
arr := [3]string{"a", "b"} // Khai báo và khởi tạo một array có độ dài là 3 với kiểu dữ liệu string
Trường hợp này đang khai báo một array có độ dài cố định. Dấu ba chấm ...
được sử dụng để tự động xác định kích thước của mảng dựa trên số lượng phần tử được cung cấp.
arr := [...]int{1,2,3,4,5,6}
- Slice.
var sli []int // Khai báo một slice
sli = make([]int, 3) // Khởi tạo slice với độ dài là 3
sli = []int{1, 2, 3} // Khởi tạo và gán giá trị cho slice
Hoặc
arr := []int{1,2,3,4,5,6}
Trường hợp này đang khai báo một slice. Dấu ngoặc vuông không có kích thước được cung cấp, và điều này làm cho slice có thể thay đổi độ dài linh hoạt sử dụng hàm append
hoặc thông qua các phép cắt slice.
Lưu ý rằng khi khai báo slice, bạn có thể sử dụng make
để cấp phát bộ nhớ cho slice hoặc sử dụng cú pháp ngắn gọn hơn với dấu ngoặc vuông []
.
- Điểm chung giữa array và slice là cả hai đều sử dụng dấu ngoặc vuông
[]
để định nghĩa kiểu dữ liệu là một mảng hoặc slice.
- Sự khác biệt quan trọng:
- Array có kích thước cố định, không thể thay đổi sau khi đã được khai báo.
- Slice có khả năng thay đổi độ dài (dynamic sizing) và tham chiếu đến một phần của mảng. Slice không cần chỉ định kích thước ngay từ khi khai báo.
Ví dụ về việc mở rộng độ dài của slice:
sli = append(sli, 4, 5) // Mở rộng slice bằng hàm append
Điều quan trọng cần lưu ý là với array, kích thước của nó là cố định và không thể thay đổi sau khi đã được khai báo. Ngược lại, slice có thể thay đổi độ dài và là một tham chiếu đến một phần của mảng. Điều này làm cho slice linh hoạt hơn khi bạn cần làm việc với các tập hợp dữ liệu có thể thay đổi kích thước.
Như vậy mặc dù cú pháp khai báo có sự tương đồng, nhưng tính chất và sử dụng của array và slice trong Go là khác nhau.