Generic
Generic hoặc tên học thuật hơn - đa hình tham số hóa (Parameterized Polymorphism) đề cập đến việc tái sử dụng và linh hoạt mã thông qua tham số hóa loại. Trong nhiều ngôn ngữ lập trình đa hình tham số hóa là một khái niệm quan trọng nó cho phép hàm hoặc cấu trúc dữ liệu xử lý dữ liệu loại khác nhau mà không cần viết mã riêng cho mỗi loại. Go ban đầu không có khái niệm generic nhưng kể từ khi ra đời cộng đồng có tiếng nói cao nhất về Go là hy vọng thêm generic cuối cùng Go vào năm 2022 ở phiên bản 1.18 đã thêm hỗ trợ cho generic.
Thiết kế
Go khi thiết kế generic đã cân nhắc các phương án sau
- stenciling: đơn hình hóa điển hình như C++ Rust loại này tạo mã template cho mỗi loại được sử dụng hiệu suất này là tốt nhất hoàn toàn không có chi phí runtime nào hiệu suất bằng gọi trực tiếp nhược điểm là làm chậm đáng kể tốc độ biên dịch (so với Go tự thân) đồng thời do tạo mã cho mỗi loại cũng dẫn đến kích thước file nhị phân biên dịch phình to.
- dictionaries: nó chỉ tạo một bộ mã đồng thời tạo một từ điển loại trong quá trình biên dịch lưu trữ trong đoạn dữ liệu chỉ đọc nó lưu trữ tất cả thông tin loại sẽ được sử dụng khi gọi hàm thì sẽ tra cứu thông tin loại theo từ điển. Cách này sẽ không làm chậm tốc độ biên dịch cũng không gây phình to kích thước nhưng sẽ tạo ra chi phí runtime lớn hiệu suất generic rất kém.
Hai phương pháp trên đại diện cho hai cực đoan Go cuối cùng chọn phương án triển khai là Gcshape stenciling nó là một lựa chọn trung gian đối với các loại có hình dạng bộ nhớ giống nhau (hình dạng xem thế nào do bộ phân phối bộ nhớ quyết định) sẽ sử dụng đơn hình hóa tạo cùng một bộ mã cho chúng ví dụ type Int int và int thực tế là cùng một loại nên dùng chung một bộ mã. Nhưng đối với con trỏ thì sao mặc dù tất cả các loại con trỏ đều cùng một hình dạng bộ nhớ ví dụ *int *Person đều là một hình dạng bộ nhớ nhưng chúng không thể dùng chung một bộ mã vì khi giải tham chiếu mục tiêu loại có bố cục bộ nhớ hoàn toàn khác nhau vì vậy Go đồng thời cũng sử dụng từ điển để lấy thông tin loại trong runtime nên generic của Go cũng tồn tại chi phí runtime.
Dẫn nhập
Trước tiên xem một ví dụ đơn giản.
func Sum(a, b int) int {
return a + b
}Đây là một hàm chức năng rất đơn giản tác dụng là cộng hai số nguyên loại int và trả về kết quả nếu muốn truyền hai số thực loại float64 để tính tổng rõ ràng là không được vì loại không khớp. Một cách giải quyết là định nghĩa thêm một hàm mới như sau
func SumFloat64(a, b float64) float64 {
return a + b
}Vậy vấn đề là nếu phát triển một bộ công cụ toán học tính tổng hai số của tất cả các loại số lẽ nào mỗi loại đều phải viết một hàm sao rõ ràng là không thể hoặc có thể sử dụng loại any cộng với reflection để判断 như sau
func SumAny(a, b any) (any, error) {
tA, tB := reflect.ValueOf(a), reflect.ValueOf(b)
if tA.Kind() != tB.Kind() {
return nil, errors.New("disMatch type")
}
switch tA.Kind() {
case reflect.Int:
case reflect.Int32:
...
}
}Nhưng viết như vậy rất rườm rà và hiệu suất thấp. Nhưng logic của hàm Sum đều giống hệt nhau chỉ là cộng hai số mà thôi lúc này cần dùng đến generic vì vậy tại sao cần generic generic là để giải quyết vấn đề không liên quan đến loại thực thi logic loại vấn đề này không quan tâm loại đưa ra là gì chỉ cần hoàn thành thao tác tương ứng là đủ.
Cú pháp
Cách viết generic như sau
func Sum[T int | float64](a, b T) T {
return a + b
}Tham số hình thức loại: T là một tham số hình thức loại loại cụ thể của tham số hình thức phụ thuộc vào truyền vào loại gì
Ràng buộc loại: int | float64 tạo thành một ràng buộc loại ràng buộc loại này quy định những loại nào được phép ràng buộc quy định phạm vi loại của tham số hình thức loại
Tham số thực tế loại: Sum[int](1,2) chỉ định thủ công loại int int chính là tham số thực tế loại.
Cách sử dụng thứ nhất chỉ định rõ sử dụng loại nào như sau
Sum[int](2012, 2022)Cách sử dụng thứ hai không chỉ định loại để trình biên dịch tự suy luận như sau
Sum(3.1415926, 1.114514)Đây là một slice generic ràng buộc loại là int | int32 | int64
type GenericSlice[T int | int32 | int64] []TỞ đây khi sử dụng không thể bỏ qua tham số thực tế loại
GenericSlice[int]{1, 2, 3}Đây là một bảng băm generic khóa loại phải là có thể so sánh được nên sử dụng interface comparable ràng buộc loại giá trị là V int | string | byte
type GenericMap[K comparable, V int | string | byte] map[K]VSử dụng
gmap1 := GenericMap[int, string]{1: "hello world"}
gmap2 := make(GenericMap[string, byte], 0)Đây là một struct generic ràng buộc loại là T int | string
type GenericStruct[T int | string] struct {
Name string
Id T
}Sử dụng
GenericStruct[int]{
Name: "jack",
Id: 1024,
}
GenericStruct[string]{
Name: "Mike",
Id: "1024",
}Đây là một ví dụ tham số hình thức slice generic
type Company[T int | string, S []T] struct {
Name string
Id T
Stuff S
}
//Cũng có thể như sau
type Company[T int | string, S []int | []string] struct {
Name string
Id T
Stuff S
}Sử dụng
Company[int, []int]{
Name: "lili",
Id: 1,
Stuff: []int{1},
}TIP
Trong struct generic khuyến nghị viết như sau
type Company[T int | string, S int | string] struct {
Name string
Id T
Stuff []S
}SayAble là một interface generic Person thực hiện interface này.
type SayAble[T int | string] interface {
Say() T
}
type Person[T int | string] struct {
msg T
}
func (p Person[T]) Say() T {
return p.msg
}
func main() {
var s SayAble[string]
s = Person[string]{"hello world"}
fmt.Println(s.Say())
}Interface generic
Interface generic có thể cung cấp khả năng ràng buộc trừu tượng tốt hơn dưới đây là một ví dụ
func PrintObj[T fmt.Stringer](s T) {
fmt.Println(s.String())
}
type Person struct {
Name string
}
func (p Person) String() string {
return fmt.Sprintf("Person: %s", p.Name)
}
func main() {
PrintObj(Person{Name: "Alice"})
}Cũng có thể sử dụng interface không generic làm tham số hình thức loại của generic
func Write[W io.Writer](w W, bs []byte) (int, error) {
return w.Write(bs)
}类型断言 generic
Chúng ta có thể sử dụng generic để类型断言 loại any ví dụ như một hàm dưới đây có thể断言 tất cả các loại.
func Assert[T any](v any) (bool, T) {
var av T
if v == nil {
return false, av
}
av, ok := v.(T)
return ok, av
}Tập loại
Sau phiên bản 1.18 định nghĩa của interface trở thành type set interface có tập loại còn được gọi là General interfaces tức interface chung.
An interface type defines a type set
Tập loại chỉ có thể dùng cho ràng buộc loại trong generic không thể dùng cho khai báo loại chuyển đổi loại类型断言. Tập loại là một tập hợp sẽ có tập rỗng tập hợp tập giao tiếp tiếp theo sẽ giải thích ba tình huống này.
Tập hợp
Interface loại SignedInt là một tập loại tập hợp của các loại số nguyên có dấu chính là SignedInt ngược lại SignedInt chính là tập cha của chúng.
type SignedInt interface {
int8 | int16 | int | int32 | int64
}Loại dữ liệu cơ bản như vậy đối với các interface chung khác cũng như vậy
type SignedInt interface {
int8 | int16 | int | int32 | int64
}
type UnSignedInt interface {
uint8 | uint16 | uint32 | uint64
}
type Integer interface {
SignedInt | UnSignedInt
}Tập giao
Tập loại của interface không rỗng là tập giao của tất cả các phần tử của nó dịch sang tiếng người là: nếu một interface chứa nhiều tập loại không rỗng thì interface đó là tập giao của các tập loại này ví dụ như sau
type SignedInt interface {
int8 | int16 | int | int32 | int64
}
type Integer interface {
int8 | int16 | int | int32 | int64 | uint8 | uint16 | uint | uint32 | uint64
}
type Number interface {
SignedInt
Integer
}Tập giao trong ví dụ chắc chắn là SignedInt
func Do[T Number](n T) T {
return n
}
Do[int](2)
DO[uint](2) //Không thể biên dịchTập rỗng
Tập rỗng là không có tập giao ví dụ như sau Integer trong ví dụ dưới là một tập loại rỗng.
type SignedInt interface {
int8 | int16 | int | int32 | int64
}
type UnsignedInt interface {
uint8 | uint16 | uint | uint32 | uint64
}
type Integer interface {
SignedInt
UnsignedInt
}Vì số nguyên không dấu và số nguyên có dấu chắc chắn không có tập giao nên tập giao là một tập rỗng bất kể truyền loại gì trong ví dụ dưới đều không thể biên dịch.
Do[Integer](1)
Do[Integer](-100)Interface rỗng
Interface rỗng và tập rỗng không giống nhau interface rỗng là tập hợp của tất cả các tập loại tức chứa tất cả các loại.
func Do[T interface{}](n T) T {
return n
}
func main() {
Do[struct{}](struct{}{})
Do[any]("abc")
}Tuy nhiên chúng ta thường sử dụng any làm tham số hình thức generic vì interface{} không đẹp.
Loại cơ sở
Khi sử dụng từ khóa type khai báo một loại mới cho dù loại cơ sở của nó nằm trong tập loại khi truyền vào cũng vẫn không thể biên dịch.
type Int interface {
int8 | int16 | int | int32 | int64 | uint8 | uint16 | uint | uint32 | uint64
}
type TinyInt int8
func Do[T Int](n T) T {
return n
}
func main() {
Do[TinyInt](1) // Không thể biên dịch cho dù loại cơ sở của nó thuộc phạm vi tập loại Int
}Có hai cách giải quyết cách thứ nhất là thêm loại này vào tập loại nhưng điều này vô nghĩa vì loại cơ sở của TinyInt và int8 là nhất quán nên có cách giải quyết thứ hai.
type Int interface {
int8 | int16 | int | int32 | int64 | uint8 | uint16 | uint | uint32 | uint64 | TinyInt
}Sử dụng ký hiệu ~ để biểu thị loại cơ sở nếu loại cơ sở của một loại thuộc tập loại này thì loại đó thuộc tập loại này như dưới đây
type Int interface {
~int8 | ~int16 | ~int | ~int32 | ~int64 | ~uint8 | ~uint16 | ~uint | ~uint32 | ~uint64
}Sau khi sửa đổi có thể biên dịch thành công.
func main() {
Do[TinyInt](1) // Có thể biên dịch vì TinyInt nằm trong tập loại Int
}Điểm cần lưu ý
Generic không thể làm loại cơ bản của một loại
Cách viết sau đây là sai tham số hình thức loại T không thể làm loại cơ bản
type GenericType[T int | int32 | int64] TMặc dù cách viết sau đây được phép nhưng vô nghĩa và có thể gây ra vấn đề tràn số nên không khuyến nghị
type GenericType[T int | int32 | int64] intLoại generic không thể sử dụng类型断言
Sử dụng类型断言 đối với loại generic sẽ không thể biên dịch generic muốn giải quyết là không liên quan đến loại nếu một vấn đề cần làm logic khác nhau theo loại khác nhau thì根本 không nên sử dụng generic nên sử dụng interface{} hoặc any.
func Sum[T int | float64](a, b T) T {
ints,ok := a.(int) // Không được phép
switch a.(type) { // Không được phép
case int:
case bool:
...
}
return a + b
}Struct ẩn danh không hỗ trợ generic
Struct ẩn danh không hỗ trợ generic mã như sau sẽ không thể biên dịch
testStruct := struct[T int | string] {
Name string
Id T
}[int]{
Name: "jack",
Id: 1
}Hàm ẩn danh không hỗ trợ generic tùy chỉnh
Hai cách viết sau đều không thể biên dịch
var sum[T int | string] func (a, b T) T
sum := func[T int | string](a,b T) T{
...
}Nhưng có thể sử dụng loại generic đã có ví dụ trong closure
func Sum[T int | float64](a, b T) T {
sub := func(c, d T) T {
return c - d
}
return sub(a,b) + a + b
}Không hỗ trợ phương thức generic
Phương thức không thể có tham số hình thức generic nhưng receiver có thể có tham số hình thức generic. Mã như sau sẽ không thể biên dịch
type GenericStruct[T int | string] struct {
Name string
Id T
}
func (g GenericStruct[T]) name[S int | float64](a S) S {
return a
}Tập loại không thể làm tham số thực tế loại
Bất kỳ interface nào có tập loại đều không thể làm tham số thực tế loại.
type SignedInt interface {
int8 | int16 | int | int32 | int64
}
func Do[T SignedInt](n T) T {
return n
}
func main() {
Do[SignedInt](1) // Không thể biên dịch
}Vấn đề tập giao trong tập loại
Đối với loại không interface trong tập hợp loại không thể có tập giao ví dụ TinyInt và ~int8 trong ví dụ dưới có tập giao.
type Int interface {
~int8 | ~int16 | ~int | ~int32 | ~int64 | ~uint8 | ~uint16 | ~uint | ~uint32 | ~uint64 | TinyInt // Không thể biên dịch
}
type TinyInt int8Nhưng đối với interface loại thì cho phép có tập giao như ví dụ dưới
type Int interface {
~int8 | ~int16 | ~int | ~int32 | ~int64 | ~uint8 | ~uint16 | ~uint | ~uint32 | ~uint64 | TinyInt // Có thể biên dịch
}
type TinyInt interface {
int8
}Tập loại không thể trực tiếp hoặc gián tiếp thêm chính nó vào tập hợp
Trong ví dụ dưới Floats trực tiếp thêm chính nó vào tập hợp mà Double lại thêm Floats nên lại gián tiếp thêm chính nó vào.
type Floats interface { // Mã không thể biên dịch
Floats | Double
}
type Double interface {
Floats
}Interface comparable không thể thêm vào tập loại
Tương tự cũng không thể thêm vào ràng buộc loại nên cơ bản là sử dụng riêng lẻ.
func Do[T comparable | Integer](n T) T { //Không thể biên dịch
return n
}
type Number interface { // Không thể biên dịch
Integer | comparable
}
type Comparable interface { // Có thể biên dịch nhưng vô nghĩa
comparable
}Tập phương thức không thể thêm vào tập loại
Bất kỳ interface nào chứa phương thức đều không thể thêm vào tập hợp loại
type I interface {
int | fmt.Stringer // cannot use fmt.Stringer in union (fmt.Stringer contains methods)
}Nhưng chúng có thể làm tập giao nhưng làm như vậy không có ý nghĩa gì
type I interface {
int
fmt.Stringer
}Sử dụng
Cấu trúc dữ liệu là tình huống sử dụng phổ biến nhất của generic dưới đây mượn hai cấu trúc dữ liệu để展示 cách sử dụng generic.
Hàng đợi
Dưới đây thực hiện một hàng đợi đơn giản bằng generic trước tiên khai báo loại hàng đợi loại phần tử trong hàng đợi có thể là bất kỳ nên ràng buộc loại là any
type Queue[T any] []TTổng cộng chỉ có bốn phương thức Pop Peek Push Size mã như sau.
type Queue[T any] []T
func (q *Queue[T]) Push(e T) {
*q = append(*q, e)
}
func (q *Queue[T]) Pop(e T) (_ T) {
if q.Size() > 0 {
res := q.Peek()
*q = (*q)[1:]
return res
}
return
}
func (q *Queue[T]) Peek() (_ T) {
if q.Size() > 0 {
return (*q)[0]
}
return
}
func (q *Queue[T]) Size() int {
return len(*q)
}Trong phương thức Pop và Peek có thể thấy giá trị trả về là _ T đây là cách sử dụng giá trị trả về có tên nhưng lại sử dụng dấu gạch dưới _ biểu thị đây là ẩn danh điều này không phải là thừa mà là để biểu thị giá trị 0 generic. Do sử dụng generic khi hàng đợi rỗng cần trả về giá trị 0 nhưng do loại không biết không thể trả về loại cụ thể mượn cách trên có thể trả về giá trị 0 generic. Cũng có thể giải quyết vấn đề giá trị 0 bằng cách khai báo biến generic đối với một biến generic giá trị mặc định của nó là giá trị 0 của loại đó như sau
func (q *Queue[T]) Pop(e T) T {
var res T
if q.Size() > 0 {
res = q.Peek()
*q = (*q)[1:]
return res
}
return res
}Heap
Ví dụ hàng đợi ở trên do không có yêu cầu gì đối với phần tử nên ràng buộc loại là any. Nhưng heap thì khác heap là một loại cấu trúc dữ liệu đặc biệt nó có thể判断 giá trị lớn nhất hoặc nhỏ nhất trong thời gian O(1) nên nó có một yêu cầu đối với phần tử đó là phải là loại có thể sắp xếp được nhưng loại có thể sắp xếp built-in chỉ có số và chuỗi nên khi khởi tạo heap cần truyền vào một bộ so sánh tùy chỉnh bộ so sánh do người gọi cung cấp và bộ so sánh cũng phải sử dụng generic như sau
type Comparator[T any] func(a, b T) intDưới đây là một triển khai binary heap đơn giản trước tiên khai báo struct generic vẫn sử dụng any để ràng buộc như vậy có thể lưu trữ bất kỳ loại nào
type Comparator[T any] func(a, b T) int
type BinaryHeap[T any] struct {
s []T
c Comparator[T]
}Triển khai vài phương thức
func (heap *BinaryHeap[T]) Peek() (_ T) {
if heap.Size() > 0 {
return heap.s[0]
}
return
}
func (heap *BinaryHeap[T]) Pop() (_ T) {
size := heap.Size()
if size > 0 {
res := heap.s[0]
heap.s[0], heap.s[size-1] = heap.s[size-1], heap.s[0]
heap.s = heap.s[:size-1]
heap.down(0)
return res
}
return
}
func (heap *BinaryHeap[T]) Push(e T) {
heap.s = append(heap.s, e)
heap.up(heap.Size() - 1)
}
func (heap *BinaryHeap[T]) up(i int) {
if heap.Size() == 0 || i < 0 || i >= heap.Size() {
return
}
for parentIndex := i>>1 - 1; parentIndex >= 0; parentIndex = i>>1 - 1 {
// greater than or equal to
if heap.compare(heap.s[i], heap.s[parentIndex]) >= 0 {
break
}
heap.s[i], heap.s[parentIndex] = heap.s[parentIndex], heap.s[i]
i = parentIndex
}
}
func (heap *BinaryHeap[T]) down(i int) {
if heap.Size() == 0 || i < 0 || i >= heap.Size() {
return
}
size := heap.Size()
for lsonIndex := i<<1 + 1; lsonIndex < size; lsonIndex = i<<1 + 1 {
rsonIndex := lsonIndex + 1
if rsonIndex < size && heap.compare(heap.s[rsonIndex], heap.s[lsonIndex]) < 0 {
lsonIndex = rsonIndex
}
// less than or equal to
if heap.compare(heap.s[i], heap.s[lsonIndex]) <= 0 {
break
}
heap.s[i], heap.s[lsonIndex] = heap.s[lsonIndex], heap.s[i]
i = lsonIndex
}
}
func (heap *BinaryHeap[T]) Size() int {
return len(heap.s)
}
func NewHeap[T any](n int, c Comparator[T]) BinaryHeap[T] {
var heap BinaryHeap[T]
heap.s = make([]T, 0, n)
heap.Comparator = c
return heap
}Sử dụng như sau
type Person struct {
Age int
Name string
}
func main() {
heap := NewHeap[Person](10, func(a, b Person) int {
return cmp.Compare(a.Age, b.Age)
})
heap.Push(Person{Age: 10, Name: "John"})
heap.Push(Person{Age: 18, Name: "mike"})
heap.Push(Person{Age: 9, Name: "lili"})
heap.Push(Person{Age: 32, Name: "miki"})
fmt.Println(heap.Peek())
fmt.Println(heap.Pop())
fmt.Println(heap.Peek())
}Kết quả xuất
{9 lili}
{9 lili}
{10 John}Có generic hỗ trợ loại không thể sắp xếp ban đầu sau khi truyền bộ so sánh cũng có thể sử dụng heap làm như vậy chắc chắn thanh lịch và tiện lợi hơn nhiều so với trước đây sử dụng interface{} để thực hiện chuyển đổi và类型断言 loại.
Pool đối tượng
Pool đối tượng phiên bản gốc chỉ có thể dùng loại any mỗi lần lấy ra đều phải类型断言 sau khi cải tạo đơn giản bằng generic có thể省去 công việc này.
package main
import (
"bytes"
"fmt"
"sync"
)
func NewPool[T any](newFn func() T) *Pool[T] {
return &Pool[T]{
pool: &sync.Pool{
New: func() interface{} {
return newFn()
},
},
}
}
type Pool[T any] struct {
pool *sync.Pool
}
func (p *Pool[T]) Put(v T) {
p.pool.Put(v)
}
func (p *Pool[T]) Get() T {
var v T
get := p.pool.Get()
if get != nil {
v, _ = get.(T)
}
return v
}
func main() {
bufferPool := NewPool(func() *bytes.Buffer {
return bytes.NewBuffer(nil)
})
for range 100 {
buffer := bufferPool.Get()
buffer.WriteString("Hello, World!")
fmt.Println(buffer.String())
buffer.Reset()
bufferPool.Put(buffer)
}
}Tóm tắt
Một đặc điểm lớn của Go là tốc độ biên dịch rất nhanh biên dịch nhanh là vì trình biên dịch làm ít tối ưu trong quá trình biên dịch việc thêm generic sẽ khiến khối lượng công việc của trình biên dịch tăng lên công việc phức tạp hơn điều này tất nhiên sẽ dẫn đến tốc độ biên dịch chậm đi thực tế khi Go1.18 vừa推出 generic quả thực khiến biên dịch chậm hơn nhóm Go vừa muốn thêm generic vừa không muốn làm chậm tốc độ biên dịch quá nhiều nhà phát triển dùng thuận tay trình biên dịch sẽ khó chịu ngược lại trình biên dịch nhẹ nhàng (nhẹ nhàng nhất đương nhiên là đừng có generic) nhà phát triển sẽ khó chịu generic hiện tại là sản phẩm thỏa hiệp giữa hai bên này.
