Go 泛型接口的正確打開方式,看看幾個(gè)例子!
自從 Go 引入泛型以來,大家在泛型上最常討論的點(diǎn)之一就是 如何設(shè)計(jì)/使用約束(constraints)。
甚至連 Go 官方都出了多篇博文來頻頻介紹這個(gè)知識點(diǎn)。今天就來源于:
圖片
我們知道泛型的類型參數(shù)可以被限制在 cmp.Ordered、comparable 之類的集合:
圖片
但有一個(gè)容易被忽視的事實(shí)是:接口本身也是類型,它們也能有類型參數(shù)。
這意味著我們可以用 “泛型接口” 來更優(yōu)雅地表達(dá)某些約束關(guān)系,尤其是涉及 元素自身比較、組合約束、指針接收者 這些場景。
下面我們就結(jié)合以下幾個(gè)例子,結(jié)合著來快速理解這個(gè)特性。
從一個(gè)樹開始:比較與約束
假設(shè)我們要寫一個(gè)泛型二叉搜索樹。
最簡單的做法是直接用 cmp.Ordered:
type Tree[E cmp.Ordered] struct {
    root *node[E]
}
func (t *Tree[E]) Insert(element E) {
    t.root = t.root.insert(element)
}
type node[E cmp.Ordered] struct {
    value E
    left  *node[E]
    right *node[E]
}
func (n *node[E]) insert(element E) *node[E] {
    if n == nil {
        return &node[E]{value: element}
    }
    switch {
    case element < n.value:
        n.left = n.left.insert(element)
    case element > n.value:
        n.right = n.right.insert(element)
    }
    return n
}這樣寫很直觀,但有個(gè)問題:它只適用于內(nèi)置可比較的類型(int、string 等)。
如果我想存 time.Time 呢?就用不了了。
常見解法是要求用戶傳一個(gè)比較函數(shù):
type FuncTree[E any] struct {
    root *funcNode[E]
    cmp  func(E, E) int
}
func NewFuncTree[E any](cmp func(E, E) int) *FuncTree[E] {
    return &FuncTree[E]{cmp: cmp}
}
func (t *FuncTree[E]) Insert(element E) {
    t.root = t.root.insert(t.cmp, element)
}這當(dāng)然能跑,但有兩個(gè)缺點(diǎn):
- 必須顯式初始化,不能用零值直接用。
 - 調(diào)用 
cmp是函數(shù)調(diào)用,編譯器不太好內(nèi)聯(lián),性能可能受影響。 
能不能換個(gè)思路?答案就是:泛型接口。
用泛型接口表達(dá) “自比較”
如果我們定義一個(gè)接口:
type Comparer interface {
    Compare(Comparer) int
}看似不錯(cuò),但寫起來很尷尬:方法參數(shù)是 Comparer,每個(gè)類型都要強(qiáng)轉(zhuǎn)回來,非常不 Go。
改進(jìn)后:
type Comparer[T any] interface {
    Compare(T) int
}這樣就好多了。比如 time.Time 有個(gè) Compare(Time) int 方法,自然就實(shí)現(xiàn)了 Comparer[time.Time]。
進(jìn)一步,我們就能寫一個(gè)支持自比較的樹:
type MethodTree[E Comparer[E]] struct {
    root *methodNode[E]
}
func (t *MethodTree[E]) Insert(element E) {
    t.root = t.root.insert(element)
}
type methodNode[E Comparer[E]] struct {
    value E
    left  *methodNode[E]
    right *methodNode[E]
}
func (n *methodNode[E]) insert(element E) *methodNode[E] {
    if n == nil {
        return &methodNode[E]{value: element}
    }
    sign := element.Compare(n.value)
    switch {
    case sign < 0:
        n.left = n.left.insert(element)
    case sign > 0:
        n.right = n.right.insert(element)
    }
    return n
}好處是:
time.Time這種自帶Compare方法的類型直接能用。- 容器仍然支持零值初始化。
 
再和 map 結(jié)合:需要 comparable
如果我們想基于樹實(shí)現(xiàn)一個(gè) OrderedSet,里面加個(gè) map 來做 O(1) 查詢:
type OrderedSet[E Comparer[E]] struct {
    tree     MethodTree[E]
    elements map[E]bool
}
func (s *OrderedSet[E]) Has(e E) bool {
    return s.elements[e]
}結(jié)果編譯報(bào)錯(cuò):
invalid map key type E (missing comparable constraint)因?yàn)?Go 要求 map key 必須是 comparable。
這時(shí)有三種寫法:
- 在 
Comparer接口里直接嵌入comparable。 - 定義一個(gè)新的 
ComparableComparer接口。 - 在 
OrderedSet類型參數(shù)約束里 inline: 
type OrderedSet[E interface {
    comparable
    Comparer[E]
}] struct { ... }哪種方式用,看團(tuán)隊(duì)習(xí)慣,但推薦避免不必要的全局約束,盡量在具體類型上加。
泛型接口不必過度約束
再看一個(gè)常見場景。
定義一個(gè)通用集合接口:
type Set[E any] interface {
    Insert(E)
    Delete(E)
    Has(E) bool
    All() iter.Seq[E]
}這里的泛型參數(shù)最好只約束成 any,而不是強(qiáng)加 comparable 或 Comparer,因?yàn)椴煌瑢?shí)現(xiàn)有不同需求。
例如基于 map 的實(shí)現(xiàn)必須要求 comparable,而基于 Tree 的則不需要。
指針接收者的坑
如果我們用 Set 來實(shí)現(xiàn)一個(gè)去重函數(shù) Unique,會(huì)遇到一個(gè)尷尬點(diǎn):
- 有些實(shí)現(xiàn)(比如 
OrderedSet)的方法用指針接收者。 - 如果我們在泛型函數(shù)里聲明 
var seen S,當(dāng)S是*OrderedSet時(shí),它會(huì)被初始化成nil,調(diào)用就 panic。 
解決方案是:用一個(gè)額外的類型參數(shù)約束“必須是某個(gè)類型的指針”:
type PtrToSet[S, E any] interface {
    *S
    Set[E]
}
func Unique[E, S any, PS PtrToSet[S, E]](...) { ... }這樣寫雖然麻煩,但至少能表達(dá)出“S 的指針實(shí)現(xiàn)了 Set”這個(gè)語義。Go 編譯器還能幫我們推斷最后一個(gè)參數(shù),使用時(shí)還算順手。
是否要約束指針接收者?
這類寫法會(huì)讓函數(shù)簽名變得很 “嚇人”。很多時(shí)候,我們其實(shí)不需要這么復(fù)雜的約束,可以換個(gè)角度:
例如,我們本來寫 Unique 想返回一個(gè) iter.Seq[E],但其實(shí)它內(nèi)部要構(gòu)建一個(gè)集合,結(jié)果已經(jīng)全存下來了,那干脆讓調(diào)用方自己傳 Set 進(jìn)來就好:
func InsertAll[E any](set Set[E], seq iter.Seq[E]) {
    for v := range seq {
        set.Insert(v)
    }
}這樣簡單明了,還能讓不同實(shí)現(xiàn)的 Set 都能復(fù)用。
例如,最簡單的 map 版:
type HashSet[E comparable] map[E]bool
func (s HashSet[E]) Insert(v E)       { s[v] = true }
func (s HashSet[E]) Delete(v E)       { delete(s, v) }
func (s HashSet[E]) Has(v E) bool     { return s[v] }
func (s HashSet[E]) All() iter.Seq[E] { return maps.Keys(s) }用接口值,而不是復(fù)雜約束,往往更容易讀懂,也更靈活。
總結(jié)
泛型接口是個(gè)很有意思的工具,能幫我們更自然地表達(dá)一些約束關(guān)系。
以下是幾個(gè)要點(diǎn):
- 可以用泛型接口來約束元素必須支持“和自己比較”。
 - 組合 
Comparer和comparable,能寫出更強(qiáng)大的容器類型。 - 接口定義最好保持寬松,把具體約束交給實(shí)現(xiàn)。
 - 如果因?yàn)橹羔樈邮照吒愠龊軓?fù)雜的泛型函數(shù)簽名,不妨退一步,改用接口值。
 
泛型給了 Go 更大的表達(dá)能力,但也帶來了復(fù)雜性。
當(dāng)然,我還是建議能用簡單方案解決的,就別上太花哨的寫法。真的容易累人。















 
 
 








 
 
 
 