☰ Mục lục series
Tài liệu Go tiếng Việt
  1. Bắt đầu (Getting started)
    1. Tổng quan
    2. Tải về và cài đặt
    3. Bắt đầu với Go
    4. Quản lý các bản cài đặt Go
    5. Plugin trình soạn thảo và IDE
  2. Dạo quanh Go (A Tour of Go)
    1. Chào mừng
    2. Package, biến và hàm
    3. Điều khiển luồng
    4. Struct, slice và map
    5. Method và interface
    6. Generics
    7. Xử lý đồng thời
  3. Hướng dẫn (Tutorials)
    1. Danh sách hướng dẫn
    2. Tạo một Go module
    3. Gọi mã của bạn từ một module khác
    4. Trả về và xử lý lỗi
    5. Trả về lời chào ngẫu nhiên
    6. Trả về lời chào cho nhiều người
    7. Thêm một test
    8. Biên dịch và cài đặt ứng dụng
    9. Kết luận
    10. Bắt đầu với workspace nhiều module
    11. Phát triển RESTful API với Go và Gin
    12. Truy cập cơ sở dữ liệu quan hệ
    13. Bắt đầu với generics
    14. Bắt đầu với fuzzing
    15. Làm việc với JSON
    16. Tìm và sửa các dependency có lỗ hổng với govulncheck
    17. Tìm và khắc phục phụ thuộc có lỗ hổng với VS Code Go
    18. Viết ứng dụng web
  4. Viết code Go
    1. Cách viết mã Go
    2. Effective Go
    3. Doc Comment trong Go
  5. Module
    1. Phát triển và publish module
    2. Quản lý phụ thuộc
    3. Tổ chức một module Go
    4. Quản lý mã nguồn của module
    5. Cách đánh số phiên bản module
    6. Quy trình phát hành và đánh phiên bản module
    7. Publish một module
    8. Phát triển một bản cập nhật major version
    9. Tham chiếu tệp go.mod
    10. Ngừng hỗ trợ 'go get' cho việc cài đặt tệp thực thi
  6. Cơ sở dữ liệu (Database)
    1. Truy cập cơ sở dữ liệu quan hệ
    2. Mở một handle cơ sở dữ liệu
    3. Thực thi câu lệnh SQL không trả về dữ liệu
    4. Truy vấn dữ liệu
    5. Dùng prepared statement
    6. Thực thi transaction
    7. Huỷ các thao tác đang chạy
    8. Quản lý kết nối
    9. Tránh rủi ro SQL injection
  7. Chẩn đoán và hiệu năng
    1. Chẩn đoán
    2. Hướng dẫn về Go Garbage Collector
    3. Tối ưu hoá dựa trên profile
    4. Data Race Detector
    5. Gỡ lỗi mã Go bằng GDB
    6. Hỗ trợ coverage profiling cho integration test
  8. Bảo mật (Security)
    1. Tổng quan bảo mật
    2. Thực hành tốt về bảo mật cho lập trình viên Go
    3. Quản lý lỗ hổng trong Go
    4. Cơ sở dữ liệu lỗ hổng của Go
    5. Quét lỗ hổng trong IDE
    6. Chính sách CNA của Go
    7. Go Fuzzing
    8. Chi tiết kỹ thuật của Go Fuzzing
    9. Chính sách bảo mật của Go
    10. Threat Model của Go
    11. Các quyết định về bảo mật của Go
    12. Tuân thủ FIPS 140-3
  9. Công cụ và toolchain
    1. Tài liệu về các lệnh
    2. Về lệnh go
    3. Go Toolchains
    4. Telemetry của Go
    5. Cài đặt Go từ mã nguồn
    6. Cài đặt và sử dụng gccgo
  10. Tham khảo
    1. Câu hỏi thường gặp (FAQ)
    2. Go 1 và tương lai của các chương trình Go
    3. Hướng dẫn chuyển đổi sang encoding/json/v2
    4. Hướng dẫn đóng góp
    5. Đóng góp cho frontend gccgo
  11. Phụ lục
    1. Bảng thuật ngữ
Hướng dẫn (Tutorials)

Hướng dẫn: Bắt đầu với generics

Bản dịch từ go.dev/doc/tutorial/generics

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:

  1. Tạo một thư mục cho mã nguồn của bạn.
  2. Thêm các hàm không dùng generics.
  3. Thêm một hàm generic để xử lý nhiều kiểu dữ liệu.
  4. Bỏ bớt type argument (đối số kiểu) khi gọi hàm generic.
  5. 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.

  1. 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:

    $ cd

    Trê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.

  2. 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
  3. 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/generics

    Lư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ã

  1. 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.

  2. Vào main.go, ở đầu tệp, dán khai báo package sau đây.

    package main

    Một chương trình độc lập (khác với một thư viện) luôn nằm trong package main.

  3. 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.
      • SumFloats nhận một map từ string sang các giá trị float64.
      • SumInts nhận một map từ string sang các giá trị int64.
  4. Ở đầu main.go, bên dưới khai báo package, dán hàm main sau đâ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ị float64 và 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.
  5. 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"
  6. 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ã

  1. 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 SumIntsOrFloats với hai type parameter (nằm trong dấu ngoặc vuông), K và V, và một đối số dùng các type parameter đó: m thuộc kiểu map[K]V. Hàm trả về một giá trị thuộc kiểu V.
    • Chỉ định cho type parameter K type constraint là comparable. Được thiết kế riêng cho những trường hợp như thế này, constraint comparable đượ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áo K là comparable là cần thiết để bạn có thể dùng K là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 V một constraint là hợp (union) của hai kiểu: int64 và 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ố m thuộc kiểu map[K]V, trong đó K và V là các kiểu đã được chỉ định cho các type parameter. Lưu ý rằng chúng tôi biết map[K]V là một kiểu map hợp lệ vì K là một kiểu so sánh được. Nếu chúng tôi không khai báo K là comparable, trình biên dịch sẽ từ chối tham chiếu tới map[K]V.
  2. 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ã

  1. 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 int64 và float64 bê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à int64 hoặc float64, bạn có thể dùng type constraint Number này thay vì viết ra int64 | float64.

  2. Bên dưới các hàm bạn đã có, dán hàm generic SumNumbers sau đâ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ề.
  3. 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 SumNumbers vớ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
}

Nội dung gốc: Go Documentation © The Go Authors, giấy phép CC BY 3.0. Bản dịch và ghi chú: Lê Văn Dũng.