Hướng dẫn này giới thiệu những kiến thức cơ bản về generics trong Go. Với generics, bạn có thể khai báo và sử dụng các hàm hoặc các kiểu dữ liệu được viết để làm việc với bất kỳ kiểu nào trong một tập hợp kiểu do mã gọi cung cấp.
Trong hướng dẫn này, bạn sẽ khai báo hai hàm đơn giản không dùng generics, rồi gom cùng một logic đó vào một hàm generic duy nhất.
Bạn sẽ đi qua các phần sau:
- Tạo một thư mục cho mã nguồn của bạn.
- Thêm các hàm không dùng generics.
- Thêm một hàm generic để xử lý nhiều kiểu dữ liệu.
- Bỏ bớt type argument (đối số kiểu) khi gọi hàm generic.
- Khai báo một type constraint (ràng buộc kiểu).
Lưu ý: Để xem các hướng dẫn khác, hãy xem Tutorials.
Yêu cầu trước khi bắt đầu
- Go. Chúng tôi khuyên bạn dùng phiên bản Go mới nhất để làm theo hướng dẫn này. Xem hướng dẫn cài đặt tại Installing Go.
- Một công cụ để sửa mã nguồn. Bất kỳ trình soạn thảo văn bản nào bạn có cũng đều dùng tốt.
- Một cửa sổ dòng lệnh. Go hoạt động tốt với mọi terminal trên Linux và Mac, và với PowerShell hoặc cmd trên Windows.
Tạo một thư mục cho mã nguồn của bạn
Trước tiên, hãy tạo một thư mục cho mã nguồn bạn sắp viết.
-
Mở cửa sổ dòng lệnh và chuyển về thư mục home của bạn.
Trên Linux hoặc Mac:
$ cdTrên Windows:
C:\> cd %HOMEPATH%Phần còn lại của hướng dẫn sẽ hiển thị dấu $ làm dấu nhắc lệnh. Các câu lệnh bạn dùng cũng sẽ chạy được trên Windows.
-
Từ dấu nhắc lệnh, tạo một thư mục cho mã nguồn của bạn tên là generics.
$ mkdir generics $ cd generics -
Tạo một module để chứa mã nguồn của bạn.
Chạy câu lệnh
go mod init, đưa vào đó module path của mã nguồn mới.$ go mod init example/generics go: creating new go.mod: module example/genericsLưu ý: Với mã nguồn dùng cho môi trường thật, bạn sẽ chỉ định một module path cụ thể hơn, phù hợp với nhu cầu của riêng bạn. Để biết thêm, hãy xem Managing dependencies.
Tiếp theo, bạn sẽ thêm một ít mã đơn giản để làm việc với map.
Thêm các hàm không dùng generics
Ở bước này, bạn sẽ thêm hai hàm, mỗi hàm cộng dồn các giá trị của một map và trả về tổng.
Bạn khai báo hai hàm thay vì một là vì bạn đang làm việc với hai loại map khác nhau: một map lưu các giá trị int64, và một map lưu các giá trị float64.
Viết mã
-
Dùng trình soạn thảo văn bản, tạo một tệp tên là main.go trong thư mục generics. Bạn sẽ viết mã Go của mình trong tệp này.
-
Vào main.go, ở đầu tệp, dán khai báo package sau đây.
package mainMột chương trình độc lập (khác với một thư viện) luôn nằm trong package
main. -
Bên dưới khai báo package, dán hai khai báo hàm sau đây.
// SumInts adds together the values of m. func SumInts(m map[string]int64) int64 { var s int64 for _, v := range m { s += v } return s } // SumFloats adds together the values of m. func SumFloats(m map[string]float64) float64 { var s float64 for _, v := range m { s += v } return s }Trong đoạn mã này, bạn:
- Khai báo hai hàm để cộng dồn các giá trị của một map và trả về tổng.
SumFloatsnhận một map từstringsang các giá trịfloat64.SumIntsnhận một map từstringsang các giá trịint64.
- Khai báo hai hàm để cộng dồn các giá trị của một map và trả về tổng.
-
Ở đầu main.go, bên dưới khai báo package, dán hàm
mainsau đây để khởi tạo hai map và dùng chúng làm đối số khi gọi các hàm bạn đã khai báo ở bước trước.func main() { // Initialize a map for the integer values ints := map[string]int64{ "first": 34, "second": 12, } // Initialize a map for the float values floats := map[string]float64{ "first": 35.98, "second": 26.99, } fmt.Printf("Non-Generic Sums: %v and %v\n", SumInts(ints), SumFloats(floats)) }Trong đoạn mã này, bạn:
- Khởi tạo một map các giá trị
float64và một map các giá trịint64, mỗi map có hai phần tử. - Gọi hai hàm bạn đã khai báo trước đó để tính tổng các giá trị của từng map.
- In kết quả ra màn hình.
- Khởi tạo một map các giá trị
-
Gần đầu main.go, ngay bên dưới khai báo package, import package bạn cần để hỗ trợ đoạn mã vừa viết.
Những dòng mã đầu tiên sẽ trông như sau:
package main import "fmt" -
Lưu main.go.
Chạy mã
Từ dòng lệnh trong thư mục chứa main.go, chạy chương trình.
$ go run .
Non-Generic Sums: 46 and 62.97
Với generics, ở đây bạn có thể viết một hàm thay vì hai. Tiếp theo, bạn sẽ thêm một hàm generic duy nhất cho các map chứa giá trị số nguyên hoặc số thực.
Thêm một hàm generic để xử lý nhiều kiểu dữ liệu
Trong phần này, bạn sẽ thêm một hàm generic duy nhất có thể nhận một map chứa giá trị số nguyên hoặc số thực, qua đó thay thế hiệu quả hai hàm bạn vừa viết bằng một hàm duy nhất.
Để hỗ trợ giá trị thuộc một trong hai kiểu, hàm duy nhất đó cần có cách khai báo những kiểu nào nó hỗ trợ. Ngược lại, mã gọi cần có cách chỉ định rằng nó đang gọi với map số nguyên hay map số thực.
Để làm được điều này, bạn sẽ viết một hàm khai báo các type parameter (tham số kiểu) bên cạnh các tham số hàm thông thường. Những type parameter này làm cho hàm trở thành generic, cho phép nó làm việc với các đối số thuộc nhiều kiểu khác nhau. Bạn sẽ gọi hàm với các type argument và các đối số hàm thông thường.
Mỗi type parameter có một type constraint đóng vai trò như một dạng meta-type cho type parameter đó. Mỗi type constraint chỉ định những type argument hợp lệ mà mã gọi có thể dùng cho type parameter tương ứng.
Dù constraint của một type parameter thường biểu diễn một tập hợp kiểu, tại thời điểm biên dịch type parameter đại diện cho một kiểu duy nhất – kiểu được mã gọi cung cấp dưới dạng type argument. Nếu kiểu của type argument không được constraint của type parameter cho phép, mã sẽ không biên dịch được.
Hãy nhớ rằng một type parameter phải hỗ trợ mọi thao tác mà mã generic thực hiện trên nó. Ví dụ, nếu mã trong hàm của bạn thử thực hiện các thao tác của string (chẳng hạn như lấy phần tử theo chỉ số) trên một type parameter mà constraint của nó bao gồm các kiểu số, thì mã sẽ không biên dịch được.
Trong đoạn mã bạn sắp viết, bạn sẽ dùng một constraint cho phép cả kiểu số nguyên lẫn kiểu số thực.
Viết mã
-
Bên dưới hai hàm bạn đã thêm trước đó, dán hàm generic sau đây.
// SumIntsOrFloats sums the values of map m. It supports both int64 and float64 // as types for map values. func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V { var s V for _, v := range m { s += v } return s }Trong đoạn mã này, bạn:
- Khai báo một hàm
SumIntsOrFloatsvới hai type parameter (nằm trong dấu ngoặc vuông),KvàV, và một đối số dùng các type parameter đó:mthuộc kiểumap[K]V. Hàm trả về một giá trị thuộc kiểuV. - Chỉ định cho type parameter
Ktype constraint làcomparable. Được thiết kế riêng cho những trường hợp như thế này, constraintcomparableđược khai báo sẵn trong Go. Nó cho phép mọi kiểu mà giá trị của nó có thể dùng làm toán hạng của các toán tử so sánh==và!=. Go yêu cầu khoá của map phải so sánh được. Vì vậy, việc khai báoKlàcomparablelà cần thiết để bạn có thể dùngKlàm khoá trong biến map. Nó cũng bảo đảm rằng mã gọi dùng một kiểu hợp lệ cho khoá của map. - Chỉ định cho type parameter
Vmột constraint là hợp (union) của hai kiểu:int64vàfloat64. Dùng|để chỉ định hợp của hai kiểu, nghĩa là constraint này cho phép cả hai kiểu đó. Trình biên dịch sẽ chấp nhận cả hai kiểu làm đối số trong mã gọi. - Chỉ định rằng đối số
mthuộc kiểumap[K]V, trong đóKvàVlà các kiểu đã được chỉ định cho các type parameter. Lưu ý rằng chúng tôi biếtmap[K]Vlà một kiểu map hợp lệ vìKlà một kiểu so sánh được. Nếu chúng tôi không khai báoKlà comparable, trình biên dịch sẽ từ chối tham chiếu tớimap[K]V.
- Khai báo một hàm
-
Trong main.go, bên dưới đoạn mã bạn đã có, dán đoạn mã sau đây.
fmt.Printf("Generic Sums: %v and %v\n", SumIntsOrFloats[string, int64](ints), SumIntsOrFloats[string, float64](floats))Trong đoạn mã này, bạn:
-
Gọi hàm generic bạn vừa khai báo, truyền vào từng map bạn đã tạo.
-
Chỉ định các type argument – những tên kiểu nằm trong dấu ngoặc vuông – để nói rõ những kiểu nào sẽ thay thế các type parameter trong hàm bạn đang gọi.
Như bạn sẽ thấy ở phần tiếp theo, bạn thường có thể bỏ qua các type argument khi gọi hàm. Go thường có thể suy ra chúng từ mã của bạn.
-
In ra các tổng mà hàm trả về.
-
Chạy mã
Từ dòng lệnh trong thư mục chứa main.go, chạy chương trình.
$ go run .
Non-Generic Sums: 46 and 62.97
Generic Sums: 46 and 62.97
Để chạy được mã của bạn, ở mỗi lời gọi trình biên dịch đã thay các type parameter bằng các kiểu cụ thể được chỉ định trong lời gọi đó.
Khi gọi hàm generic bạn đã viết, bạn đã chỉ định các type argument để nói cho trình biên dịch biết dùng kiểu nào thay cho các type parameter của hàm. Như bạn sẽ thấy ở phần tiếp theo, trong nhiều trường hợp bạn có thể bỏ qua các type argument này vì trình biên dịch có thể tự suy ra chúng.
Bỏ bớt type argument khi gọi hàm generic
Trong phần này, bạn sẽ thêm một phiên bản đã chỉnh sửa của lời gọi hàm generic, với một thay đổi nhỏ để đơn giản hoá mã gọi. Bạn sẽ bỏ các type argument, vốn không cần thiết trong trường hợp này.
Bạn có thể bỏ qua type argument trong mã gọi khi trình biên dịch Go có thể suy ra các kiểu bạn muốn dùng. Trình biên dịch suy ra type argument từ kiểu của các đối số hàm.
Lưu ý rằng điều này không phải lúc nào cũng làm được. Ví dụ, nếu bạn cần gọi một hàm generic không có đối số nào, bạn sẽ phải đưa các type argument vào lời gọi hàm.
Viết mã
-
Trong main.go, bên dưới đoạn mã bạn đã có, dán đoạn mã sau đây.
fmt.Printf("Generic Sums, type parameters inferred: %v and %v\n", SumIntsOrFloats(ints), SumIntsOrFloats(floats))Trong đoạn mã này, bạn:
- Gọi hàm generic mà không kèm các type argument.
Chạy mã
Từ dòng lệnh trong thư mục chứa main.go, chạy chương trình.
$ go run .
Non-Generic Sums: 46 and 62.97
Generic Sums: 46 and 62.97
Generic Sums, type parameters inferred: 46 and 62.97
Tiếp theo, bạn sẽ đơn giản hoá hàm hơn nữa bằng cách gom hợp của các kiểu số nguyên và số thực vào một type constraint có thể tái sử dụng, chẳng hạn từ những đoạn mã khác.
Khai báo một type constraint
Trong phần cuối này, bạn sẽ chuyển constraint đã định nghĩa trước đó vào một interface riêng để có thể tái sử dụng nó ở nhiều nơi. Khai báo constraint theo cách này giúp mã gọn gàng hơn, chẳng hạn khi constraint phức tạp hơn.
Bạn khai báo một type constraint dưới dạng một interface. Constraint đó cho phép mọi kiểu hiện thực interface này. Ví dụ, nếu bạn khai báo một interface constraint với ba method, rồi dùng nó với một type parameter trong một hàm generic, thì các type argument dùng để gọi hàm phải có đủ cả ba method đó.
Các interface constraint cũng có thể tham chiếu tới các kiểu cụ thể, như bạn sẽ thấy trong phần này.
Viết mã
-
Ngay phía trên
main, ngay sau các câu lệnh import, dán đoạn mã sau đây để khai báo một type constraint.type Number interface { int64 | float64 }Trong đoạn mã này, bạn:
-
Khai báo kiểu interface
Numberđể dùng làm type constraint. -
Khai báo một hợp của
int64vàfloat64bên trong interface.Về bản chất, bạn đang chuyển phần hợp từ khai báo hàm sang một type constraint mới. Nhờ đó, khi bạn muốn ràng buộc một type parameter chỉ được là
int64hoặcfloat64, bạn có thể dùng type constraintNumbernày thay vì viết raint64 | float64.
-
-
Bên dưới các hàm bạn đã có, dán hàm generic
SumNumberssau đây.// SumNumbers sums the values of map m. It supports both integers // and floats as map values. func SumNumbers[K comparable, V Number](m map[K]V) V { var s V for _, v := range m { s += v } return s }Trong đoạn mã này, bạn:
- Khai báo một hàm generic có cùng logic với hàm generic bạn đã khai báo trước đó, nhưng dùng kiểu interface mới thay cho phần hợp làm type constraint. Cũng như trước, bạn dùng các type parameter cho kiểu của đối số và kiểu trả về.
-
Trong main.go, bên dưới đoạn mã bạn đã có, dán đoạn mã sau đây.
fmt.Printf("Generic Sums with Constraint: %v and %v\n", SumNumbers(ints), SumNumbers(floats))Trong đoạn mã này, bạn:
-
Gọi
SumNumbersvới từng map, in ra tổng các giá trị của mỗi map.Như ở phần trước, bạn bỏ qua các type argument (những tên kiểu trong dấu ngoặc vuông) trong các lời gọi hàm generic. Trình biên dịch Go có thể suy ra type argument từ các đối số khác.
-
Chạy mã
Từ dòng lệnh trong thư mục chứa main.go, chạy chương trình.
$ go run .
Non-Generic Sums: 46 and 62.97
Generic Sums: 46 and 62.97
Generic Sums, type parameters inferred: 46 and 62.97
Generic Sums with Constraint: 46 and 62.97
Kết luận
Làm tốt lắm! Bạn vừa làm quen với generics trong Go.
Các chủ đề gợi ý tiếp theo:
- Go Tour là một phần giới thiệu từng bước rất hay về những kiến thức nền tảng của Go.
- Bạn sẽ tìm thấy những thực hành tốt hữu ích của Go được mô tả trong Effective Go và How to write Go code.
Mã nguồn hoàn chỉnh
Bạn có thể chạy chương trình này trong Go playground. Trên playground, chỉ cần bấm nút Run.
package main
import "fmt"
type Number interface {
int64 | float64
}
func main() {
// Initialize a map for the integer values
ints := map[string]int64{
"first": 34,
"second": 12,
}
// Initialize a map for the float values
floats := map[string]float64{
"first": 35.98,
"second": 26.99,
}
fmt.Printf("Non-Generic Sums: %v and %v\n",
SumInts(ints),
SumFloats(floats))
fmt.Printf("Generic Sums: %v and %v\n",
SumIntsOrFloats[string, int64](ints),
SumIntsOrFloats[string, float64](floats))
fmt.Printf("Generic Sums, type parameters inferred: %v and %v\n",
SumIntsOrFloats(ints),
SumIntsOrFloats(floats))
fmt.Printf("Generic Sums with Constraint: %v and %v\n",
SumNumbers(ints),
SumNumbers(floats))
}
// SumInts adds together the values of m.
func SumInts(m map[string]int64) int64 {
var s int64
for _, v := range m {
s += v
}
return s
}
// SumFloats adds together the values of m.
func SumFloats(m map[string]float64) float64 {
var s float64
for _, v := range m {
s += v
}
return s
}
// SumIntsOrFloats sums the values of map m. It supports both floats and integers
// as map values.
func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
var s V
for _, v := range m {
s += v
}
return s
}
// SumNumbers sums the values of map m. It supports both integers
// and floats as map values.
func SumNumbers[K comparable, V Number](m map[K]V) V {
var s V
for _, v := range m {
s += v
}
return s
}