Slice là một trình bao bọc thuận tiện, linh hoạt và mạnh mẽ bên trên một mảng. Slice không sở hữu bất kỳ dữ liệu nào của riêng nó. Nó chỉ là tham chiếu đến mảng hiện có.
Trong hướng dẫn này, chúng ta sẽ tìm hiểu cách tạo một slice, sửa đổi một slice, thêm phần tử vào slice, slice đa chiều, tối ưu bộ nhớ khi sử dụng slice trong Go.
Một slice có các phần tử thuộc kiểu T được biểu diễn bằng []T
package main
import (
"fmt"
)
func main() {
a := [5]int{76, 77, 78, 79, 80}
var b []int = a[1:4] //creates a slice from a[1] to a[3]
fmt.Println(b)
}
Chạy chương trình trong playground
Cú pháp a[start:end]
tạo một slice từ mảng a
bắt đầu từ chỉ mục start
đến chỉ mục end - 1
. Vì vậy, tại dòng số 9 của chương trình trên a[1:4]
tạo ra một slice của mảng a
bắt đầu từ chỉ mục 1 đến 3. Do đó, slice b
có các giá trị [77 78 79]
.
Hãy xem xét một cách khác để tạo một slice.
package main
import (
"fmt"
)
func main() {
c := []int{6, 7, 8} //creates and array and returns a slice reference
fmt.Println(c)
}
Chạy chương trình trong playground
Trong chương trình trên, tại dòng số 9 c := []int{6, 7, 8}
tạo một mảng với 3 số nguyên và trả về một tham chiếu slice được lưu trữ trong biến c.
Một slice không sở hữu bất kỳ dữ liệu nào của riêng nó. Nó chỉ là một đại diện của mảng bên dưới. Bất kỳ sửa đổi nào được thực hiện đối với slice sẽ được phản ánh trong mảng bên dưới.
package main
import (
"fmt"
)
func main() {
darr := [...]int{57, 89, 90, 82, 100, 78, 67, 69, 59}
dslice := darr[2:5]
fmt.Println("array before",darr)
for i := range dslice {
dslice[i]++
}
fmt.Println("array after",darr)
}
Chạy chương trình trong playground
Ở dòng số 9 của chương trình trên, chúng ta tạo biến dslice
từ các chỉ mục 2, 3, 4 của mảng. Vòng lặp for tăng giá trị trong các chỉ mục này lên một. Khi chúng ta in mảng sau vòng lặp for, chúng ta có thể thấy rằng các thay đổi đối với lát cắt được phản ánh trong mảng. Đầu ra của chương trình là
array before [57 89 90 82 100 78 67 69 59]
array after [57 89 91 83 101 78 67 69 59]
Khi một slice chia sẻ cùng một mảng bên dưới, những thay đổi mà mỗi slice ra sẽ được phản ánh trong mảng.
package main
import (
"fmt"
)
func main() {
numa := [3]int{78, 79 ,80}
nums1 := numa[:] //creates a slice which contains all elements of the array
nums2 := numa[:]
fmt.Println("array before change 1",numa)
nums1[0] = 100
fmt.Println("array after modification to slice nums1", numa)
nums2[1] = 101
fmt.Println("array after modification to slice nums2", numa)
}
Chạy chương trình trong playground
Tại dòng số 9, giá trị bắt đầu và kết thúc trong numa[:]
bị thiếu. Các giá trị mặc định cho bắt đầu và kết thúc là 0
và len(numa)
tương ứng. Cả hai slice nums1
và nums2
chia sẻ cùng một mảng. Đầu ra của chương trình là:
array before change 1 [78 79 80]
array after modification to slice nums1 [100 79 80]
array after modification to slice nums2 [100 101 80]
Từ đầu ra, rõ ràng là khi các slice chia sẻ cùng một mảng. Các sửa đổi được thực hiện đối với bất kỳ slice nào được phản ánh trong mảng.
Chiều dài của slice là số phần tử trong slice. Dung lượng của slice là số phần tử trong mảng bên dưới bắt đầu từ chỉ mục mà từ đó slice được tạo ra.
Hãy viết một số mã để hiểu điều này tốt hơn.
package main
import (
"fmt"
)
func main() {
fruitarray := [...]string{"apple", "orange", "grape", "mango", "water melon", "pine apple", "chikoo"}
fruitslice := fruitarray[1:3]
fmt.Printf("length of slice %d capacity %d", len(fruitslice), cap(fruitslice)) //length of fruitslice is 2 and capacity is 6
}
Chạy chương trình trong playground
Trong chương trình trên, fruitslice
được tạo từ chỉ mục 1 và 2 của fruitarray
. Do đó độ dài của fruitslice
là 2.
Chiều dài của fruitarray
là 7. fruiteslice
được tạo ra từ chỉ mục 1
của fruitarray
. Do đó, dung lượng của fruitslice
bắt đầu từ chỉ mục 1
trong fruitarray
tức là từ orange
đến phần tử cuối cùng là chikoo
và giá trị đó là 6
. Do đó dung lượng của fruitslice
là 6. Chương trình in kết quả sau:
length of slice 2 capacity 6
Slice thể được cắt lại theo dung lượng của nó. Bất cứ điều gì vượt quá điều đó sẽ khiến chương trình gặp lỗi thời gian chạy.
package main
import (
"fmt"
)
func main() {
fruitarray := [...]string{"apple", "orange", "grape", "mango", "water melon", "pine apple", "chikoo"}
fruitslice := fruitarray[1:3]
fmt.Printf("length of slice %d capacity %d\n", len(fruitslice), cap(fruitslice)) //length of is 2 and capacity is 6
fruitslice = fruitslice[:cap(fruitslice)] //re-slicing furitslice till its capacity
fmt.Println("After re-slicing length is",len(fruitslice), "and capacity is",cap(fruitslice))
}
Chạy chương trình trong playground
Tại dòng số 11 của chương trình trên, fruitslice
được cắt lại theo dung lượng của nó. Kết quả chương trình trên,
length of slice 2 capacity 6
After re-slicing length is 6 and capacity is 6
func make([]T, len, cap) []T
có thể được sử dụng để tạo slice bằng cách truyền kiểu, độ dài và dung lượng. Tham số dung lượng là tùy chọn và mặc định là bằng độ dài. Function make
tạo một mảng và trả về một slice tham chiếu tới nó.
package main
import (
"fmt"
)
func main() {
i := make([]int, 5, 5)
fmt.Println(i)
}
Chạy chương trình trong playground
Các giá trị được gán bằng 0 theo mặc định khi một slice được tạo bằng lệnh make. Chương trình trên sẽ xuất ra kết quả:
[0 0 0 0 0]
Như chúng ta đã biết mảng bị giới hạn ở độ dài cố định và độ dài của chúng không thể tăng lên. Chiều dài của slice là động và các phần tử mới có thể được thêm vào slice sử dụng function append
. Định nghĩa của function append là func append(s []T, x ...T) []T
.
x ... T có nghĩa là function chấp nhận số lượng đối số thay đổi cho tham số x. Kiểu function này được gọi là function biến thiên.
Tuy nhiên, một câu hỏi có thể làm phiền bạn. Nếu slice được hỗ trợ bởi các mảng và bản thân các mảng có độ dài cố định thì tại sao slice lại có độ dài động. Điều xảy ra ngầm là, khi các phần tử mới được thêm vào slice, một mảng mới sẽ được tạo ra.
Các phần tử của mảng hiện có được sao chép sang mảng mới này và một tham chiếu slice mới cho mảng mới này được trả về. Dung lượng của slice mới gấp đôi dung lượng của slice cũ.
Khá tuyệt phải không :). Chương trình sau đây sẽ làm rõ mọi thứ.
package main
import (
"fmt"
)
func main() {
cars := []string{"Ferrari", "Honda", "Ford"}
fmt.Println("cars:", cars, "has old length", len(cars), "and capacity", cap(cars)) //capacity of cars is 3
cars = append(cars, "Toyota")
fmt.Println("cars:", cars, "has new length", len(cars), "and capacity", cap(cars)) //capacity of cars is doubled to 6
}
Chạy chương trình trong playground
Trong chương trình trên, dung lượng của cars
ban đầu là 3. Chúng ta thêm một phần tử mới vào cars
ở dòng số 10. Lúc này dung lượng của cars
tăng lên gấp đôi và trở thành 6. Đầu ra của chương trình trên là:
cars: [Ferrari Honda Ford] has old length 3 and capacity 3
cars: [Ferrari Honda Ford Toyota] has new length 4 and capacity 6
Zero value của kiểu slice là nil
. Một slice nil
có độ dài và dung lượng bằng 0. Có thể thêm các giá trị vào một slice nil
bằng cách sử dụng function append
.
package main
import (
"fmt"
)
func main() {
var names []string //zero value of a slice is nil
if names == nil {
fmt.Println("slice is nil going to append")
names = append(names, "John", "Sebastian", "Vinay")
fmt.Println("names contents:",names)
}
}
Chạy chương trình trong playground
Trong chương trình trên names
là slice nil
và chúng ta đã thêm 3 chuỗi vào names
. Đầu ra của chương trình là:
slice is nil going to append
names contents: [John Sebastian Vinay]
Cũng có thể nối một slice này với một slice khác bằng toán tử ...
. Bạn có thể tìm hiểu thêm chi tiết về toán tử này trong hướng dẫn khác.
package main
import (
"fmt"
)
func main() {
veggies := []string{"potatoes","tomatoes","brinjal"}
fruits := []string{"oranges","apples"}
food := append(veggies, fruits...)
fmt.Println("food:",food)
}
Chạy chương trình trong playground
Tại dòng số 10 của chương trình trên food
được tạo ra bằng cách nối fruits
vào veggies
. Đầu ra của chương trình là:
food: [potatoes tomatoes brinjal oranges apples]
Slice có thể được coi là đại diện bởi một kiểu cấu trúc bên trong. Cái này nó thì trông như thế nào:
type slice struct {
Length int
Capacity int
ZerothElement *byte
}
Một slice chứa độ dài, dung lượng và một con trỏ đến phần tử đầu tiên của mảng. Khi một slice được truyền cho một function, mặc dù nó được truyền theo giá trị, biến con trỏ sẽ tham chiếu đến cùng một mảng cơ bản.
Do đó, khi một slice được truyền cho một function dưới dạng tham số, những thay đổi được thực hiện bên trong function sẽ áp dụng lên slice. Hãy viết một chương trình để kiểm tra điều này.
package main
import (
"fmt"
)
func subtactOne(numbers []int) {
for i := range numbers {
numbers[i] -= 2
}
}
func main() {
nos := []int{8, 7, 6}
fmt.Println("slice before function call", nos)
subtactOne(nos) //function modifies the slice
fmt.Println("slice after function call", nos) //modifications are visible outside
}
Chạy chương trình trong playground
Lệnh gọi hàm trong dòng số 17 của chương trình trên trừ mỗi phần tử của slice đi 2. Khi slice được in sau lệnh gọi hàm, những thay đổi này có thể nhìn thấy được.
Nếu bạn có thể nhớ lại, điều này khác với một mảng mà các thay đổi được thực hiện đối với một mảng bên trong một hàm không hiển thị bên ngoài hàm. Đầu ra của chương trình trên là:
slice before function call [8 7 6]
slice after function call [6 5 4]
Tương tự như mảng, slice có thể có nhiều kích thước.
package main
import (
"fmt"
)
func main() {
pls := [][]string {
{"C", "C++"},
{"JavaScript"},
{"Go", "Rust"},
}
for _, v1 := range pls {
for _, v2 := range v1 {
fmt.Printf("%s ", v2)
}
fmt.Printf("\n")
}
}
Chạy chương trình trong playground
Đầu ra của chương trình là:
C C++
JavaScript
Go Rust
Slice giữ một tham chiếu đến mảng bên dưới. Miễn là slice còn trong bộ nhớ, mảng không thể được thu gom rác. Điều này có thể đáng quan tâm khi nói đến quản lý bộ nhớ.
Giả sử rằng chúng ta có một mảng rất lớn và chúng ta chỉ quan tâm đến việc xử lý một phần nhỏ của nó. Từ đó, chúng ta tạo một slice từ mảng đó và bắt đầu xử lý slice đó.
Điều quan trọng cần lưu ý ở đây là mảng sẽ vẫn nằm trong bộ nhớ vì slice tham chiếu đến nó.
Một cách để giải quyết vấn đề này là sử dụng func copy(dst, src []T) int
để tạo một bản sao của slice đó. Bằng cách này, chúng ta có thể sử dụng slice mới và mảng ban đầu có thể được thu gom rác.
package main
import (
"fmt"
)
func countries() []string {
countries := []string{"USA", "Singapore", "Germany", "India", "Australia"}
neededCountries := countries[:len(countries)-2]
countriesCpy := make([]string, len(neededCountries))
copy(countriesCpy, neededCountries) //copies neededCountries to countriesCpy
return countriesCpy
}
func main() {
countriesNeeded := countries()
fmt.Println(countriesNeeded)
}
Chạy chương trình trong playground
Tại dòng số 9 của chương trình trên, neededCountries := countries[:len(countries)-2]
tạo một slice với 2 phần tử cuối cùng của countries
. Dòng số 11 của chương trình trên sao chép neededCountries
tới countriesCpy
và cũng trả về nó từ hàm ở dòng tiếp theo. Bây giờ mảng countries
có thể được thu gom rác vì neededCountries
không còn được tham chiếu nữa.
Trong hướng dẫn tiếp theo, chúng ta sẽ tìm hiểu về function biến thiên trong Go.
Như thường lệ, cảm ơn vì đã đọc. Vui lòng để lại phản hồi và nhận xét có giá trị của bạn.
Bạn có thể vui lòng tắt trình chặn quảng cáo ❤️ để hỗ trợ chúng tôi duy trì hoạt động của trang web.
Trong hướng dẫn này, chúng ta sẽ tìm hiểu phương thức (method) trong Go là gì? Cú pháp khai báo phương thức, so sánh phương thức với hàm, ... trong Go.
Trong hướng dẫn này, chúng ta sẽ tìm hiểu struct là gì, cách khai báo và sử dụng một struct trong Go, struct ẩn danh, so sanh hai struct, ...
Trong hướng dẫn này, chúng ta sẽ tìm hiểu cách con trỏ (pointer) hoạt động trong Go và nó khác với con trỏ trong các ngôn ngữ khác như C và C++ như thế nào.
Chuỗi (string) xứng đáng được đề cập đặc biệt trong Go vì chúng khác biệt trong cách triển khai khi so sánh với các ngôn ngữ khác.