Series Golang cơ bản (Phần 11: Mảng và slices)

Chào mừng các bạn đến với phần 11 của series hướng dẫn về Golang. Ở phần này chúng ta sẽ xử lý các mảng và slices trong Go


Mảng

Mảng là tập hợp của các phần tử có cùng một loại. Ví dụ: tập hợp các số nguyên 5, 8, 9, 79, 76 tạo thành một mảng. Nếu các phần tử trong mảng có type (loại) khác nhau Go sẽ không thực thi được, ví dụ một mảng chứa cả chuỗi và số nguyên sẽ không được coi là hợp lệ trong Go.

Khai báo

Một mảng thuộc loại n[T]. n sẽ biểu thị số phần tử có trong mảng và T biểu thị loại của mỗi phần tử. Số lượng phần tử n cũng được coi là một phần của type (Chúng ta sẽ thảo luận chi tiết hơn trong phần này).

Có nhiều cách khác nhau để khai báo mảng. Hãy xem từng phần một.

package main

import (  
    "fmt"
)


func main() {  
    var a [3]int //int array with length 3
    fmt.Println(a)
}

Chạy trên Playground

var a [3] int khai báo một mảng nguyên có độ dài 3. Tất cả các phần tử trong một mảng được tự động gán giá trị bằng không trong mảng dạng này. Trong trường hợp này a là một mảng nguyên và do đó tất cả các phần tử của a được gán cho 0, giá trị của int băng 0. Chạy chương trình trên sẽ có đầu ra [0 0 0].

Chỉ số của một mảng bắt đầu từ 0 và kết thúc ở length - 1. Chúng ta cùng gán một vài giá trị cho mảng trên.

package main

import (  
    "fmt"
)


func main() {  
    var a [3]int //int array with length 3
    a[0] = 12 // array index starts at 0
    a[1] = 78
    a[2] = 50
    fmt.Println(a)

Chạy trên Playground

a[0] gán giá trị cho phần tử đầu tiên của mảng. Chương trình sẽ có đầu ra [12 78 50]

Chúng ta cùng tạo một mảng bàng cách sử dụng short hand declaration (khai báo ngắn).

package main 

import (  
    "fmt"
)

func main() {  
    a := [3]int{12, 78, 50} // short hand declaration to create array
    fmt.Println(a)
}

Chạy trên Playground 

Chương trình trên sẽ in cùng một đầu ra [12 78 50]

Không nhất thiết tất cả các phần tử trong một mảng phải được gán một giá trị trong quá trình khai báo ngắn.

package main

import (  
    "fmt"
)

func main() {  
    a := [3]int{12} 
    fmt.Println(a)
}

Chạy trên Playground 

Dòng thứ 8 trong chương trình trên a := [3]int{12} khai báo một mảng có độ dài là 3 nhưng chỉ cung cấp một giá trị là 12. Hai phần tử còn lại sẽ được tự động gán 0. Chương trình sẽ có đầu ra [12 0 0].

Bạn thậm chí có thể bỏ qua chiều dài của mảng khi khai báo và thay thế nó bằng ... . Điều này được thực hiện như sau.

package main

import (  
    "fmt"
)

func main() {  
    a := [...]int{12, 78, 50} // ... makes the compiler determine the length
    fmt.Println(a)
}

Chạy trên Playground 

Kích thước của mảng là một phần của types (loại). Do đó [5] int[25] int là các loại riêng biệt. Chính vì điều này, mảng không thể thay đổi kích cỡ. Đừng lo lắng về hạn chế này vì các slices tồn tại để khắc phục điều đó.

package main

func main() {  
    a := [3]int{5, 78, 8}
    var b [5]int
    b = a //not possible since [3]int and [5]int are distinct types
}

Chạy trên Playground 

Ở dòng thứ 6 của chương trình trên, chúng tôi đang cố gắng để gán một biến kiểu [3] int cho một biến kiểu [5] int mà không hợp lệ, do đó sẽ bị báo lỗi main.go:6: cannot use a (type [3]int) as type [5]int in assignment.

Mảng là một kiểu giá trị

Mảng trong Go là các loại giá trị và không phải là loại tham chiếu. Điều này có nghĩa là khi chúng được gán cho một biến mới, một bản copy của bản gốc sẽ được gán cho biến mới này. Nếu thay đổi được thực hiện trên biến mới, nó sẽ không xét lại trong mảng ban đầu.

package main

import "fmt"

func main() {  
    a := [...]string{"USA", "China", "India", "Germany", "France"}
    b := a // a copy of a is assigned to b
    b[0] = "Singapore"
    fmt.Println("a is ", a)
    fmt.Println("b is ", b) 
}

Chạy trên Playground 

Dòng 7 trong chương trình trên, một bản sao của a được gán cho b. Ở dòng 8, phần tử đầu tiên của b được đổi thành Singapore. Điều này sẽ không xét lại trong mảng ban đầu a. Chương trình vẫn sẽ output.

a is [USA China India Germany France]  
b is [Singapore China India Germany France]  

Tương tự, khi mảng được truyền cho các hàm như các tham số, chúng được truyền theo giá trị và mảng ban đầu không thay đổi.

package main

import "fmt"

func changeLocal(num [5]int) {  
    num[0] = 55
    fmt.Println("inside function ", num)

}
func main() {  
    num := [...]int{5, 6, 7, 8, 8}
    fmt.Println("before passing to function ", num)
    changeLocal(num) //num is passed by value
    fmt.Println("after passing to function ", num)
}

Chạy trên Playground 

Dòng 13 trong chương trình trên, mảng num được truyền giá trị tới function changeLocal, do đó nó sẽ không bị thay đổi khi gọi hàm. Chương trình sẽ output.

before passing to function  [5 6 7 8 8]  
inside function  [55 6 7 8 8]  
after passing to function  [5 6 7 8 8]  

Độ dài của mảng

Độ dài của mảng được xác định bằng cách truyền mảng như một tham số tới function len 

package main

import "fmt"

func main() {  
    a := [...]float64{67.7, 89.8, 21, 78}
    fmt.Println("length of a is",len(a))

Chạy trên Playground 

Đầu ra của chương trình trên là length of a is 4

Vòng lặp mảng sử dụng range (phạm vi)

Vòng lặp for có thể sử dụng để lặp qua các phần tử trong một mảng

package main

import "fmt"

func main() {  
    a := [...]float64{67.7, 89.8, 21, 78}
    for i := 0; i < len(a); i++ { //looping from 0 to the length of the array
        fmt.Printf("%d th element of a is %.2f\n", i, a[i])
    }
}

Chạy trên Playground 

Chương trình trên sử dụng vòng lặp for để lặp qua các phần tử của mảng bắt đầu từ 0 đến length- 1. Chương trình này hoạt động và sẽ in ra màn hình

0 th element of a is 67.70  
1 th element of a is 89.80  
2 th element of a is 21.00  
3 th element of a is 78.00  

Go cung cấp một cách tốt hơn và ngắn gọn hơn để lặp qua một mảng bằng cách sử dụng range của vòng lặp for. range trả về mục và cả giá trị tại mục đó. Cùng viết lại đoạn code trên bằng cách sử dụng range. Chúng ta cũng sẽ tìm thấy tổng của tất cả các phần tử của mảng.

package main

import "fmt"

func main() {  
    a := [...]float64{67.7, 89.8, 21, 78}
    sum := float64(0)
    for i, v := range a {//range returns both the index and value
        fmt.Printf("%d the element of a is %.2f\n", i, v)
        sum += v
    }
    fmt.Println("\nsum of all elements of a",sum)
}

 Chạy trên Playground

Dòng 8 for i, v := range a của chương trình là dạng range của vòng lặp trên. Nó trả về cả chỉ mục và giá trị tại mục đó. Chúng tôi in các giá trị và tính tổng của tất cả các phần tử của mảng a. Đầu ra của chương trình là

0 the element of a is 67.70  
1 the element of a is 89.80  
2 the element of a is 21.00  
3 the element of a is 78.00

sum of all elements of a 256.5  

Trong trường hợp bạn muốn giữ lại giá trị và bỏ qua chỉ mục, bạn có thể thay thế bằng _

for _, v := range a { //ignores index  
}

Vòng lặp ở trên đã bỏ qua chỉ mục. Tương tự, giá trị cũng có thể bỏ qua.

Mảng đa chiều

Tất cả các mảng chúng ta tạo đến giờ đều là mảng đa chiều

package main

import (  
    "fmt"
)

func printarray(a [3][2]string) {  
    for _, v1 := range a {
        for _, v2 := range v1 {
            fmt.Printf("%s ", v2)
        }
        fmt.Printf("\n")
    }
}

func main() {  
    a := [3][2]string{
        {"lion", "tiger"},
        {"cat", "dog"},
        {"pigeon", "peacock"}, //this comma is necessary. The compiler will complain if you omit this comma
    }
    printarray(a)
    var b [3][2]string
    b[0][0] = "apple"
    b[0][1] = "samsung"
    b[1][0] = "microsoft"
    b[1][1] = "google"
    b[2][0] = "AT&T"
    b[2][1] = "T-Mobile"
    fmt.Printf("\n")
    printarray(b)
}

Chạy trên Playground 

Ở dòng 17, một mảng chuỗi 2 chiều được khai báo ngắn. Dấu phẩy ở cuối dòng 20 rất quan trọng. Thực tế lexer  đã tự động truyền vào dấu chấm phẩy theo quy tắc. Nếu bạn muốn biết thêm về lý do tại sao điều này lại cần thiết, vui lòng truy cập trang https://golang.org/doc/effective_go.html#semicolons

Một mảng 2d b khác được khai báo trong hàng 23 và mỗi chỉ mục lần lượt được thêm một chuỗi. Đây là một cách khác để khởi tạo một mảng 2d.

Function printarray ở dòng 7 sử dụng 2 vòng lặp range để in ra nội dung của mảng 2d. Đầu ra của chương trình trên là

lion tiger  
cat dog  
pigeon peacock 

apple samsung  
microsoft google  
AT&T T-Mobile  

Mặc dù các mảng trông có vẻ gọn nhưng chiều dài của chúng có giới hạn cố định. Độ dài của mảng không thể tăng. 

Thực tế trong Go, slices phổ biến hơn so với mảng thông thường.

Slices

Slices chính là sự thuận tiện, flexible (linh hoạt) và như một vỏ bọc ở đầu của một mảng. Slices không có bất kì dữ liệu nào. Chúng là các tham chiếu đến mảng hiện có.

Tạo một slice 

Một slice có phần tử kiểu T và được biểu thị 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 trên Playground 

Cú pháp a[start: end] tạo ra một slice từ mảng a bắt đầu từ mục start đến end - 1. Vì vậy ở dòng 9 của chương trình trên a[1:4] tạo ra một slice biểu thị mảng a bắt đầu từ các mục 1 đến 3. Do đó, slice b có các giá trị [77 78 79].

Có một cách khác để tạo một slide.

package main

import (  
    "fmt"
)

func main() {  
    c := []int{6, 7, 8} //creates and array and returns a slice reference
    fmt.Println(c)
}

Chạy trên Playground 

Dòng thứ 9 ở chương trình trên 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 trong c.

Sửa đổi một slice

Một slice không sở hữu bất kì một dữ liệu nào của riêng nó. Nó chỉ là đại diện cho một mảng cơ bản. Bất kì một sửa đổi được thực hiện với slice sẽ được xét lại trong mảng cơ bản.

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 trên Playground 

Ở dòng 9 của chương trình trên, chúng tôi tạo dslice từ các mục 2, 3, và 4. Vòng lặp for tăng các giá trị của các mục này lên 1. Khi chúng ta in ra mảng sau khi vòng lặp được thực hiện, chúng ta có thể thấy rằng sự thay đổi của slice được xét lại trong mảng. Đầu ra của chương trình sẽ 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 số slices nằm trong một mảng cơ bản, mỗi thay đổi tạo rã sẽ được xét lại 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 trên Playground 

Dòng 9, ở numa[:] giá trị khi bắt đầu và kết thúc bị mất. Các giá trị mặc định khi bắt đầu và kết thúc là 0 và len(numa) tương ứng. cả 2 slice nums1nums2 đều nằm trong một mảng. Đầu ra sẽ 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]  

Qua đầu ra của chương trình trên ta có thể nhận ra một cách rõ ràng rằng khi các slices nằm chung trong cùng một mảng, mỗi chỉnh sửa sẽ được xét lại trong mảng .

Độ dài và sức chứa của một slice

Độ dài của một slice chính là số lượng của các phần tử có trong slice đó. Sức chứa của slice là chỉ số lượng phần tử cơ bản bắt đầu từ mục mà slice đó được tạo ra.

Chúng ta cùng code để hiểu rõ 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 is 2 and capacity is 6
}

Chạy trên Playground 

Ở chương trình trên, fruitslice đã được tạo ra từ mục 1 và 2 của mảng fruitarray. Do đó độ dài của fruitslice là 2.

Độ dàicủa fruitarray là 7. fruitslice được tạo ra từ mục 1 của fruitarray. Do đó sức chứa của fruitslice không có phần tử nào trong fruitarray bắt đầu từ mục 1, ví dụ từ orange6 kí tự do đó sức chứa của fruitslice là 6. Chương trình này có đầu ra độ dài là 2 và sức chứa là 6.

Một slice có thể re-slice theo sức chứa của nó. Bất cứ điều gì cũng có làm cho chương trình bị lỗi "run time erro".

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 trên Playground 

Dòng thứ 11 của chương trình trên, fruitslice đã re-slice sức chứa của chính nó. Chương trình trên có đầu ra

length of slice 2 capacity 6  
After re-slicing length is 6 and capacity is 6  

Tạo slice sử dụng make

func make([]T, len, cap) []T có thể được sử dụng để tạo slice bằng cách truyền vào type (loại), độ dài và sức chứa. Tham số là tùy chọn và độ dài mặc định. Function make tạo ra một mảng và trả về một slice tham chiếu.

package main

import (  
    "fmt"
)

func main() {  
    i := make([]int, 5, 5)
    fmt.Println(i)
}

Chạy trên Playground 

Các giá trị mặc định là 0 khi một slice được tạo sử dụng make. Chương trình trên sẽ có đầu ra [0 0 0 0 0].

Thêm một slice

Như chúng ta đã biết là 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 được. Slices có sự linh động và các phần tử mới có thể thêm vào bằng cách sử dụng function append. Hàm này có dạng func append(s []T, x ...T) []T

x ...T trong function có nghĩa là hàm sẽ chấp nhận các biến là đối số cho tham số x. Dạng function này được gọi là variadic functions (hàm bất định)

Nếu slices được hỗ trợ bới mảng và mảng có độ dài cố định thì làm cách nào để thay đổi điều đó. Khi các phần tử mới được nối vào thì một mảng mới cũng sẽ được tạo ra. Phần tử của mảng hiện có được sao chép vào mảng mới này và tham chiếu slice mới cho mảng này được trả về. Sức chứa của slice mới hiện tại đã nhiều gấp đôi slice cũ. Cũng hay ho đấy chứ?. Chương trình bên dưới sẽ làm điều này rõ ràng hơn.

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 trên Playground 

ở chương trình trên, sức chứa của cars ban đầu là 3. Chúng tôi thêm vào phần tử tới cars ở dòng 10 và gán vào slice trả về append(cars, "Toyota"). Bây giờ sức chứa của cars đã nhân đôi lên 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  

Giá trị zero (không) của một loại slice là nil. nil slice có độ dài và sức chứa bằng 0. Có thể thêm một 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 trên Playground 

Ở chương trình trên names là nil và chúng tôi đã thêm 3 chuỗi vào names. Đầu ra sẽ là 

slice is nil going to append  
names contents: [John Sebastian Vinay]  

Cũng có thể thêm một slice này đến một slice khác bằng cách sử dụng toán tử ... .Bạn có thể học thêm về toán tử này ở phần hướng dẫn về variadic function  

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 trên Playground 

Ở dòng 10 của chương trình trên food được tạo bằng cách thêm fruits tới veggies. Đầu ra của chương trình là food: [potatoes tomatoes brinjal oranges apples]

Chuyển một slice đến một function

Các slice có thể coi như được biêủ thị bằng một cấu trúc. Trông nó như thế này

type slice struct {  
    Length        int
    Capacity      int
    ZerothElement *byte
}

Một slice gồm chiều dài, sức chứa và một con trỏ đến phần tử con của mảng. Khi một slice được chuyển đến một function, mặc dù nó được truyền theo giá trị, con trỏ sẽ tham chiếu đến cùng một mảng cơ bản. Do đó khi slice chuyển một function như một tham số, các thay đổi bên trong function cũng sẽ hiển thị bên ngoài function. Chúng ta cùng viết một chương trình để kiểm tra 

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 trên Playground  

Function ở dòng 17 của chương trình trên sẽ giàm từng phần tử của slice xuống 2. slice in ra sau khi function được gọi, những thay đổi này sẽ biến mất. Nếu có thể nhớ lại, điều này khác với một mảng trong đó các thay đổi làm nên một mảng trong một hàm sẽ không biến mất bên ngoài function. Đầ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] 

Slices đa chiều

Tương tự như mảng, các 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 trên Playground 

Đầu ra sẽ là

C C++  
JavaScript  
Go Rust  

Tối ưu bộ nhớ

Slices giữ tham chiếu đến mảng phía dưới. Miễn là slice nằm trong bộ nhớ, mảng sẽ không thể tự thu hồi bộ nhớ. Điều này có thể được cân nhắc khi nói đến quản lý bộ nhớ. Cùng giả định rằng chúng ta có một mảng rất dài và 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ý. Điều quan trọng cần lưu ý ở đây là mảng vẫn sẽ ở trong bộ nhớ vì slice tham chiếu nó.

Có một cách để xử lý vấn đề này là sử dụng copy function func copy(dst, src []T) int để tạo một bản sao của slice đó. Bằng cách này chúng tôi có thể sử dụng slice mới và mảng ban đầu để quản lý bộ nhớ 

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 trên Playground 

Ở dòng 9 của chương trình trên, neededCountries := countries[:len(countries)-2] tạo ra một slice của countries chặn 2 phần tử cuối. Dòng 11 sao chép neededCountries đến countriesCpy vả chỉ trả về từ function ở dòng tiếp theo. Ngay lúc này, mảng countries có thể quản lý dung lượng khi neededCountries không còn được tham chiếu nữa. 

Tôi đã biên soạn tất cả các khái niệm mà chún tôi đã thảo luận cho đến nay trong một chương trình duy nhất. Bạn có thể down nó từ github.

Đó là tất cả về mảng và slices. Cảm ơn bạn đã đọc.

Phần tiếp theo - Variadic Function


Được dịch từ: https://golangbot.com/learn-golang-series/

Cấu trúc thư mục một project sử dụng Go-Micro Cấu trúc thư mục một project sử dụng Go-Micro Techmaster team Blog Home Giới thiệu Go Module trong Go v1.11, chào tạm biệt GOPATH! Giới thiệu Go Module trong Go v1.11, chào tạm biệt GOPATH! Tào Quỳnh
Đình Quân