天天看點

Go 1.18泛型的局限性初探

前言

Go 1.18 版本之後正式引入泛型,它被稱作類型參數(type parameters),本文初步介紹 Go 中泛型的使用。長期以來 go 都沒有泛型的概念,隻有接口 interface 偶爾類似的充當泛型的作用,然而接口終究無法滿足一些基本的泛型需求,比如這篇文章裡,我們會嘗試用 Go 的泛型循序漸進地實作一些常見的函數式特性,進而探索 Go 泛型的優勢和不足。

Go 1.18

在 Go1.18 可以通過如下指令安裝體驗:

go install golang.org/dl/go1.18@latest
go1.18 download
           

例1: 泛型版本的求和函數

import (
	"golang.org/x/exp/constraints"
)

func Sum[T constraints.Integer](values ...T) T {
	var sum T
	for _, v := range values {
		sum += v
	}
	return sum
}
           
constraints 原本是放在标準庫的包,但是近期被移除了,改到了 x/exp 中,參見 #50792

這個版本實作了對任意多個同類型的整數求和。Sum 後面的中括号 [] 内就是定義類型參數的地方,其中 T 為類型參數名,constraints.Integer 是對該類型參數的限制,即 T 應該滿足的條件,在這裡我們要求 T 是一個整數。剩下的代碼就和普通沒有泛型的代碼一緻了,隻不過後面 T 可以當作一個類型來使用。

泛型文法

  • 函數名後可以附帶一個方括号,包含了該函數涉及的類型參數(Type Paramters)的清單:

    func F[T any](p T) { ... }

  • 這些類型參數可以在函數參數和函數體中(作為類型)被使用
  • 自定義類型也可以有類型參數清單:

    type M[T any] []T

  • 每個類型參數對應一個類型限制,上述的 any 就是預定義的比對任意類型的限制
  • 類型限制在文法上以 interface 的形式存在,在 interface 中嵌入類型 T 可以表示這個類型必須是 T:
type Integer1 interface {
    int
}
           
  • 嵌入單個類型意義不大,我們可以用 | 來描述類型的 union:
type Integer2 interface {
    int | int8 | int16 | int32 | int64
}
           
  • ~T 文法可以表示該類型的「基礎類型」是 T,比如說我們的自定義類型 type MyInt int 不滿足上述的 Integer1 限制,但滿足以下的限制:
type Integer3 interface {
    ~int
}
           

高階函數執行個體

filter 操作是高階函數的經典應用,它接受一個函數

f(func (T) bool)

和一個線性表

l([] T)

,對 l 中的每個元素應用函數

f

,如結果為

true

,則将該元素加入新的線性表裡,否則丢棄該元素,最後傳回新的線性表。

func Filter[T any](f func(T) bool, src []T) []T {
        var dst []T
        for _, v := range src {
                if f(v) {
                        dst = append(dst, v)
                }
        }
        return dst
}

func main() {
        src := []int{-2, -1, -0, 1, 2}
        dst := Filter(func(v int) bool { return v >= 0 }, src)
        fmt.Println(dst)
}
// Output:
// [0 1 2]
           

讓人開心的改變 : )

實作一個三元操作

衆所周知Go語言不支援三元運算符操作,現在有了泛型,讓我們來模拟一個:

// IFF if yes return a else b
func IFF[T any](yes bool, a, b T) T {
	if yes {
		return a
	}
	return b
}

// IFN if yes return func, a() else b().
func IFN[T any](yes bool, a, b func() T) T {
	if yes {
		return a()
	}
	return b()
}

func main() {
        a := -1
	assert.Equal(t, utils.IFF(a > 0, a, 0), 0)
	assert.Equal(t, utils.IFN(a > 0, func() int { return a }, func() int { return 0 }), 0)
}
           

令人沮喪 😦

泛型類型系統的不足

衆多函數式特性的實作依賴于一個強大類型系統,Go 的類型系統顯然不足以勝任, 在 Go 語言中引入泛型之後,類型系統有哪些水土不服的地方。

編譯期類型判斷

當我們在寫一段泛型代碼裡的時候,有時候會需要根據 T 實際上的類型決定接下來的流程,可 Go 的完全沒有提供在編譯期操作類型的能力。運作期的 workaround 當然有,怎麼做呢:将 T 轉化為

interface{}

,然後做一次 type assertion, 比如我想實作一個通用的字元串類型到數字類型的轉換函數:

import "strconv"

type Number interface {
	int | int32 | int64 | uint32 | uint64 | float64
}

func Str2Number[N Number](strNumber string) (N, error) {
	var num N
	switch (interface{})(num).(type) {
	case int:
		cn, err := strconv.Atoi(strNumber)
		return N(cn), err
	case int32:
		cn, err := strconv.ParseInt(strNumber, 10, 32)
		return N(cn), err
	case int64:
		cn, err := strconv.ParseInt(strNumber, 10, 64)
		return N(cn), err
	case uint32:
		cn, err := strconv.ParseUint(strNumber, 10, 32)
		return N(cn), err
	case uint64:
		cn, err := strconv.ParseUint(strNumber, 10, 64)
		return N(cn), err
	case float64:
		cn, err := strconv.ParseFloat(strNumber, 64)
		return N(cn), err
	}
	return 0, nil
}
           

無法辨認「基礎類型」

在類型限制中可以用 ~T 的文法限制所有 基礎類型為 T 的類型,這是 Go 在文法層面上首次暴露出「基礎類型」的概念,在之前我們隻能通過 reflect.(Value).Kind 擷取。而在 type assertion 和 type switch 裡并沒有對應的文法處理「基礎類型」:

type Int interface {
        ~int | ~uint
}

func IsSigned[T Int](n T) {
        switch (interface{})(n).(type) {
        case int:
                fmt.Println("signed")
        default:
                fmt.Println("unsigned")
        }
}

func main() {
        type MyInt int
        IsSigned(1)
        IsSigned(MyInt(1))
}
// Output:
// signed
// unsigned
           

乍一看很合理,MyInt 确實不是 int。那我們要如何在函數不了解 MyInt 的情況下把它當 int 處理呢, 比較抱歉的是目前在1.18中沒辦法對這個進行處理。

類型限制不可用于 type assertion

一個直覺的想法是單獨定義一個 Signed 限制,然後判斷 T 是否滿足 Signed:

type Signed interface {
        ~int
}

func IsSigned[T Int](n T) {
        if _, ok := (interface{})(n).(Signed); ok {
                fmt.Println("signed")
        } else {
                fmt.Println("unsigned")
        }
}
           

但很可惜,類型限制不能用于 type assertion/switch,編譯器報錯如下:

interface contains type constraints

盡管讓類型限制用于 type assertion 可能會引入額外的問題,但犧牲這個支援讓 Go 的類型表達能力大大地打了折扣。

總結

  • 确實可以實作部分函數式特性能以更通用的方式。
  • 靈活度比代碼生成更高 ,用法更自然,但細節上的小問題很多。
  • 1.18 的泛型在引入 type paramters 文法之外并沒有其他大刀闊斧的改變,導緻泛型和這個語言的其他部分顯得有些格格不入,也使得泛型的能力受限。 至少在 1.18 裡,我們要忍受泛型中存在的種種不一緻。
  • 受制于 Go 類型系統的表達能力,我們無法表示複雜的類型限制,自然也無法實作完備的函數式特性。

推廣

推廣下個人項目,目前也正在使用Go 1.18的特性也踩了很多坑:

YoyoGo is a simple, light and fast , dependency injection based micro-service framework written in Go. Support Nacos ,Consoul ,Etcd ,Eureka ,kubernetes.

https://github.com/yoyofx/yoyogo

作者:

YOYOFx

出處:https://www.cnblogs.com/maxzhang1985/p/12673160.html

版權:本文采用「署名-非商業性使用-相同方式共享 4.0 國際」知識共享許可協定進行許可。

歡迎大家關注微信号。掃下面的二維碼或者收藏下面的二維碼關注吧(長按下面的二維碼圖檔、并選擇識别圖中的二維碼)