1. 介紹
泛型可能是1.18版本最大的更新了,畢竟官方文檔都寫在了第一條
泛型的基本介紹就不寫了,c#中有最優雅的泛型實現,可以去簡單看看
全面的泛型概述可見泛型提案 https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md
更多細節可見官方文檔 https://go.dev/ref/spec
下面只搬運一下對泛型的簡單介紹
- 函數和類型聲明的語法接受類型參數
- 可以通過方括號中的類型參數列表來實例化參數化函數和類型
- 接口類型的語法現在允許嵌入任意類型以及Union和〜T類型元素。這些接口只能用作類型約束。接口現在可以定義一組類型和一組方法
- 新的預聲明標識符any是空接口的別名,可以使用any代替interface{}
- 新的預聲明標識符comparable表示可以使用==或!=做比較的所有類型的一個接口,它可以被用作類型約束
有三個實驗性的package在使用泛型
2. any關鍵字
any其實就是interface{}的別名
type any = interface {}
以下代碼雖然不是泛型,但用 Go 1.18 可以正常運行,證明 any 和 interface{} 是一樣的:
// 這里的 any 並非泛型的約束,而是類型
func test(x any) any {
return x
}
func main() {
fmt.Println(test( "a" ))
}
泛型中,any 換為 interface{} 也可以:
// 注意其中的 T interface{},正常應該使用 T any
func Print[T interface {}](s ...T) {
for _, v := range s {
fmt.Print(v)
}
}
func main() {
Print( "Hello, " , "playground\n" )
}
可見,之所以引入 any 關鍵字,主要是讓泛型修飾時短一點,少一些括號。any 比 interface{} 會更清爽
3. 泛型包slices
目前,slices 包有 14 個函數,可以分成幾組:
- slice比較
- 元素查找
- 修改slice
- 克隆slice
其中,修改slice分為插入元素、刪除元素、連續元素去重、slice擴容和縮容
3.1 slice比較
比較兩個 slice 中的元素,細分為是否相等和普通比較:
func Equal[E comparable](s1, s2 []E) bool
func EqualFunc[E1, E2 any](s1 []E1, s2 []E2, eq func (E1, E2) bool) bool
func Compare[E constraints.Ordered](s1, s2 []E) int
func CompareFunc[E1, E2 any](s1 []E1, s2 []E2, cmp func (E1, E2) int) int
其中 comparable 約束是語言實現的(因為很常用),表示可比較約束(相等與否的比較)。主要,其中的 E、E1、E2 等,只是泛型類型表示,你定義時,可以用你喜歡的,比如 T、T1、T2 等。
看一個具體的實現:
func Equal[E comparable](s1, s2 []E) bool {
if len(s1) != len(s2) {
return false
}
for i, v1 := range s1 {
v2 := s2[i]
if v1 != v2 {
return false
}
}
return true
}
3.2 元素查找
在 slice 中查找某個元素,分為普通的所有查找和包含判斷:
func Index[E comparable](s []E, v E) int
func IndexFunc[E any](s []E, f func (E) bool) int
func Contains[E comparable](s []E, v E) bool
其中,IndexFunc 的類型參數沒有使用任何約束(即用的 any),說明查找是通過 f 參數進行的,它的實現如下:
func IndexFunc[E any](s []E, f func (E) bool) int {
for i, v := range s {
if f(v) {
return i
}
}
return -1
}
參數 f 是一個函數,它接收一個參數,類型是 E,是一個泛型,和 IndexFunc 的第一個參數類型 []E 的元素類型保持一致即可,因此可以直接將遍歷 s 的元素傳遞給 f
3.3 修改slice
一般不建議做相關操作,因為性能較差。如果有較多這樣的需求,可能需要考慮更換數據結構
// 往 slice 的位置 i 處插入元素(可以多個)
func Insert[S ~[]E, E any](s S, i int, v ...E) S
// 刪除 slice 中 i 到 j 的元素,即刪除 s[i:j] 元素
func Delete[S ~[]E, E any](s S, i, j int) S
// 將連續相等的元素替換為一個,類似於 Unix 的 uniq 命令。Compact 修改切片的內容,它不會創建新切片
func Compact[S ~[]E, E comparable](s S)
func CompactFunc[S ~[]E, E any](s S, eq func (E, E) bool) S
// 增加 slice 的容量,至少增加 n 個
func Grow[S ~[]E, E any](s S, n int) S
// 移除沒有使用的容量,相當於縮容
func Clip[S ~[]E, E any](s S) S
以上類型約束都包含了兩個:
- S ~[]E:表明這是一個泛型版 slice,這是對 slice 的約束。注意 [] 前面的
~
,表明支持自定義 slice 類型,如 type myslice []int - E any 或 E comparable:對上面 slice 元素類型的約束。
3.4 克隆slice
獲得 slice 的副本,會進行元素拷貝,注意,slice 中元素的拷貝是淺拷貝,非值類型不會深拷貝。
func Clone[S ~[]E, E any](s S) S {
// Preserve nil in case it matters.
if s == nil {
return nil
}
return append(S([]E{}), s...)
}
3.5 總結
因為泛型的存在,相同的功能對於不同類型的slice可以少寫一份代碼,如果想使用slice泛型的相關操作,建議復制golang.org/x/exp中的函數進行使用或修改
4. 泛型包maps
目前maps包只有8個函數,實現的功能也比較基礎,大概包含了以下幾種操作類型:
- 清空map
- 拷貝、克隆
- 相等判斷
- kv操作
4.1 清空map
清空map就一個函數,實現起來也非常簡單
func Clear[M ~ map [K]V, K comparable, V any](m M)
4.2 拷貝克隆
其中包含了拷貝和克隆,作用稍有不同
func Clone[M ~ map [K]V, K comparable, V any](m M) M
func Copy[M ~ map [K]V, K comparable, V any](dst, src M)
很容易理解克隆和拷貝的區別,克隆就是返回M的一份淺拷貝,兩份副本指向同一個map,已經預感到類似於slice的坑了...,拷貝就是將src map中所有的kv復制到dst map中,如果dst已經存在key,將會被覆蓋。
4.3 相等判斷
主要是判斷兩個兩個map是否相等,從函數簽名可以看出兩個函數的區別
func Equal[M1, M2 ~ map [K]V, K, V comparable](m1 M1, m2 M2) bool
func EqualFunc[M1 ~ map [K]V1, M2 ~ map [K]V2, K comparable, V1, V2 any](m1 M1, m2 M2, eq func (V1, V2) bool) bool
Equal函數限定兩個map的key和value必須是可比較的,也就是說kv必須都可以使用==和!=做判斷,而EqualFunc限定兩個map的key必須是可比較的,而value的比較按照eq函數定義的比較規則
4.4 kv操作
kv操作包括了刪除指定kv、返回所有的key、返回所有的value
func DeleteFunc[M ~ map [K]V, K comparable, V any](m M, del func (K, V) bool)
func Keys[M ~ map [K]V, K comparable, V any](m M) []K
func Values[M ~ map [K]V, K comparable, V any](m M) []V
值得一提的是,keys和values函數返回的元素都是無序的,這三個方法讓我想到了kv數據庫...
4.5 總結
maps提供的函數比slices更簡單一些,關於kv操作個人覺得會有一些應用場景,map的相等判斷在泛型里可能不是很有必要了...
5. 泛型的簡單使用案例
來看一個CRUD接口的定義
type Model interface {
ID() string
}
type DataProvider[MODEL Model] interface {
FindByID(id string) (MODEL, error)
List() ([]MODEL, error)
Update(id string, model MODEL) error
Insert(model MODEL) error
Delete(id string) error
}
現在我們可以定義一個使用DataProvider的HTTP處理程序:
type HTTPHandler[MODEL Model] struct {
dataProvider DataProvider[MODEL]
}
func (h HTTPHandler[MODEL]) FindByID(rw http.ResponseWriter, req *http.Request) {
// validate request here
id = // extract id here
model, err := h.dataProvider.FindByID(id)
if err != nil {
// error handling here
return
}
err = json.NewEncoder(rw).Encode(model)
if err != nil {
// error handling here
return
}
}
我們可以為每個方法實現一次,然后就完成了。我們甚至可以在事務的另一端創建一個客戶端,只需要為基本方法實現一次。
為什么在此使用泛型而不是簡單的我們已經定義的Model接口》
與在此使用Model類型本身相比,泛型有一些優點:
- 使用泛型方法,DataProvider根本不需要知道Model,也不需要實現它。它可以簡單地提供非常強大的具體類型
- 我們可以擴展這個解決方法並使用具體類型進行操作。讓我們看看插入或者更新的驗證器是什么樣子
type HTTPHandler[MODEL any] struct {
dataProvider DataProvider[MODEL]
InsertValidator func (new MODEL) error
UpdateValidator func (old MODEL, new MODEL) error
}
在這個驗證器中是泛型方法的真正優勢所在。我們將解析 HTTP 請求,如果定義了自定義的 InsertValidator,那么我們可以使用它來驗證模型是否檢出,我們可以以類型安全的方式進行並使用具體模型:
type User struct {
FirstName string
LastName string
}
func InsertValidator(u User) error {
if u.FirstName == "" { ... }
if u.LastName == "" { ... }
}
所以我們有一個泛型的處理器,我們可以用自定義回調來調整它,它直接為我們獲取有效負載。沒有類型轉換。沒有 map。只有結構體本身。
參考:
https://go.dev/doc/go1.18
https://pkg.go.dev/golang.org/x/exp
https://mp.weixin.qq.com/s/1Tm_E86cgTrhzZ2Rnm7UjA
https://mp.weixin.qq.com/s/tjHOd6jvGj7tpmf1K4wlYg
https://mp.weixin.qq.com/s/wg5fNsB--5nIgJ6EBBc0PA