go1.18泛型探索与总结

本文内容参考自 https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md

俯瞰

  1. 长这样

    func F[T any](p T) {
    var a T
    }
  2. 类型参数(type parameters)可以在参数和函数体中使用

  3. type 也可以有类型参数列表(type parameter list)

    type M[T any] []T
  4. 类型参数(type parameter)也有约束(type constraint),就像普通参数有类型一样

    func F[T Constraint](p T) { ... }
  5. 类型约束(type constraint)是interface

  6. any = interface{}

  7. 约束接口(Interface types used as type constraints)可以嵌入额外的元素来限定满足约束的类型参数(type arguments)集合

    1. 类型:T
    2. 近似(approximation):~T(底层是T)
    3. 联合(union):T1|T2|...
  8. 泛型函数只能使用所有类型都支持的操作

  9. 调用泛型函数,需要传入类型参数

  10. (接上)但是常见场景下可以自动类型推断,无需传入

设计

类型参数(Type parameters)

import "fmt"

func Print[T any](s []T) {
for _, v := range s {
fmt.Println(v)
}
}

func main() {
Print([]string{"a", "b", "c"})
Print[string]([]string{"a", "b", "c"})
}
  • 使用方括号
  • 描述类型参数(type parameters)的附加可选参数列表
  • 出现在常规参数的前面,方法名的后面
  • 类型参数(type parameters)也有元类型,叫约束(constraints)
  • any = interface{}
  • 类型参数(type parameters)T,也可以用在普通参数里,修饰普通参数的类型,也可以用在函数体中
  • 声明时不知道具体是什么类型,运行时替换成真正的类型

约束(constraints)

func Stringify[T any](s []T) (ret []string) {
for _, v := range s {
ret = append(ret, v.String()) // INVALID
}
return ret
}

上述代码是不可以的。any类型并没有String() string 函数。所以需要更详细的类型约束,这样无法通过编译。

Go的设计是通过显式的定义约束,从而在编辑阶段确保运行时不出异常。

Go中的interface设定类似于一种约束,只有实现了interface中定义的方法的结构体实例,才可以用这个interface来声明自己的类型。如下面代码中 Event 的实例也可以是 Stringer 的实例。

import "fmt"

type Stringer interface {
String() string
}

type Event struct {
EventName string
EventID int
}

func (r Event) String() string {
return fmt.Sprintf("event{name:%s,id:%d}", r.EventName, r.EventID)
}

func main() {
var e Stringer = Event{
EventName: "eventA",
EventID: 1,
}
fmt.Println(e.String())
}

正因为如此,调用泛型函数类似将变量赋值给interface类型。当然,也就只能调用这个interface中所定义的方法。

import "fmt"

type Stringer interface {
String() string
}

func Stringify[T Stringer](s []T) (ret []string) {
for _, v := range s {
ret = append(ret, v.String())
}
return ret
}

type Event struct {
EventName string
EventID int
}

func (r Event) String() string {
return fmt.Sprintf("event{name:%s,id:%d}", r.EventName, r.EventID)
}

func main() {

events := []Event{
{
EventName: "eventA",
EventID: 1,
},
{
EventName: "eventB",
EventID: 2,
},
}
eventStrs := Stringify(events)
for _, eventStr := range eventStrs {
fmt.Println(eventStr)
}
}

any 约束

现在我们知道了约束就是interface,所以,any就是约束。any 同等于 interface{}。隐式的声明在全局block中。

多类型参数

func Print2[T1, T2 any](s1 []T1, s2 []T2) { ... }
func Print2Same[T any](s1 []T, s2 []T) { ... }
type Stringer interface {
String() string
}

type Plusser interface {
Plus(string) string
}

func ConcatTo[S Stringer, P Plusser](s []S, p []P) []string {
r := make([]string, len(s))
for i, v := range s {
r[i] = p[i].Plus(v.String())
}
return r
}

泛型 types

type Vector[T any] []T

type的类型参数也可以像函数的类型参数一样。

在使用的时候,必须要传入具体的类型参数,叫做实例化。

var v Vector[int]

泛型 type 也可以有方法,类型参数必须要在 receiver 定义中声明。

func (v *Vector[T]) Push(x T) { 
*v = append(*v, x)
}

但是名称可以随意。如果方法体中没有用到,也可以用下划线省略。

func (v *Vector[_]) Foo() { 
...
}

泛型 type 的成员可以引用自己,以实现(单、双向)链表、等结构。但要保证类型参数的顺序。否则会陷入无限初始化。

type List[T any] struct {
next *List[T] // this reference to List[T] is OK
val T
}

// This type is INVALID.
type P[T1, T2 any] struct {
F *P[T2, T1] // INVALID; must be [T1, T2]
}

操作符(operator)

func Smallest[T any](s []T) T {
r := s[0]
for _, v := range s[1:] {
if v < r { // INVALID
r = v
}
}
return r
}

any的实例并不一定都能支持<操作。因为<并不是方法。

GO 的理念是相比于为<写个约束,不如让约束决定哪些type可以接受。

类型集合(type sets)

每个类型都有对应的类型集合,如T:{T} ,只有 T 本身的类型集合。

但是多个 interface types:E1, E2,interface{ E1; E2 }的 type sets 是他们的交集。

类型集合的约束

之前提到了满足约束就是实现约束的方法。现在可以下更严格的定义:类型参数(type argument)满足约束,当且仅当它是约束的类型集合中的成员。

约束元素(Constraint elements)

普通interface类型的元素是方法签名和嵌入interface类型。

现在允许在用作约束的接口类型中使用三个附加元素。

如果使用这些附加元素中的任何一个,则接口类型不能用作普通类型,而只能用作约束。

任意类型约束元素(Arbitrary type constraint element)

不仅仅指 any,如 type Integer interface { int } 也是,当他被使用的时候,他代表的类型集合是 { int },只要在这个集合内部的数据类型都可以传入,此例中,也就是 int

近似约束元素(Approximation constraint element)

“不仅仅要int,还要背后实际上是int的所有类型,都要”。为达到这个目的,使用近似类型约束元素。

写作:~T

他的类型集合是T及背后为T的所有 types。

type MyString string
type AnyString interface{ ~string }

AnyString 的类型集合为:{ ~string }及其他背后为 string的类型,如 MyString

联合约束元素(Union constraint element)

type PredeclaredSignedInteger interface {
int | int8 | int16 | int32 | int64
}
type SignedInteger interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}

前者的类型集合是{int, int8, int16, int32, int64}。后者包含前者及背后为前者类型的所有类型。

基于类型集合的 operator

type Ordered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
~float32 | ~float64 |
~string
}

所有数值类型的类型集合Ordered作为泛型,即可进行大小的比较。

func Smallest[T Ordered](s []T) T {
r := s[0]
for _, v := range s[1:] {
if v < r {
r = v
}
}
return r
}

约束中的对比

==!=的对比方式,使用 comparable (预定义约束)实现。

也可以嵌入其他约束生成新的约束:

type ComparableHasher interface {
comparable
Hash() uintptr
}

例子

类型推断(Type inference)

使用类型推断来避免必须显式写出部分或全部类型参数。

在很多场景下,可以通过入参(可以是普通参数的类型,也可以是函数参数的入参和出参)推断出类型参数,或者通过一部分类型参数,推断出其他类型参数。

func Map[F, T any](s []F, f func(F) T) []T {
ret := make([]T, len(s))
for i, v := range s {
ret[i] = f(v)
}
return ret
}

func main() {
s := []int{2, 3, 4, 5, 4}
f := func(i int) int64 { return int64(i) }
var r []int64
r = Map[int, int64](s, f)
fmt.Println(r)
r = Map[int](s, f)
fmt.Println(r)
r = Map(s, f)
fmt.Println(r)
}

类型统一(Type unification)

类型推断的基础就是类型统一。类型统一是通过比较类型的结构来工作的。

func main() {
var a []map[int]bool
UnificationTest1(a) // ✓
UnificationTest2(a) // ✓
UnificationTest2[map[int]bool](a) // ✓
UnificationTest3(a) // ✓
UnificationTest3[[]map[int]bool](a) // ✓
UnificationTest4(a) // ✓
UnificationTest4[int, bool](a) // ✓
UnificationTest5(a) // ×
UnificationTest6[int](a) // ×
}
func UnificationTest1(p []map[int]bool) {}
func UnificationTest2[T1 any](p []T1) {}
func UnificationTest3[T1 any](p T1) {}
func UnificationTest4[T1, T2 comparable](p []map[T1]T2) {}
func UnificationTest5[T1 comparable](p []map[T1]string) {}
func UnificationTest6[T1 any](p []struct{}) {}

函数参数类型推断

类型推断只能推断那些在函数入参中使用到了的类型。

比如下面的例子中,Double1函数的入参是[]T,在调用这个函数时传入的参数是[]int{1, 2, 3},即可成功推断:T就是int

但是如果像Double2函数一样,入参没有用到类型参数T,或者说类型参数T只在函数体或函数返回值定义用到了,那就是不能推断的。如果需要具体类型,只能通过反射或强转在运行时达到目的。

package main

type Integer interface {
int | int8 | int16 | int32 | int64 | ...
}

func main() {
Double1[int]([]int{1, 2, 3})
// 2, 4, 6
Double1([]int{1, 2, 3})
// 2, 4, 6
Double2[int]([]any{1, 2, 3})
// 2, 4, 6
}

func Double1[T Integer](s []T) []T {
ret := make([]T, 0, len(s))
for _, v := range s {
ret = append(ret, v+v)
}
return ret
}

func Double2[T Integer](s []any) []T {
ret := make([]T, 0, len(s))
for _, v := range s {
sum := v.(int) + v.(int)
// ret = append(ret, sum) // INVALID, T can not be inferred
switch retTyped := (any)(&ret).(type) {
case *[]int:
*retTyped = append(*retTyped, sum)
}
}
return ret
}

约束类型推断

如果约束中,存在没有在函数入参中使用到的类型,如果这个类型与其他函数入参中使用到了的类型相关联,那么也是可以进行自动类型推断的。

package main

import "fmt"

func main() {
type MySlice []int

var V1 = Double(MySlice{1})
fmt.Printf("%T\n", V1) //[]int
fmt.Println(V1) //[2]

var V2 = DoubleDefined(MySlice{1})
fmt.Printf("%T\n", V2) //main.MySlice
fmt.Println(V2) //[2]
}

type Integer interface{ ~int }

func Double[E Integer](s []E) []E {
r := make([]E, len(s))
for i, v := range s {
r[i] = v + v
}
return r
}
func DoubleDefined[S ~[]E, E Integer](s S) S {
r := make(S, len(s))
for i, v := range s {
r[i] = v + v //可以编译和运行,但可能在GoLand中被标注红色波浪线报错
}
return r
}

首先通过入参,可以得到 S 就是 MySlice 类型。

{S -> MySlice}

其次,通过 S~[]E 知道 MySlice 是底层为 E 切片的类型,而 MySliceint 切片,所以 Eint

{S -> MySlice, E -> int}

这样一来,就确定了此函数的两个类型参数。

指针方法

下面例子尝试用FromStrings方法将字符串切片,转化为符合Setter约束的类型切片。

package main

import "strconv"

type Setter interface {
Set(string)
}

func FromStrings[T Setter](s []string) []T {
//T在这里是 *Settable,其零值是nil
result := make([]T, len(s))
for i, v := range s {
result[i].Set(v)
}
return result
}

type Settable int

func (p *Settable) Set(s string) {
i, _ := strconv.Atoi(s)
// assign to nil
*p = Settable(i)
}

func main() {
//nums := FromStrings[Settable]([]string{"1", "2"}) //×
nums := FromStrings[*Settable]([]string{"1", "2"}) //✓ ×
for _, s := range nums {
println(s)
}
}

因为Settable实际是int类型,所以给原地它赋值必须是赋值给指针类型,引用类型不生效。所以func (p *Settable) Set(s string)是指针类型的receiver。

FromStrings[Settable]([]string{"1", "2"})调用,是不成立的,因为Settable并不是Setter约束的类型集合(type set)之一,而在此代码中,*Settable才是。

如果将类型参数改为*Settable,也是不行的。因为 *Settable其零值是nil,在后续的调用中会出异常。

解决办法是同时穿两个,一个值类型、一个指针类型,值类型用于初始化零值,指针类型用于调用Set方法。

package main

import "strconv"

type Setter[B any] interface {
*B
Set(string)
}

func FromStrings[T any, PT Setter[T]](s []string) []T {
//T在这里是 Settable,其零值是0
result := make([]T, len(s))
for i, v := range s {
p := PT(&result[i]) //强转
p.Set(v)
}
return result
}

type Settable int

func (p *Settable) Set(s string) {
i, _ := strconv.Atoi(s) // real code should not ignore the error
*p = Settable(i)
}

func main() {
nums := FromStrings[Settable]([]string{"1", "2"})
for _, s := range nums {
println(s)
}
}

这样的方式显得比较尴尬,但是可以实现需求,而且使用类型推断可以显得不那么尴尬。

{T -> Settable}
{T -> Settable, PT -> *T}
{T -> Settable, PT -> *Settable}

自引用约束

考虑以下代码如何简化

type equalInt int

func (a equalInt) Equal(b equalInt) bool { return a == b }

func indexEqualInt(s []equalInt, e equalInt) int {
return Index[equalInt](s, e)
}

type Equaler[T any] interface {
Equal(T) bool
}

func Index[T Equaler[T]](s []T, e T) int {
for i, v := range s {
if e.Equal(v) {
return i
}
}
return -1
}

func main() {
println(indexEqualInt([]equalInt{1, 2, 3}, 3))
}

其复杂就复杂在定义了Equaler约束,在Index的类型参数列表中,T自引用。

简化方式是写自引用的匿名约束。

func Index[T interface{ Equal(T) bool }](s []T, e T) int {
...
}

约束相互嵌套

就像接口可以相互嵌套一样,约束也可以相互嵌套。

type Signed interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}

type Unsigned interface {
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}

type Integer interface {
Signed | Unsigned
}
type Addable interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
~float32 | ~float64 | ~complex64 | ~complex128 |
~string
}

type Byteseq interface {
~string | ~[]byte
}

// truned out to be just the type set ~string.
type AddableByteseq interface {
Addable
Byteseq
}

注意,嵌套的约束越多,符合约束的类型就越少。

泛型的问题

性能(理论上)

在函数中使用泛型,将减少编译时间(AddInt, AddInt32, … AddInt64等函数都合一了),但是代码运行是将会有一点性能损耗(类型转换上)。

在类型中使用泛型,将增加编译时间(类型集合(type set)中的每一个符合的都要编译一遍),但是运行时不会有性能损耗。

泛型包欠缺

承诺的全新的slicessetssort在当前版本中都还没有。

接口方法类型+联合类型还没实现

前面提到过了约束支持数据类型和方法两种,是要同时满足才可以的。因为 fmt.Stringer 是包含方法的接口,如果用【或】的关系:

type Stringish interface {
string | fmt.Stringer
}

上面的实例在编译阶段就会报错。

cannot use fmt.Stringer in union (fmt.Stringer contains methods)

有些约束不可能有满足的类型

下面代码描述了数据类型要是intfloat32的,且要有String() string方法实现。

type Unsatisfiable interface {
int | float32
String() string
}

然而现实中,无论是int还是float32,他们都没有String() string方法,所以是无法找到满足约束的类型,即上面的约束有空类型集合(empty type set)。

例子

Map/Reduce/Filter

func Map[T1, T2 any](s []T1, f func(T1) T2) []T2 {
r := make([]T2, len(s))
for i, v := range s {
r[i] = f(v)
}
return r
}

func Reduce[T1, T2 any](s []T1, initializer T2, f func(T2, T1) T2) T2 {
r := initializer
for _, v := range s {
r = f(r, v)
}
return r
}

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

func main() {
s := []int{1, 2, 3}
floats := Map(s, func(i int) float64 { return float64(i) })
// Now floats is []float64{1.0, 2.0, 3.0}.
sum := Reduce(s, 0, func(i, j int) int { return i + j })
// Now sum is 6.
evens := Filter(s, func(i int) bool { return i%2 == 0 })
// Now evens is []int{2}.
}

map keys

func Keys[K comparable, V any](m map[K]V) []K {
r := make([]K, 0, len(m))
for k := range m {
r = append(r, k)
}
return r
}

func main() {
k := Keys(map[int]int{1: 2, 2: 4})
// Now k is either []int{1, 2} or []int{2, 1}.
}

等等。。。

数据库翻页读取泛型案例

(假代码模仿通过rpc接口翻页读取)

package main

import (
"fmt"
"math/rand"
"time"
)

func init() {
rand.Seed(time.Now().UnixNano())
}

type Order struct {
Id *int
OrderName *string
}
type SKU struct {
Id *int
SKUName *string
}
type BaseResponse struct {
StatusCode int32
}
type GetOrdersResponse struct {
Data []*Order
BaseResponse
}

func (r *GetOrdersResponse) GetData() []*Order {
return r.Data
}
func (r *GetOrdersResponse) GetBaseResponse() BaseResponse {
return r.BaseResponse
}

type GetSKUsResponse struct {
Data []*SKU
BaseResponse
}

func (r *GetSKUsResponse) GetData() []*SKU {
return r.Data
}
func (r *GetSKUsResponse) GetBaseResponse() BaseResponse {
return r.BaseResponse
}

type Model interface {
Order | SKU
}
type Resp[T Model] interface {
*GetOrdersResponse | *GetSKUsResponse
GetData() []*T
GetBaseResponse() BaseResponse
}

func GetModels[T Model, R Resp[T]]() []*T {
t := make([]*T, 0)
var r = new(R)
for i := 0; i < 2; i++ {
switch (any)(t).(type) {
case []*Order:
rTyped := (any)(r).(**GetOrdersResponse)
*rTyped = genFakeOrders()
case []*SKU:
rTyped := (any)(r).(**GetSKUsResponse)
*rTyped = genFakeSKUs()
}
if (*r).GetBaseResponse().StatusCode != 0 {
return nil
}
//通过返回的还有多少页,总量等,控制翻页
//日志等共同代码
t = append(t, (*r).GetData()...)
}
return t
}

func main() {
models := GetModels[Order, *GetOrdersResponse]()
for _, m := range models {
fmt.Printf("id: %d, orderName: %s\n", *m.Id, *m.OrderName)
}
models2 := GetModels[SKU, *GetSKUsResponse]()
for _, m := range models2 {
fmt.Printf("id: %d, skuName: %s\n", *m.Id, *m.SKUName)
}
}

func genFakeOrders() *GetOrdersResponse {
num := rand.Intn(7)
orders := make([]*Order, 0)
for i := 0; i < num; i++ {
id := rand.Intn(120)
name := fmt.Sprintf("order_%d", i)
orders = append(orders, &Order{
Id: &id,
OrderName: &name,
})
}
return &GetOrdersResponse{
Data: orders,
BaseResponse: BaseResponse{StatusCode: int32(0)},
}
}

func genFakeSKUs() *GetSKUsResponse {
num := rand.Intn(8)
skus := make([]*SKU, 0)
for i := 0; i < num; i++ {
id := rand.Intn(100)
name := fmt.Sprintf("sku_%d", i)
skus = append(skus, &SKU{
Id: &id,
SKUName: &name,
})
}
return &GetSKUsResponse{
Data: skus,
BaseResponse: BaseResponse{StatusCode: int32(0)},
}
}


   转载规则


《go1.18泛型探索与总结》 Harbor Zeng 采用 知识共享署名 4.0 国际许可协议 进行许可。
 本篇
go1.18泛型探索与总结 go1.18泛型探索与总结
本文内容参考自 https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md 俯瞰 长这样 func F[T any](p T) { var a T} 类型参数(type parameters)可以在参数和函数体中使用 type 也可以有类型参
2022-05-03
下一篇 
JVM垃圾回收机制知识点 JVM垃圾回收机制知识点
堆为什么要分成年轻代和老年代? 因为年轻代和老年代不同的特点,需要采用不同的垃圾回收算法。 年轻代的对象,它的特点是创建之后很快就会被回收,所以需要用一种垃圾回收算法; 老年代的对象,它的特点是需要长期存活,所以需要另外一种垃圾回收算法; 所以需要分成两个区域来放不同的对象。 绝大多数对象都是朝生夕灭的; 如果一个区域中大多数对象都是朝生夕灭,那么把它们集中放在一起,每次回收时只关注如何保
2021-11-13
  目录