Slice trong Go

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.

Tạo một slice

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.

Sửa đổi một slice

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à 0len(numa) tương ứng. Cả hai slice nums1nums2 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 và dung lượng của một slice

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  

Tạo slice bằng cách function make

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]

Thêm phần tử vào slice

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]

Truyền một slice vào một function

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]  

Slice đa chiều

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  

Tối ưu hóa bộ nhớ

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.

Function biến thiên trong Go
Trong hướng dẫn này chúng ta sẽ tìm hiểu function biến thiên là gì, cách khai báo, sử dụng và ích của 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.

Nếu Comdy hữu ích và giúp bạn tiết kiệm thời gian làm việc

Bạn có thể vui lòng đưa Comdy vào whitelist của trình chặn quảng cáo ❤️ để hỗ trợ chúng tôi trong việc trả tiền cho dịch vụ lưu trữ web để duy trì hoạt động của trang web.

Go
Bài Viết Liên Quan:
Phương thức (method) trong Go
Trung Nguyen 02/12/2021
Phương thức (method) trong Go

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.

Struct trong Go
Trung Nguyen 28/11/2021
Struct 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, ...

Con trỏ trong Go
Trung Nguyen 28/11/2021
Con trỏ trong Go

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 trong Go
Trung Nguyen 28/11/2021
Chuỗi trong Go

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.