Go泛型设计介绍

1,150 阅读15分钟

2021年Golang社区提交了使用类型参数向Go语言增加泛型编程的设计提案,支持泛型的第一个测试版本Go 1.18 Beta 1已于2021年12月发布,Go 1.18正式版本也已于2022年3月发布。为了后续使用中能快速上手泛型特性,我们需要对泛型有一定的了解。本文主要对提案中的泛型设计部分做一个简单介绍。

概述

在保证与Go 1完全兼容的情况下,为类型及函数声明增加可选的类型参数(Type Parameters)来扩展支持泛型函数和类型。

类型参数使用接口类型(Interface Types)进行约束(Constraint),并允许在接口类型中内嵌附加元素来指定约束的类型集合。

可以通过显式指定类型实参,来实例化泛型类型和函数。为了简洁性,类型推断机制可以在大多数场景下减少甚至忽略泛型实例化时指定的类型实参个数。

概念

Generic function types

ParameterizedFunctionType = "func" [TypeParameters] Signature

TypeParameters = "[" TypeParameterList "]"
TypeParameterList = TypeParameterDecl { "," TypeParameterDecl }
TypeParameterDecl = TypeIdentifierList TypeConstaint

Signature = Parameters [Result]
Result = Parameters | Type
Parameters = "(" [ParameterList [","]] ")"
ParameterList = ParameterDecl { "," ParameterDecl }
ParameterDecl = [ IdentifierList ] [ "..." ] Type

如:

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

Generic types

TypeDecl = "type" (TypeSpec | "(" { TypeSpec ";" } ")" )
TypeSpec = AliasDecl | TypeDef

TypeDef = identifier [ TypeParameters ] Type
Type = TypeName [ TypeArgs ] | TypeLit | "(" Type ")" 
TypeName = identifier | QualifiedIdent
TypeArgs = "[" TypeList [ "," ] "]"
TypeList = Type { "," Type }
TypeLit = StructType | InterfaceType


StructType = "struct" [ TypeParameters ] "{" { FieldDecl ";" }"}"

TypeParameters = ... (同上)

FieldDecl = (IdentifierList Type | EmbededField ) [Tag]
EmbeddedField = [ "*" TypeName ]
Tag = string_lit

如:

type Vector[T any] []T

type Tp[T any] struct {
    t T
}

Type Constraints

类型约束由接口类型表示,当前预定义了两个内置约束:

  • any:是空接口的别名,用于代替interface{},表示任意类型;
  • comparable:指代可以使用==!=进行比较的类型集合。

作为类型约束的接口类型,支持嵌入附加元素:

  • T:仅限定到类型T
  • ~T:限定到所有底层类型为T的类型;
  • T1 | T2 | ...:限定到列表中的任意类型。
InterfaceType = "interface" "{" {(MethodSpec | InterfaceTypeName | ConstraintElem ) ";" } "}"
ConstraintElem = ConstraintTerm { "|" ConstraintTerm }
ConstraintTerm = ["~"] Type

如:

type Number interface {
    int64 | ~uint64 | float64
}

设计

Type Parameters

对于泛型代码,在定义时使用抽象数据类型(类型参数:Type Parameters),运行时则使用真实的类型(类型实参:Type Arguments)进行替换。

假设有一个Filter函数,其可以从一个切片中过滤出满足谓词的元素,这里切片的元素类型是未知的。

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

这里面临的第一个问题是:如何声明类型参数T

类型参数与普通函数的参数类似,如果将类型参数与函数的参数放置在一起,则需要能对两者进行区分。基于此,提案中的设计决策为:增加一个可选的参数列表来描述类型参数。类型参数列表放置在普通参数列表之前,为了对两种参数列表进行区分,类型参数列表使用[]界定。相较于普通参数的类型,类型参数可以具有元类型(Meta-Types)——约束(Constraints)。

func Filter[T any](s []T, p func(T)bool) []T {...}

由于Filter包含类型参数,故在调用Filter时需要为其提供类型实参。类型实参传递方式与类型参数的声明方式类似,由使用[]界定、由,分隔的类型来表示。

filered := Filter[int]([]int{1, 2, 3, 4, 5, 6}, func(i int) bool {return i % 2 == 0 })

Type Constraints

func Stringify[T any](s []T) []string {
    r := []string{}
    for _, e := range s {
        r = append(r, e.String()) // INVAILD
    }
    return r
}

Stringify函数初看之下似乎没有问题,但仔细分析却发现:e的类型为TT可以是任意类型,故T并不一定具有String() string方法,因此e.String()是无效的。

在C++中,泛型函数(函数模板)在定义时可在泛型类型上调用任何方法。当使用没有相应方法的类型实参进行调用时,会在编译时提示此错误。

由于Go被设计为支持大规模编程,需要考虑泛型函数的定义与调用相距比较远的情形。通常,所有的泛型代码都期望类型参数满足某些要求,这些要求可以称作“约束”(Constraints)。上述代码中约束比较明显,即要求类型必须有一个String() string方法。

如果直接从泛型函数的实现来派生约束,则泛型函数定义的一些小改动都可能会改变约束,这意味着泛型代码的微小改动可能导致调用代码的意外中断这样的大影响。为了避免这种旷,需要将约束依赖于泛型定义的关系进行调整,对约束进行抽象,使泛型函数的定义与调用都依赖于此抽象。调用方只能传递满足约束的实参,泛型函数定义只能以约束允许的方式使用(如约束允许的方法)。

Defining Type Constraints

在Go中,接口类型是一些方法的集合,只有实现了这些方法的类型可以赋值给此接口类型。与之相似,在定义泛型函数时,函数体仅能使用约束允许的方法;在调用泛型函数时,类型实参必须实现类型参数的约束。

type Stringer interface { // this define same as fmt.Stringer interface
    String() string
}

func Stringfy[T Stringer](s []T) []string {
    r := []string{}
    for _, e := range s {
        r = append(r, e.String())
    }
    return r
}
any Constraint

any约束作为Go预定义的约束,可以看作是空接口interface{}的别名,可以表示任何类型,可以允许的操作有:

  • 声明此类型的变量;
  • 将同类型的值赋值给变量;
  • 将此类型变量作为函数的参数及返回值;
  • 获取变量的地址;
  • 将此类型变量赋值给interface{}
  • 将类型T转换为类型T(允许但没有意义);
  • 使用类型断言将接口类型转换到此类型;
  • type switch中作为case的类型;
  • 将类型传递给一些预定义函数,如new T

如:

func X[T any](s T) {...}

Multiple Type Parameters

在泛型中,可以包含多个类型参数,对于每个类型参数都应有自己的约束,一个约束也可以应用于多个类型参数。

// 每个类型参数都有一个独立的约束
func X1[T1 Cons1, T2 Cons2](s1 T1, s2 T2) {...}

// 一个约束作用于多个类型参数
func X2[T1 any, T2 any](s1 T1, s2 T2) {...}

// 同上
func X3[T1, T2 any](s1 T1, s2 T2) {...}

Generic Types

在泛型中,不仅有泛型函数,还应有泛型类型。相对于通常的Go类型,泛型类型在形式上扩展了函数参数。

type Vector[T any] []T

在使用泛型类型时,需要提供类型实参,这一过程被称“实例化”(Instantiation)。每一次实例化,都会通过使用类型实参替换形参而形成一个类型。

// instantiation []int
var v Vector[int]

泛型类型中也可以定义方法,其receiver必须声明与泛型类型定义时类型参数相应的类型参数,此时无需为类型参数指定约束,也不要求具有相同的名称。

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

若某些类型参数在方法无需使用,则可使用_替代。

func (v *Vector[_]) Size() int {
    return len(*v)
}

泛型类型允许直接或间接引用自身,为了防止实例化时陷入无限递归,此时要求类型实参必须是类型形参,且顺序相同。

type List[T any] struct {
    next *List[T]
    val T
}

type P[T1, T2 any] struct {
    F *P[T2, T1] // INVAILD: must be P[T1, T2]
}

尽管泛型类型的方法可以使用类型参数,但其不能再添加额外的类型参数。

Operators

在使用接口类型表示约束时,由于接口类型仅提供一组方法的集合,故在泛型函数中除了执行所有类型都支持的操作之外,就只能调用类型参数允许的方法。有时,用户需要在泛型函数中执行一些其它操作,如<等。

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

在其它泛型实现中一般都支持上述泛型函数,但由于Go中<并不是方法而一个操作符(Operator),考虑到Go中的算术、比较和逻辑操作符仅能作用于语言预定义的类型(或底层类型是预定义类型的类型),故而上述中v < r表达式无效。

相对于在约束中声明支持的操作符,为约束增加其接受类型的语义表达也许能更好地解决上述问题。为了说明约束接受的类型,特引入了类型集(Type Set)的概念。

Type Sets

对于每一个类型都关联一个类型集,非接口类型T对应的类型集为{T},接口类型IT对应的类型集为{T | T中声明了接口IT中的所有方法}。接口类型IT对应的类型集是一个无限集,由于IT也声明了其自身定义的所有方法,故其自身也是其类型集的元素。空接口interface{}的类型集为所有类型。

可以通过接口中的元素来分析其对应的类型集,接口类型可以支持方法签名、嵌入的类型两种元素,接口类型对应的类型集为其所有元素对应类型的交集。

Type Sets of Constraints

对于类型实参是否满足约束的定义,之前是以类型实参是否实现了约束来界定,引入类型集后可以表述为:类型实参是否是约束的类型集的元素。同时,为了更好地表达约束的类型集,可以为约束场景下的接口类型引入三个附加元素(且仅能用于约束场景)。

InterfaceType = "interface" "{" ( MethodSpec | InterfaceTypeName | ConstaintElem ) "}"
ConstraintElem = ConstraintTerm { "|" ConstraintTerm }
ConstraintTerm = ["~"] Type
Arbitrary Type Constraint Element

约束中允许添加任意非接口类型作为其元素。

type Integer interface {
    int
}

此时Integer的类型集为{int}

内嵌的类型可以是引用参数的类型字面量,但不可直接使用类型参数本身。

type EmbeddedParameter[T any] interface {
    T // INVAILD: may not list a plain type parameter
}

type Setter[T any] interface {
    Set(string)
    *T // non-interface type constraint element
}
Approximation Constraint Element

约束中允许添加近似元素(Approximation Element)。

type Integer interface {
    ~int
}

此时Integer的类型集为{T | 底层类型为int的类型}

由于~T意味着类型集的元素的底层类型均为T,若类型T的底层类型不是T则会提示错误。类型与其底层类型相同的类型有:

  • 类型字面量,如[]bytestruct { f int }
  • Go的大多数预定义类型,如intstring(但不包括error)。
type MyString string 
type ApproximateMyString interface {
    ~MyString // INVALID: underlying type of MyString is string, but not MyString
}

type ApproximateParameter[T any] interface {
    ~T // INVALID: T is a type parameter
}
Union Constraint Element

约束中允许添加联合元素(Union Element),形式为由竖线(|)分隔的约束元素。

type PredeclaredSignedInteger interface {
    int | int8 | int16 | int32 | int64 // also can union approximation element
}

此时PredeclaredSignedInteger的类型集为{int, int8, int16, int32, int64}

引入类型集的目的是为了在泛型中对类型参数的值使用操作符(如<),在引入约束的内嵌元素后可以解决上述问题。

type Ordered interface {
    Integer | Float | ~string
}

type Integer interface {
    Signed | Unsigned
}

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

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

type Float interface {
    ~float32 | ~float64
}

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

Go 1.18中新增了golang.org/x/exp/constraints包来预定义一些常用的约束(Complex、Float、Integer、Ordered、Signed及Unsigned)

Comparable Types in Constraints

在Go语言中,==!=操作符不仅可以作用于语言预定义的类型,而且也可作用于struct、array和interface等类型。为了方便写一个允许比较操作的类型约束,新增了预定义约束comparable,以其表示允许==!=操作的类型。

func Equal[T comparable](l, r T) bool {
    return l == r
}

func ImpossibleConstraint interface {
    comparable
    []int // because slice are not comparable, so ImpossibleConstraint no type can satify
}
Composite Types in Constraints

对于复合类型(如string、pointer、array、slice、struct、function、map、channel)增加了一个额外限制:只有当操作接受相同的入参类型并生成相同类型的结果时,才能使用此操作。此限制可以更容易地推断泛型函数中某些操作的类型。

type StructField interface {
    struct { a int; x int }
    struct { b int; x float64 }
    struct { c int; x uint64 }
}

type Increment[T StructField](p *T) {
    v := p.x // INVALID: type of p.x is not the same for all types in set
    v++
    p.x = v
}
Type Parameters in Type Sets

约束元素中的类型字面量,可以引用约束的类型参数。

// matches a slice of the type parameter
type SliceConstraint[T any] interface {
    ~[]T
}
Type Conversions

对于一个含有两个类型参数FromTo的泛型函数,如果From的约束类型集中的所有类型均可转换成To的约束类型集中的任一类型,则From类型的值可以转换成To类型的值。

func Convert[From, To constraints.Integer](from From) To {
    to := To(from)
    if From(to) != from {
        panic("conversion out of range")
    }
    return to
}
Type Sets of Embedded Constraints

约束中允许内嵌其它约束作为元素,对应的类型集为所有内嵌约束的类型集的交集。

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

type Addable interface {
    constraints.Integer | constraints.Float | ~string
}

// type set is ~string
type AddableByteseq interface {
    Addable
    Byteseq
}

Mutually Referencing Type Parameters

在类型参数列表中,约束可以引用类型参数,而无论此类型是否在约束之前声明。

在图论中,一个图(Graph)由顶点(Vertex,或节点:Node)和边(Edge)两类元素构成。在使用泛型表达时,Node期望有一个Edges() []Edge方法来获取节点相关的所有边,Edge又期望有一个Node() (Node, Node)方法来表达边所关联的两端节点。

type NodeConstraint[Edge any] interface {
    Edges() []Edge
}

type EdgeConstraint[Node any] interface {
    Nodes() (Node, Node)
}

type Graph[Node NodeConstraint[Edge], Edge EdgeConstraint[Node]] struct {...}

func New[Node NodeConstraint[Edge], Edge EdgeConstraint[Node]](nodes []Node) *Graph[Node, Edge] {...}

func (g *Graph[Node, Edge]) ShortestPath(from, to Node) []Edge {...}

这里的NodeEdge可以是接口,也可以是非接口类型。

type Vertex struct {...}

func (v *Vertex) Edges() []*FromTo {...}

type FromTo struct {...}

func (f *FromTo) Nodes() (*Vertex, *Vertex) {...}

var g = New[*Vertex, *FromTo]([]*Vertex{...})

注意:由于VertexFromTo并未实现对应的约束,故不能直接将VertexFromTo直接传递给New函数。

当使用泛型接口作为约束时,首先使用类型列表中的类型实参来实例化类型形参,然后将相应的类型实参与实例化的约束进行比较。在上例中,Node的约束是NodeConstraint[Edge]Edge的约束是EdgeConstraint[Node]Node的约束使用类型实参*FromTo实例化为NodeConstraint[*FromTo],此时Node要求具有方法Edges() []*FromTo,验证Node的类型实参*Vertex满足上述约束。

除了使用struct之外,也可以使用接口类型进行实例化。

type NodeInterface interface {
    Edges() []EdgeInterface
}

type EdgeInterface interface {
    Nodes() (NodeInterface, NodeInterface)
}

var g = New[NodeInterface, EdgeInterface]([]NodeInterface{...})

Type Inference

在实例化泛型函数或类时,每次都指定所有类型实参的方式比较繁琐,通过实参类型推断(Argument Type Inference)从非类型参数中推断类型实参,可以在实例化时减少甚至完全不指定类型实参。

func Map[S, D any](s []S, f func(S) D) []T {...}

var s []int

f := func(i int) int64 {return int64(i)}

// Specify both type argument
r := Map[int, int64](s, f)

// Specify just the first type argument, and the second be inferred
r1 := Map[int](s, f)

// Don't specify any type argument, and let both be inferred
r2 := Map(s, f)

在不指定所有类型实参使用泛型时,若有些未指定的类型实参无法推断,则会提示错误。

Type Unification

在不考虑类型参数情况下,若两个类型的结构相同(Identical),且结构中对应的类型等价(Equivalent),则此两类型统一(Type Unification)。在类型统一情况下,一个类型中的类型参数可以匹配其它类型的子类型(类型的一部分或全部),从而形成类型参数与其它类型的关联列表。当前的类型推断就是基于类型统一实现的。

[]map[int]bool可以与如下类型统一:

  • []map[int]bool
  • T1T1匹配[]map[int]bool
  • []T1T1匹配map[int]bool
  • []map[T1]T2T1匹配intT2匹配bool

对于两个类型统一的类型,两个类型均可包含参数,如[]T1可与T2类型统一。

Function Argument Type Inference

泛型函数的实参类型推断采用两遍算法(Two-pass Algorithm):

  • 第一遍:忽略调用时的非类型常量(Untyped Constraints)及其对应的泛型函数定义中的类型,基于类型统一,将函数定义中的类型参数与调用时的类型实参进行关联。
  • 检查调用时的非类型常量,如果没有非类型常量或其对应的函数定义中的类型已经与其它输入类型匹配,则类型统一已完成。
  • 第二遍:对于函数定义中尚未确定类型的参数对应的非类型常量,使用通常的方式确定非类型化常量的默认类型。然后对余下的类型再进行一次类型统一。

采用两遍算法,以便在某些情况下,后面的实参可以推断非类型常量的类型。

// []int与[]S类型统一,故S匹配int
// strconv.Itoa的签名为func(int) string,与func(S) D类型统一,故S匹配int、D匹配string
// 类型参数S匹配两次,两次均匹配int
// 类型统一成功,最终调用为Map[int, string](...)
strs := Map([]int{1, 2, 3}, strconv.Itoa)

func NewPair[F any](f1, f2 F) *Pair[F] {...}

// 不使用类型推断
NewPair[int](1, 2)

// 两个函数参数均为非类型常量
// 忽略第一遍
// 第二遍:非类型常量的默认类型为int
// 进行第二遍类型统一:F匹配int
// 最终调用为NewPair[int](...)
NewPair(1, 2)

// 第一个参数为非类型常量
// 第一遍:忽略第一个实参,F与int64类型统一
// 由于非类型常量对应的类型参数已完全确定,故不需要第二遍
// 最终调用为NewPair[int64](...)
NewPair(1, int64(2))

// 第一和第二个类型实参均为非类型常量
// 第一遍:由于参数全部为非类型常量,故不做任何事情
// 第二遍:第一个非类型常量的默认类型为int,第二个非类型常量的默认类型为float64
// F期望与int和float64都类型统一,故类型统一失败,提示编译错误
NewPair(1, 2.5)

注意:在类型推断之后,编译器还需要校验类型实参是否能满足约束

Constraint Type Inference

基于类型参数约束,约束类型推断可以从其它类型实参处推断类型实参。主要用于类型参数中引用其它类型名或类型约束基于其它类型参数的场景。

约束类型推断过程:

  1. 创建一个类型参数到类型实参的映射表,并使用已确定实参的形参进行初始化;
  2. 对于每个具有结构约束的类型参数,将类型参数与结构类型统一起来,在映射表中增加类型参数到其约束的映射条目;
  3. 寻找映射到完全已知实参的类型参数T,并在映射表中寻找出现T的条目,并使用其实参进行替换;
  4. 使用函数类型推断(不包含非类型常量推断,也即第二遍);
  5. 重复2、3
  6. 使用函数类型推断(非类型常量推断,也即第二遍);
  7. 重复2、3。

如定义一个泛型函数,其接受一个指定类型的切片,并期望返回一个同类型但元素值为两倍的新切片。

func Double[E constraints.Integer](s []E) []E {
    r := make([]E, len(s))
    for i, v := range s {
        r[i] = v + v
    }
    return r
}

type MySlice []int

// v1的类型为[]int,而非MySlice
// 使用函数实参类型推断,MySlice与[]E类型统一,从而推断出E与int匹配
var v1 = Double(MySlice{1})

可以通过再增加一个类型参数的方式来达到期望。

func DoubleDefined[S ~[]E, E constraints.Integer](s S) S {
    r := make(S, len(s))
    for i, v := range s {
        r[i] = v + v
    }
    return r 
}

var v2 = DoubleDefined[MySlice, int](MySlice{1})

// 使用函数实参类型推断,S匹配MySlice,映射表为:{S -> MySlice}
// 使用约束类型推断,S有一个结构类型约束~[]E,则~[]E与MySlice类型统一
// 考虑到MySlice定义为[]int,故E匹配int,映射表为:{S -> MySlice, E -> int}
// 最终调用为DoubleDefined[MySlice, int](MySlice{1})
var v3 = DoubleDefined(MySlice{1})

定义一个泛型函数,接受一个string切片,并期望返回具有方法Set(string)的类型T的切片,且元素均由对应的string调用Set来设置。

// 类型约束:要求类型实现Set(string)方法
type Setter interface {
    Set(string)
}

func FromStrings[T Setter](s []string) []T {
    r := make([]T, len(s))
    for i, v := range s {
        r[i].Set(v)
    }
    return r
}

type Settable int

func (s *Settable) Set(s string) {
    i, _ := strconv.Atoi(str)
    *s = i
}

var nums1 = FromStrings[Settable]([]string{"1", "2"}) // can't compile

var nums2 = FromStrings[*Settable]([]string{"1", "2"}) // panic by nil pointer

nums1无法编译的原因是:FromStrings的类型约束要求类型具有方法Set(string),而Settable并无此方法,具有此方法的是*Settable

nums2 panic的原因是:FromStrings内部创建[]T,也即[]*Settable,而后调用r[i].Set(v)进行设置,但由于r[i]的值为nil,故调用Set(v)报错。

可以通过再增加一个类型参数的方式达到预期。

// 类型约束要求:
// 类型实现Set(string)方法
// 类型是一个指向参数的指针
type Setter2[B any] interface {
    Set(string)
    *B
}

// 使用两个类型参数,以便可以返回T的切片,并在内部调用*T的方法
func FromStrings2[T any, PT Setter2[T]](s []string) []T {
    r := make([]T, len(s))
    for i, v := range s {
        r[i].Set(v)
    }
    return r
}

// 使用已知的类型初始化映射表:{T -> Settable}
// 将类型参数与结构类型统一,也即PT与Setter2[T]统一,百Setter2[T]即为*T,映射表变更为:{T -> Settable, PT -> *T}
// 在映射表中查找并替换已知类型,映射表变更为{T -> Settable, PT -> *Settable}
// 最终调用为FromStrings2[Settable, *Settable]([]string{"1", "2"})
var nums3 = FromStrings2[Settable]([]string{"1", "2"})

通过PT,可以在调用时不显式指定*T而增加相应约束。

在进行类型推断后,仍要校验推断的实参是否满足约束。

type Unsettable int

// 类型推断后的调用为FromStrings2[Unsettable, *Unsettable]([]string{"1", "2"})
// 但*Unsettable类型并未实现Set(string)方法,约束校验失败,编译错误
var nums4 = FromStrings2[Unsettable]([]string{"1", "2"}) // INVALID

Using Type That Refer To Themselves In Constraints

在泛型函数中,经常出现类型参数的方法参数中依赖自身类型的情况,如包含一个比较方法。

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

对于Equaler,需要在约束定义中表达引用自身类型的方法,可以使用接口字面量、接口约束定义两种方式处理。

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

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

func Index[T Equaler[T]](s []T, e T) int {...}

Values Of Type Parameters Are Not Boxed

在Go中,接口的值均存储指针,将一个非指针值赋值给接口变量会导致值被装箱(Boxed),此时真实值被存储在堆或栈上,而接口的值却保存一个指向此真实值的指针。

在Java中装箱就是自动将基本类型转换为包装类型的过程;拆箱就是自动将包装类型转换为基本类型的过程。

在泛型设计中,泛型类型的值并不会被装箱。

type Pair[T1, T2 any] struct {
    first T1
    second T2
}

在对Pair进行实例化时,其字段并不会被装箱,也不会出现意外出现的内存分配。Pair[int, string]被转化为struct {first int; second string}

Reflect

当一个泛型函数或类型实例化时,所有的类型参数都将被替换为普通的非泛型类型。此时,仍可使用reflect.TypeOf函数来获取实例化的类型,对于实例化的泛型类型其会在方括号中显示类型实参,但对于实例化的泛型函数却不会显示此信息。

func Double[E constraints.Integer](s []E) []E {
    r := make([]E, len(s))
    for i, v := range s {
        r[i] =  v + v
    }
    return r
}

type List[T any] struct {
    head, tail *element[T]
}

type element[T any] struct {
    next *element[T]
    val T
}

f := Double[int]
fmt.Println(reflect.TypeOf(f)) // func([]int) []int

c := List[int]{}
fmt.Println(reflect.TypeOf(c)) // main.List[int]

实现

提案中介绍:可以为每一组类型实参分别编译代码,也可以如接口类型的方法调用般处理每个类型实参进行编译,也可以在同时采用这两种方式来平衡编译速度(分别编译慢)和运行速度(对每个类型实参的每个操作都使用方法调用)。

总结

泛型设计可以关注如下要点:

  • 函数和类型可以包含类型参数,这些类型参数使用接口类型来定义约束
  • 约束描述了类型参数及实参所需的方法及允许的操作
  • 类型推断通常可以忽略泛型函数调用时的类型实参(泛型类型不支持类型推断)
  • 此设计完全向后兼容。

Go语言是一门简洁性的语言,泛型使其变得有点儿复杂,但仍可接受。Go泛型设计仅覆盖了基本需求,许多特性尚不支持:

  • 不支持特化:常用函数的多个泛型版本,这些版本设计用于处理特定的类型参数
  • 不支持元编程:编写在编译时执行的代码来生成要在运行时执行的代码
  • 泛型函数参数不支持协变和逆变
  • 不支持柯理化(Currying):部分实例化泛型函数或类型
  • 不支持可变类型参数:编写单一的泛型函数,该函数接受不同数量的类型参数和常规参数
  • 不支持非类型参数:非类型参数表示一个值而非一个类型,如type Matrix[n int] [n][n]float64

在Go 1.18中新增了三个实验性质的范型包:

  • golang.org/x/exp/constraints:提供了一些标准的约束
  • golang.org/x/exp/slice:提供了操作任意类型切片的泛型方法
  • golang.org/x/exp/maps:提供了操作任意类型map的泛型方法。

References

Golang泛型提案:go.googlesource.com/proposal/+/…