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.

Function biến thiên là gì?

Nói chung, các function chỉ chấp nhận số lượng đối số cố định. Function biến thiên (function variadic) là một function chấp nhận số lượng đối số không cố định. Nếu tham số cuối cùng của định nghĩa function có tiền tố là dấu ba chấm ... , thì function có thể chấp nhận bất kỳ số lượng đối số nào cho tham số đó.

Chỉ tham số cuối cùng của một function mới có thể áp dụng. Chúng ta sẽ tìm hiểu lý do tại sao lại như vậy trong phần tiếp theo của hướng dẫn này.

Cú pháp khai báo function biến thiên

func hello(a int, b ...int) {  
}

Trong function trên, tham số b là tham số biến thiên vì nó có tiền tố là dấu ba chấm và nó có thể chấp nhận bất kỳ số lượng đối số nào. Function này có thể được gọi bằng cú pháp sau:

hello(1, 2) //passing one argument "2" to b  
hello(5, 6, 7, 8, 9) //passing arguments "6, 7, 8 and 9" to b  

Trong đoạn mã trên, ở dòng số 1 chúng ta gọi function hello với một đối số 2 cho tham số b và chúng ta truyền bốn đối số 6, 7, 8, 9 cho tham số b ở dòng tiếp theo.

Cũng có thể không truyền đối số cho một function biến thiên.

hello(1)  

Trong đoạn mã trên, chúng ta gọi function hello không có đối số cho b. Điều này vẫn làm việc tốt.

Bây giờ tôi đoán bạn đã hiểu tại sao tham số biến thiên chỉ nên ở cuối cùng.

Hãy thử tạo tham số biến thiên ở vị trí đầu tiên của function hello.

Cú pháp sẽ như thế này:

func hello(b ...int, a int) {  
}

Trong function trên, không thể truyền các đối số cho tham số a vì bất kỳ đối số nào chúng ta truyền vào sẽ được gán cho tham số đầu tiên b vì nó là tham số biến thiên.

Do đó, các tham số biến thiên chỉ có thể xuất hiện ở vị trí cuối cùng trong định nghĩa function. Function trên sẽ không biên dịch được với lỗi:

syntax error: cannot use ... with non-final parameter b

Tìm hiểu cách hoạt động của function biến thiên

Hãy tạo một function biến thiên của riêng chúng ta. Chúng ta sẽ viết một chương trình đơn giản để tìm xem một số nguyên có tồn tại trong danh sách đầu vào của các số nguyên hay không.

package main

import (  
    "fmt"
)

func find(num int, nums ...int) {  
    fmt.Printf("type of nums is %T\n", nums)
    found := false
    for i, v := range nums {
        if v == num {
            fmt.Println(num, "found at index", i, "in", nums)
            found = true
        }
    }
    if !found {
        fmt.Println(num, "not found in ", nums)
    }
    fmt.Printf("\n")
}
func main() {  
    find(89, 89, 90, 95)
    find(45, 56, 67, 45, 90, 109)
    find(78, 38, 56, 98)
    find(87)
}

Chạy chương trình trong playground

Trong chương trình trên, func find(num int, nums ...int) ở dòng số 7, chấp nhận số lượng đối số thay đổi cho tham số nums. Bên trong function find, kiểu của nums[]int một slice kiểu số nguyên.

Cách hoạt động của function biến thiên là bằng cách chuyển đổi số lượng đối số có thể thay đổi thành một phần của loại tham số biến thiên. Ví dụ, tại dòng số 22 của chương trình trên, đối số variadic của function find là 89, 90, 95.

Function find mong đợi một tham số biến thiên kiểu int. Do đó, ba đối số này sẽ được trình biên dịch chuyển đổi thành một slice kiểu int []int{89, 90, 95} và sau đó nó sẽ được truyền cho function find.

Tại dòng số 10, vòng lặp for duyệt qua slice nums và in ra vị trí của num nếu nó có trong slice. Nếu không, nó sẽ in rằng số không được tìm thấy.

Kết quả chương trình trên như sau:

type of nums is []int  
89 found at index 0 in [89 90 95]

type of nums is []int  
45 found at index 2 in [56 67 45 90 109]

type of nums is []int  
78 not found in  [38 56 98]

type of nums is []int  
87 not found in  []  

Tại dòng số 25 của chương trình trên, lệnh gọi function find chỉ có một đối số. Chúng ta chưa chuyển bất kỳ đối số nào cho tham số biến thiên nums ...int. Như đã thảo luận trước đó, điều này hoàn toàn hợp pháp và trong trường hợp này, nums sẽ là một slice nil có độ dài và dung lượng bằng 0.

Tham số slice so với tham số biến thiên

Chúng ta chắc chắn sẽ có một câu hỏi lởn vởn trong đầu bạn bây giờ. Trong phần trên, chúng ta đã biết rằng các tham số biến thiên cho một function trên thực tế được chuyển đổi một slice.

Vậy thì tại sao chúng ta lại cần các kiểu khai báo function khác nhau khi chúng ta có thể đạt được điều đó bằng cách sử dụng slice?

Tôi đã viết lại chương trình ở trên bằng cách sử dụng slice như bên dưới.

package main

import (  
    "fmt"
)

func find(num int, nums []int) {  
    fmt.Printf("type of nums is %T\n", nums)
    found := false
    for i, v := range nums {
        if v == num {
            fmt.Println(num, "found at index", i, "in", nums)
            found = true
        }
    }
    if !found {
        fmt.Println(num, "not found in ", nums)
    }
    fmt.Printf("\n")
}
func main() {  
    find(89, []int{89, 90, 95})
    find(45, []int{56, 67, 45, 90, 109})
    find(78, []int{38, 56, 98})
    find(87, []int{})
}

Chạy chương trình trong playground

Sau đây là những ưu điểm của việc sử dụng tham số biến thiên thay vì đối số slice.

  1. Không cần tạo một slice trong mỗi lần gọi function. Nếu bạn nhìn vào chương trình ở trên, chúng ta đã tạo các slice mới trong mỗi lần gọi function ở dòng số 22, 23, 24 và 25. Việc tạo slice bổ sung này có thể tránh được khi sử dụng function biến thiên.
  2. Tại dòng số 25 của chương trình trên, chúng ta đang tạo một slice trống chỉ để đáp ứng chữ ký của function find. Điều này hoàn toàn không cần thiết trong trường hợp sử dụng function biến thiên. Dòng này chỉ có thể là find(87) khi function biến thiên được sử dụng.
  3. Cá nhân tôi cảm thấy rằng chương trình với function biến thiên dễ đọc hơn chương trình sử dụng slice :)

Function append là một function biến thiên

Bạn đã bao giờ tự hỏi làm thế nào function append trong thư viện chuẩn được sử dụng để nối các giá trị vào một slice chấp nhận bất kỳ số lượng đối số nào. Đó là bởi vì nó là một function biến thiên.

func append(slice []Type, elems ...Type) []Type  

Trên đây là định nghĩa của function append. Trong định nghĩa này elems là một tham số biến thiên. Do đó, append có thể chấp nhận một số lượng đối số không cố định.

Truyền slice vào một function biến thiên

Hãy thử truyền slice cho một function biến thiên và tìm hiểu điều gì sẽ xảy ra từ ví dụ dưới đây:

package main

import (  
    "fmt"
)

func find(num int, nums ...int) {  
    fmt.Printf("type of nums is %T\n", nums)
    found := false
    for i, v := range nums {
        if v == num {
            fmt.Println(num, "found at index", i, "in", nums)
            found = true
        }
    }
    if !found {
        fmt.Println(num, "not found in ", nums)
    }
    fmt.Printf("\n")
}
func main() {  
    nums := []int{89, 90, 95}
    find(89, nums)
}

Chạy chương trình trong playground

Tại dòng số 23, chúng ta đang truyền một slice cho một function biến thiên.

Điều này sẽ không hoạt động. Chương trình trên sẽ không thành công với lỗi biên dịch:

./prog.go:23:10: cannot use nums (type []int) as type int in argument to find

Tại sao điều này không hoạt động? Chữ ký của function find được cung cấp ở bên dưới:

func find(num int, nums ...int)  

Theo định nghĩa của một function biến thiên, nums ...int có nghĩa là nó sẽ chấp nhận một số lượng đối số không cố định kiểu int.

Tại dòng số 23 của chương trình ở trên, nums là slice []int được truyền cho function find đang mong đợi một tham số biến thiên kiểu int.

Như chúng ta đã thảo luận, các đối số khác nhau này sẽ được chuyển đổi thành một slice kiểu int vì function find mong đợi các tham số biến thiên kiểu int.

Trong trường hợp này, nums đã là một slice []int và trình biên dịch cố gắng tạo mới một slice []int tức là trình biên dịch cố gắng làm:

find(89, []int{nums})  

cái nào sẽ thất bại vì nums là một slice []int và không phải một số lượng đối số không cố định kiểu int.

Vậy có cách nào để truyền một slice đến một function biến thiên không? Câu trả lời là .

Có một cú pháp có thể được sử dụng để truyền slice vào một function biến thiên. Bạn phải kết thúc slice bằng dấu ba chấm. Nếu làm điều này, slice được truyền trực tiếp đến function mà không cần tạo slice mới.

Trong chương trình trên nếu bạn thay thế find(89, nums) ở dòng số 23 thành find(89, nums...), chương trình sẽ biên dịch và in kết quả sau:

type of nums is []int  
89 found at index 0 in [89 90 95]  

Đây là chương trình đầy đủ để bạn tham khảo.

package main

import (  
    "fmt"
)

func find(num int, nums ...int) {  
    fmt.Printf("type of nums is %T\n", nums)
    found := false
    for i, v := range nums {
        if v == num {
            fmt.Println(num, "found at index", i, "in", nums)
            found = true
        }
    }
    if !found {
        fmt.Println(num, "not found in ", nums)
    }
    fmt.Printf("\n")
}
func main() {  
    nums := []int{89, 90, 95}
    find(89, nums...)
}

Chạy chương trình trong playground

Bonus

Chỉ cần chắc chắn rằng bạn biết mình đang làm gì khi sửa đổi một slice bên trong một function biến thiên.

Hãy xem một ví dụ đơn giản.

package main

import (  
    "fmt"
)

func change(s ...string) {  
    s[0] = "Go"
}

func main() {  
    welcome := []string{"hello", "world"}
    change(welcome...)
    fmt.Println(welcome)
}

Chạy chương trình trong playground

Bạn nghĩ đầu ra của chương trình trên là gì? Nếu bạn nghĩ nó sẽ là [Go world] Xin chúc mừng bạn! Bạn đã hiểu function biến thiên  và slice. Nếu bạn hiểu sai, không có vấn đề gì lớn, hãy để tôi giải thích cách chúng ta có được đầu ra này.

Tại dòng số 13 của chương trình trên, chúng ta đang sử dụng cú ... và truyền slice làm tham số biến thiên cho function change.

Như chúng ta đã thảo luận, nếu ... được sử dụng, thì chính slice welcome sẽ được truyền như một đối số mà không cần tạo slice mới. Do đó welcome sẽ được truyền cho function change dưới dạng đối số.

Bên trong function biến thiên, phần tử đầu tiên của slice được thay đổi thành Go. Do đó chương trình này xuất ra:

[Go world]

Đây là một chương trình khác để hiểu function biến thiên.

package main

import (  
    "fmt"
)

func change(s ...string) {  
    s[0] = "Go"
    s = append(s, "playground")
    fmt.Println(s)
}

func main() {  
    welcome := []string{"hello", "world"}
    change(welcome...)
    fmt.Println(welcome)
}

Chạy chương trình trong playground

Tôi sẽ để nó như một bài tập để bạn tìm ra cách hoạt động của chương trình trên :).

Trong hướng dẫn tiếp theo, chúng ta sẽ tìm hiểu map trong Go:

Map trong Go
Trong hướng dẫn này, chúng ta sẽ tìm hiểu map là gì, cú pháp khai báo map, thêm phần tử, xóa phần tử, duyệt các phần tử, ... của map trong Go.

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. Chúc bạn ngày mới tốt lành.

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.