Tất tần tật về Generics trong Go

0 0 0

Người đăng: Gung Typical

Theo Viblo Asia

Lập trình tổng quát (Generic programming) là một phong cách hay một mô hình lập trình cho phép lập trình viên viết mã trong các ngôn ngữ có kiểu dữ liệu mạnh (strongly-typed) bằng cách sử dụng các kiểu dữ liệu sẽ được chỉ định sau. Các kiểu này được cung cấp dưới dạng tham số khi khởi tạo.

Generics giúp bạn viết mã áp dụng được cho nhiều kiểu dữ liệu khác nhau mà không phải lặp lại cùng một logic. Điều này cải thiện khả năng tái sử dụng mã, tăng tính linh hoạt và đảm bảo an toàn kiểu (type safety).

Trong Go, generics được triển khai thông qua tham số kiểu (type parameters). Một tham số kiểu là một dạng tham số đặc biệt, dùng làm đại diện cho bất kỳ kiểu dữ liệu nào. Chúng được sử dụng trong định nghĩa hàm, phương thức, và kiểu dữ liệu; và được thay thế bằng kiểu cụ thể khi hàm được gọi.

Trước khi có Generics

Xét ví dụ sau: viết một hàm nhận vào hai tham số kiểu int và trả về giá trị nhỏ hơn trong hai số đó. Rất đơn giản:

func Min(a, b int) int { if a < b { return a } return b
}

Hàm trên hoạt động tốt, nhưng bị giới hạn: tham số chỉ được phép là kiểu int. Nếu yêu cầu mở rộng để hỗ trợ so sánh hai giá trị kiểu float64, ta sẽ phải viết thêm:

func Min(a, b int) int { if a < b { return a } return b
} func MinFloat64(a, b float64) float64 { if a < b { return a } return b
}

Bạn có thể thấy, mỗi lần có yêu cầu mới, ta lại phải lặp lại cùng một logic. Generics chính là giải pháp cho vấn đề này.

import "golang.org/x/exp/constraints" func Min[T constraints.Ordered](x, y T) T { if x < y { return x } return y
}

Cú pháp cơ bản của Generics

// Định nghĩa hàm
func F[T any](p T){...} // Định nghĩa kiểu
type M[T any] []T // Ràng buộc kiểu cụ thể, như any, comparable
func F[T Constraint](p T){..} // Ký hiệu “~” đại diện cho kiểu cơ sở (underlying type)
type E interface { ~string
} // Chỉ định nhiều kiểu
type UnionElem interface { int | int8 | int32 | int64
}

Ký hiệu ~ trong Generics

Trong Go, ký hiệu ~ được dùng để biểu diễn ràng buộc kiểu cơ sở.

Ví dụ, ~int có nghĩa là chấp nhận bất kỳ kiểu nào có kiểu cơ sở là int, bao gồm cả kiểu tự định nghĩa:

type MyInt int type Ints[T int | int32] []T func main() { a := Ints[int]{1, 2} // Hợp lệ b := Ints[MyInt]{1, 2} // Lỗi biên dịch println(a) println(b)
}

MyInt không thỏa mãn int | int32. Cần sửa lại bằng cách thêm ~:

type Ints[T ~int | ~int32] []T

Ràng buộc kiểu (Type Constraints)

  • any: Chấp nhận mọi kiểu dữ liệu
  • comparable: Hỗ trợ toán tử ==!=
  • ordered: Hỗ trợ toán tử so sánh như >, <

Tham khảo thêm tại: https://pkg.go.dev/golang.org/x/exp/constraints

Khi nào nên dùng Generics?

  • Làm việc với container có sẵn trong ngôn ngữ: Khi viết các hàm thao tác với kiểu container như slice, map, channel mà không phụ thuộc vào kiểu phần tử, dùng generics sẽ hữu ích (ví dụ: hàm lấy danh sách các key của bất kỳ kiểu map nào).
  • Cấu trúc dữ liệu dùng chung: Với các cấu trúc dữ liệu như linked list, binary tree... dùng generics giúp tạo ra các cấu trúc tổng quát hơn, hiệu quả hơn và được kiểm tra kiểu tại thời điểm biên dịch.
  • Triển khai phương thức dùng chung: Khi các kiểu khác nhau cần triển khai phương thức giống nhau, generics giúp tái sử dụng logic.
  • Ưu tiên function thay vì method: Khi cần viết hàm so sánh hay thao tác chung, nên viết dưới dạng function hơn là yêu cầu interface có method.

Ví dụ: Sắp xếp slice sử dụng hàm so sánh:

type SliceFn[T any] struct { s []T less func(T, T) bool
} func (s SliceFn[T]) Len() int { return len(s.s)
}
func (s SliceFn[T]) Swap(i, j int) { s.s[i], s.s[j] = s.s[j], s.s[i]
}
func (s SliceFn[T]) Less(i, j int) bool { return s.less(s.s[i], s.s[j])
} func SortFn[T any](s []T, less func(T, T) bool) { sort.Sort(SliceFn[T]{s, less})
}

Khi nào không nên dùng Generics?

  • Không thay thế interface: Nếu chỉ cần gọi các method có sẵn, hãy dùng interface thay vì generics.
  • Không dùng nếu mỗi kiểu có logic khác nhau: Nếu mỗi kiểu cần triển khai method khác nhau, hãy dùng interface và viết riêng, không nên dùng type parameter.
  • Dùng reflection khi cần linh hoạt hơn: Khi phải thao tác với kiểu không có method và xử lý khác biệt rõ ràng, dùng reflection là phù hợp (ví dụ: gói encoding/json).

Nguyên tắc đơn giản

Nếu bạn đang viết đi viết lại cùng một đoạn code chỉ khác kiểu dữ liệu, thì hãy cân nhắc dùng generics.

Nói cách khác, chỉ nên dùng generics khi bạn thấy mình bắt đầu sao chép cùng một logic cho các kiểu khác nhau.

Bình luận

Bài viết tương tự

- vừa được xem lúc

Embedded Template in Go

Getting Start. Part of developing a web application usually revolves around working with HTML as user interface.

0 0 65

- vừa được xem lúc

Tại sao bạn nên học Go ?

. Trong những năm gần đây, có một sự trỗi dậy của một ngôn ngữ lập trình mới Go hay Golang. Không có gì làm cho các developer chúng ta phát cuồng hơn một ngôn ngữ mới, phải không ?.

0 0 42

- vừa được xem lúc

Implement Multi-Connections Download in Go - Part II

Getting Start. In Part I we've looked at how to implement core download functionality as well as define a public API for client to use our package.

0 0 28

- vừa được xem lúc

Sự khó chịu khi mới học Golang và cách giải quyết siêu đơn giản

Gần đây mình đang tìm một giải pháp backend thay thế cho Java Spring (do nó nặng vãi chưởng), sẵn tiện học luôn ngôn ngữ mới. Bỏ ra một ngày để tìm hiểu, khảo sát vài ngôn ngữ backend, cuối cùng mình

0 0 62

- vừa được xem lúc

Serverless Series (Golang) - Bài 9 - Codepipeline Notification with AWS Chatbot and AWS SNS

Giới thiệu. Chào các bạn tới với series về Serverless, ở bài trước chúng ta đã tìm hiểu về cách dựng CI/CD với Codepipeline.

0 0 30

- vừa được xem lúc

Golang và Goroutines

Giới thiệu. Dạo gần đây mình có tìm hiểu về Go hay còn được gọi là Golang thì mình thấy Go có đặc điểm nổi bật là có tốc độ xử lý nhanh và hỗ trợ xử lý đa luồng (concurrency) rất tốt với Goroutines.

0 0 54