终于!12年后Golang支持泛型了!(内含10个实例)

Posted QcloudCommunity

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了终于!12年后Golang支持泛型了!(内含10个实例)相关的知识,希望对你有一定的参考价值。

导语 | 泛型是一些语言的标配,可以极大地便利开发者,但Golang在之前并不支持泛型。在今年的Go1.17中已经发布了泛型的体验版,这一功能也是为1.18版本泛型正式实装做铺垫。本文将介绍一下泛型在Golang的使用样例及其泛型的发展历史,需要体验的同学可以使用:https://go2goplay.golang.org/或者自行在docker中安装版本。

一、泛型

(一)什么是泛型

谈泛型的概念,可以从多态看起,多态是同一形式表现出不同行为的一种特性,在编程语言中被分为两类,临时性多态和参数化多态。

临时性多态(Ad hoc Polymorphism),根据实参类型调用相应的版本,仅支持数量十分有限的调用,也被称作特设多态,例如函数重载。

func Add(a, b int) int { return a+b }
func Add(a, b float64) float64 { return a+b } // 注意: Golang中不允许同名函数
Add(1, 2)     // 调用第一个
Add(1.0, 2.0) // 调用第二个
Add(“1”, “2”) // 编译时不检查,运行时找不到实现,崩溃或者编译时直接不通过

参数化多态(Parametric Polymorphism),根据实参生成不同的版本,支持任意数量的调用,即泛型,简言之,就是把元素类型变成了参数。

func Add(a, b T)  T {  return a+b  }
Add(1, 2)              // 编译器生成 T = int 的 Add
Add(float64(1.0), 2.0) // 编译器生成 T = float64 的 Add
Add("1", "2")          // 编译器生成 T = string 的 Add

(二)泛型困境

泛型和其他特性一样不是只有好处,为编程语言加入泛型会遇到需要权衡的两难问题。语言的设计者需要在编程效率、编译速度和运行速度三者进行权衡和选择,编程语言要选择牺牲一个而保留另外两个。

在2009年的时候,Russ Cox提出来的一个关于泛型的问题叫做泛型困境,用来收集人们对Golang中泛型的一些意见和建议,对Golang泛型设计当中的问题进行解释,并表示他们并不急于去实现泛型,因为还没有找到一个合适的实现方案去解决困境。

而泛型困境的本质是,关于泛型,你想要缓慢的程序员、缓慢的编译器和臃肿的二进制文件,还是缓慢的执行时间。简单来说就是:要么苦了程序员,要么苦了编绎器,要么降低运行时效率。

以C、C++和Java为例,它们在泛型的设计上有着不同考量:

  • C语言是系统级的编程语言,没有支持泛型,本身提供的抽象能力非常有限。结果是牺牲了程序员的开发效率,与Golang目前的做法一样,它们都需要手动实现不同类型的相同逻辑。但是不引入泛型的好处也显而易见,那就是降低了编译器实现的复杂度,也能保证源代码的编译速度;

  • C++与C语言的选择完全不同,它使用编译期间类型特化实现泛型,提供了非常强大的抽象能力。虽然提高了程序员的开发效率,不再需要手写同一逻辑的相似实现,但是编译器的实现变得非常复杂,泛型展开会生成的大量重复代码也会导致最终的二进制文件膨胀和编译缓慢,我们往往需要链接器来解决代码重复的问题;

  • Java在1.5版本引入了泛型,它的泛型是用类型擦除实现的。Java的泛型只是在编译期间用于检查类型的正确,为了保证与旧版本JVM的兼容,类型擦除会删除泛型的相关信息,导致其在运行时不可用。编译器会插入额外的类型转换指令,与C语言和C++在运行前就已经实现或者生成代码相比,Java类型的装箱和拆箱会降低程序的执行效率

而C、C++和Java相比,Golang旨在作为一种编写服务器程序的语言,这些程序随着时间的推移易于维护,侧重于可伸缩性、可读性和并发性等多种方面。泛型编程在当时似乎对Golang的目标并不重要,因此为了简单起见被排除在外。

(三)实现泛型的方式

例如下面是一位程序猿自己写的一个实现类似泛型的代码:

二、Golang中的泛型

(一)关于Go泛型的开发进度

Go是一门强类型语言,意味着程序中的每个变量和值都有某种特定的类型,例如int、string等。没有泛型,很多人以此“鄙视”Golang。当然,也有人觉得根本不需要泛型。有泛型,不代表你一定要用。在复用代码等场景下,泛型还是很有必要和帮助的。比如:

func Add(a, b int) int
func AddFloat(a, b float64) float64

在泛型的帮助下,上面代码就可以简化成为:

func Add[T any](a, b T) T
  • Add后面的[T any],T表示类型的标识,any表示T可以是任意类型。

  • a、b和返回值的类型T和前面的T是同一个类型。

  • 为什么用[],而不是其他语言中的<>,官方有过解释,大概就是<>会有歧义。曾经计划使用() ,因为太容易混淆,最后使用了[]。

Golang团队一直在尝试泛型的设计,之前也有很多的努力和尝试,包括各种泛型提案和实现方式,但最后都被否决了。Golang核心作者给出的解释是泛型并不是不可或缺的特性,属于重要但不紧急,应该把精力集中在更重要的事情上,例如GC的延迟优化,编译器自举等。现在他们认为Goalng现在更加成熟了,加上目前泛型是Golang社区呼声最高的,希望被尽快实现的语言特性,因此,可以考虑某种形式的泛型编程。

目前,在1.17的版本中Golang终于推出来泛型的尝鲜版了,官方目前预计此更改将在2022年初的Go1.18版本中可用(We currently expect that this change will be available in the Go1.18 release in early 2022.)。而泛型,是Golang多年来最令人兴奋和根本性的变化之一。

(二)Golang1.17中泛型的要点

  • 函数可以通过type关键字引入额外的类型参数(type parameters)列表:func F(type T)(p T) { ... } 。

  • 这些类型参数可以像一般的参数一样在函数体中使用。

  • 类型也可以拥有类型参数列表:type M(type T) []T。

  • 每个类型参数可以拥有一个约束:func F(type T Constraint)(p T) { ... }。

  • 使用interface来描述类型的约束。

  • 被用作类型约束的interface可以拥有一个预声明类型列表,限制了实现此接口的类型的基础类型。

  • 使用泛型函数或类型时需要传入类型实参。

  • 一般情况下,类型推断允许用户在调用泛型函数时省略类型实参。

  • 如果类型参数具有类型约束,则类型实参必须实现接口。

  • 泛型函数只允许进行类型约束所规定的操作。


(三)如何使用

  • 可以直接在https://go2goplay.golang.org/进行测试使用。

  • 在docker中装个1.17的版本,运行时用go run-gcflags=-G=3~/main.go命令就好了。

三、Golang泛型案例

(一)如何对泛型进行输出

下面的例子是一个对泛型输出的基本例子。函数可以有一个额外的类型参数列表,它使用方括号,但看起来像一个普通的参数列表:func F[T any](p T) { ... },代码中的[T any]即为类型参数,意思是该函数支持任何T类型,当我们调用printSlice[string]([]string{“Hello”,“World”})时,会被类型推导为string类型,不过在编译器完全可以实现类型推导时,也可以省略显式类型,如:printSlice([]string{“Hello”,“World”}) ,这样也将会是对的;

package main


import (
  "fmt"
)


func printSlice[T any](s []T) {
  for _, v := range s {
    fmt.Printf("%v ", v)
  }
  fmt.Print("\\n")
}


func main() {
  printSlice[int]([]int{1, 2, 3, 4, 5})
  printSlice[float64]([]float64{1.01, 2.02, 3.03, 4.04, 5.05})
  printSlice([]string{"Hello", "World"})
  printSlice[int64]([]int64{5, 4, 3, 2, 1})
}
输出为:
1 2 3 4 5 
1.01 2.02 3.03 4.04 5.05 
Hello World 
5 4 3 2 1

(二)如何用泛型约束使用的类型范围

这个例子包含了一个类型约束。每个类型参数都有一个类型约束,就像每个普通参数都有一个类型:func F[T Constraint](p T) { ... },类型约束是接口类型。该提案扩展了interface语法,新增了类型列表(type list)表达方式,专用于对类型参数进行约束。

package main


import (
  "fmt"
)


type Addable interface {
  type int, int8, int16, int32, int64,
    uint, uint8, uint16, uint32, uint64, uintptr,
    float32, float64, complex64, complex128,
    string
}


func add[T Addable] (a, b T) T {
  return a + b
}


func main() {
  fmt.Println(add(1,2))
  fmt.Println(add("hello","world"))
}
输出为:
3
helloworld

在官方的最新proposal里有提到,在Golang中,并不是所有的类型都满足+号运算。在1.17的版本中,泛型函数只能使用类型参数所能实例化出的任意类型都能支持的操作。

比如下面的add函数的类型参数T没有任何约束,它可以被实例化为任何类型;那么这些实例化后的类型是否都支持+操作符运算呢?显然不是;因此,报错了!对于没有任何约束的类型参数实例,允许对其进行的操作包括:

  • 声明这些类型的变量。

  • 使用相同类型的值为这些变量赋值。

  • 将这些类型的变量以实参形式传给函数或从作为函数返回值。

  • 取这些变量的地址。

  • 将这些类型的值转换或赋值给interface{}类型变量。

  • 通过类型断言将一个接口值赋值给这类类型的变量。

  • 在type switch块中作为一个case分支。

  • 定义和使用由该类型组成的复合类型,比如:元素类型为该类型的切片。

  • 将该类型传递给一些内置函数,比如new。

这就意味着,如果不用interface约束,直接使用的话,你讲得到如下的结果:

package main


import (
  "fmt"
)
func add[T any] (a, b T) T {
  return a + b
}


func main() {
  fmt.Println(add(1,2))
  fmt.Println(add("hello","world"))
}
输出:
type checking failed for main
prog.go2:8:9: invalid operation: operator + not defined for a (variable of type parameter type T)

在约束里,甚至可以放进去接口如下:

package main


import (
  "fmt"
)


type Addable interface {
  type int,interface{}
}


func add[T Addable] (a T) T {
  return a
}


func main() {
  fmt.Println(add(1))
}

接着假如我们去掉string,如下代码所示。以该示例为例,如果编译器通过类型推导得到的类型不在这个接口定义的类型约束列表中,那么编译器将允许这个类型参数实例化;否则就像类型参数实例化将报错!

package main


import (
  "fmt"
)


type Addable interface {
  type int, int8, int16, int32, int64,
    uint, uint8, uint16, uint32, uint64, uintptr,
    float32, float64, complex64, complex128
}


func add[T Addable] (a, b T) T {
  return a + b
}


func main() {
  fmt.Println(add(1,2))
  fmt.Println(add("hello","world"))
}
输出为:
type checking failed for main
prog.go2:19:14: string does not satisfy Addable (string or string not found in int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr, float32, float64, complex64, complex128)

注意:我们自己定义的带有类型列表的接口将无法用作接口变量类型,如下代码将会报错

package main


type MyType interface {
  type int
}


func main() {
  var n int = 6
  var i MyType
  i = n
  _ = i
}
输出为:
type checking failed for main
prog.go2:9:8: interface contains type constraints (int)

(三)泛型中的接口本身对范型进行约束

package main


import (
  "fmt"
  "strconv"
)


type MyStringer interface {
  String() string
}


type StringInt int
type myString string


func (i StringInt) String() string {


  return strconv.Itoa(int(i))
}


func (str myString) String() string {
  return string(str)
}


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


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


func main() {
  fmt.Println(stringify([]StringInt{1, 2, 3, 4, 5}))
  fmt.Println(stringify2([]myString{"1", "2", "3", "4", "5"}))
}
输出为:
[1 2 3 4 5]
[1 2 3 4 5]

代码中我们声明了MyStringer接口,并且使用StringInt和myString类型实现了此接口;在范型方法中,我们声明了范型的类型为:任意实现了MyStringer接口的类型;只要实现了这个接口,那么你就可以直接使用,在现在某些需要传interface{}作为参数的函数里面,可以直接指定类型了。当你改为如下代码时

func main() {
  fmt.Println(Stringify([]int{1, 2, 3, 4, 5}))
}

会报错:

输出为:
type checking failed for main
prog.go2:27:14: int does not satisfy MyStringer (missing method String)

只有实现了Stringer接口的类型才会被允许作为实参传递给Stringify泛型函数的类型参数并成功实例化!当然也可以将MyStringer接口写成如下的形式:

type MySignedStringer interface {
  type int, int8, int16, int32, int64
  String() string
}

表示只有int, int8, int16, int32, int64,这样类型参数的实参类型既要在MySignedStringer的类型列表中,也要实现了MySignedStringer的String方法,才能使用。像这种不在里面的type StringInt uint就会报错。

(四)泛型中如何操作切片

可以看到在下面的例子里面,我们声明了一个可以存放任何类型的切片,叫做slice,如type slice[T any] []T。和泛型函数一样,使用泛型类型时,首先要对其进行实例化,即显式为类型参数赋值类型。如果在类型定义时,将代码改成vs:=slice{5,4,2,1},那么你会得到如note1中的结果。因为编译器并没有办法进行类型推导,也就是表示它并不知道,你输出的是那种类型。哪怕你在interface里面定义了约束。哪怕你在接口中定义了类型约束type int, string,同样会报错,如note2所示。

package main


import (
  "fmt"
)


type slice[T any] []T


/*
type any interface {
  type int, string
}*/


func printSlice[T any](s []T) {
  for _, v := range s {
    fmt.Printf("%v ", v)
  }
  fmt.Print("\\n")
}


func main() {
  // note1: cannot use generic type slice[T interface{}] without instantiation
  // note2: cannot use generic type slice[T any] without instantiation
  vs := slice[int]{5, 4, 2, 1}
  printSlice(vs)


  vs2 := slice[string]{"hello", "world"}
  printSlice(vs2)
}
输出为:
5 4 2 1 
hello world

(五)如何利用泛型实现最大值最小值函数

package main


import (
  "fmt"
)


type minmax interface {
  type int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr, float32, float64
}


func max[T minmax](a []T) T {
  m := a[0]
  for _, v := range a {
    if m < v {
      m = v
    }
  }
  return m
}


func min[T minmax](a []T) T {
  m := a[0]
  for _, v := range a {
    if m > v {
      m = v
    }
  }
  return m
}


func main() {
  vi := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
  result := max(vi)
  fmt.Println(result)


  vi = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
  result = min(vi)
  fmt.Println(result)
}
输出为:
10
1

(六)如何使用Golang泛型自带的comparable约束

当你写成如下代码时,便会报错:

package main


import (
  "fmt"
)


func findFunc[T any](a []T, v T) int {
  for i, e := range a {
    if e == v {
      return i
    }
  }
  return -1
}


func main() {
  fmt.Println(findFunc([]int{1, 2, 3, 4, 5, 6}, 5))
}
输出为:
type checking failed for main
prog.go2:9:6: cannot compare e == v (operator == not defined for T)

因为不是所有的类型都可以==比较,所以Golang内置提供了一个comparable约束,表示可比较的。参考下面代码:

package main




import (
  "fmt"
)


func findFunc[T comparable](a []T, v T) int {
  for i, e := range a {
    if e == v {
      return i
    }
  }
  return -1
}


func main() {
  fmt.Println(findFunc([]int{1, 2, 3, 4, 5, 6}, 5))
}
输出为:
4

(七)如何在泛型中操作指针

package main


import (
  "fmt"
)


func pointerOf[T any](v T) *T {
  return &v
}


func main() {
  sp := pointerOf("foo")
  fmt.Println(*sp)


  ip := pointerOf(123)
  fmt.Println(*ip)
  *ip = 234
  fmt.Println(*ip)
}
输出为:
foo
123
234

(八)Golang泛型中如何操作map

在现实开发过程中,我们往往需要对slice中数据的每个值进行单独的处理,比如说需要对其中数值转换为平方值,在泛型中,我们可以抽取部分重复逻辑作为map函数:

package main


import (
  "fmt"
)


func mapFunc[T any, M any](a []T, f func(T) M) []M {
  n := make([]M, len(a), cap(a))
  for i, e := range a {
    n[i] = f(e)
  }
  return n
}


func main() {
  vi := []int{1, 2, 3, 4, 5, 6}
  vs := mapFunc(vi, func(v int) string {
    return "<" + fmt.Sprint(v*v) + ">"
  })
  fmt.Println(vs)
}
输出为:
[<1> <4> <9> <16> <25> <36>]

(九)如何在Golang泛型中使用队列操作

在现实开发过程中,我们有可能会需要一个队列去处理一些数据,在泛型中,我们可以抽取部分重复逻辑来实现

package main


import (
  "fmt"
)


type queue[T any] []T


func (q *queue[T]) enqueue(v T) {
  *q = append(*q, v)
}


func (q *queue[T]) dequeue() (T, bool) {
  if len(*q) == 0 {
    var zero T
    return zero, false
  }
  r := (*q)[0]
  *q = (*q)[1:]
  return r, true
}


func main() {
  q := new(queue[int])
  q.enqueue(5)
  q.enqueue(6)
  fmt.Println(q)
  fmt.Println(q.dequeue())
  fmt.Println(q.dequeue())
  fmt.Println(q.dequeue())
}
输出为:
&amp;[5 6]
5 true
6 true
0 false

(十)Golang泛型中新加入的一些约束包

官方也引入了一些官方包来方面泛型的使用,具体如下:

// constraints 定义了一组与类型参数一起使用的约束
package constraints


// Signed是允许任何有符号整数类型的约束。
type Signed interface { ... }


// Unsigned是允许任何无符号整数类型的约束。
type Unsigned interface { ... }


// Integer是允许任何整数类型的约束。
type Integer interface { ... }


// Float是一个允许任何浮点类型的约束。
type Float interface { ... }


// Complex是允许任何复杂数值类型的约束。
type Complex interface { ... }


// Ordered是一个约束,允许任何有序类型:任何支持操作符< <= >= >的类型。
type Ordered interface { ... }

使用方式示例如下:

package main


import (
  "constraints"
  "fmt"
)


type v[T constraints.Ordered] T


type Vector[T constraints.Ordered] struct {
  x, y T
}


func (v *Vector[T]) Add(x, y T) {
     v.x += T(x)
     v.y += T(y)
}


func (v *Vector[T]) String() string {
     return fmt.Sprintf("{x: %v, y: %v}", v.x, v.y)
}


func NewVector[T constraints.Ordered](x, y T) *Vector[T] {
     return &amp;Vector[T]{x: x, y: y}
}


func main() {
     v := NewVector[float64](1, 2)
     v.Add(2, 3)
    fmt.Println(v)
}

四、总结

尽管最新的proposal冗长而详尽,但总结起来如下:

  • 函数和类型可以具有类型参数,该类型参数使用可选约束(接口类型)定义,约束描述了这些参数所需的方法和允许的类型。

  • 当使用类型参数调用函数时,类型推断通常会允许用户省略类型参数。

  • 泛型函数只能使用约束允许的所有类型支持的操作

  • 此设计完全向后兼容,但建议对func F(x(T))的含义进行更改。


(一)适用性

此外,标准库中将会引入一系列新的package。

  • 一个新slices包将会被引入,它与现存的bytes和strings包类似,用于操作任何类型元素的slice。新的maps和chans包将会提供一些简单的算法,用于操作任意类型的元素。set包也会被引入。

  • 一个新constraints包将会提供一系列标准约束,如“所有整数类型”或“所有数值类型”这类约束。

  • 诸如container/list,container/ring这些包,或者是诸如sync.Map,sync/atomic.Value之类,将会升级到编译时类型安全(使用新的名字或新的版本 )。

  • math包将会为数值类型提供一系列简单的标准算法,比如呼声很高的Min和Max函数。

  • 可能会开发新的特殊容器,这些容器是编译时类型安全的,也可能会增加泛型的sort包。


(二)复杂性

Golang的一大优点是它的简单性。显然,这种设计使语言更加复杂,对于泛型推出,无论采用什么技术,都会增加Golang的复杂性,提升其学习门槛,代码的可读性也可能会下降,官方对其增加的复杂性的解释如下:

  • 对于阅读编写良好的通用代码而不是编写代码的人来说,增加的复杂性很小。

  • 预计大多数包不会定义泛型类型或函数,但许多包可能会使用其他地方定义的泛型类型或函数。

  • 在常见情况下,泛型函数的工作方式与非泛型函数完全相同。


(三)效率

官方目前尚不清楚人们期望从通用代码中获得什么样的效率,他们将其划分为泛型函数和泛型类型。

  • 可以使用基于接口的方法编译泛型函数。这将优化编译时间,因为函数只编译一次,但会有一些运行时间成本。

  • 对于每组类型参数,泛型类型可能被编译多次。这显然会带来编译时间成本,但不应该有任何运行时间成本。编译器还可以选择使用类似于接口类型的方法来实现泛型类型,使用专用方法访问依赖于类型参数的每个元素。

(四)温馨提示

1.17版本的Golang,泛型玩玩就行,不要用到生产中。

五、Golang泛型的发展历史

(一)Type Functions (2010) by Ian Lance Taylor

type Lesser(t) interface {
        Less(t) bool
}
func Min(a, b t type Lesser(t)) t {
        if a.Less(b) {
                return a
        }
        return b
}

关键设计

  • 在标识符后使用(t)作为类型参数的缺省值,语法存在二义性。

  • 既可以表示使用类型参数Greater(t),也可以表示实例化一个具体类型Greater(t),其中t为推导的具体类型,如int。

  • 为了解决二义性,使用type进行限定:Vector(t type)func F(arg0, arg1 t type) t { … }

  • 使用接口Greater(t) 对类型参数进行约束,跟在type后修饰。

提案还包含一些其他的备选语法:

generic(t) func ..
$t // 使用类型参数
t  // 实例化具体类型

评述

  • 确实是一个糟糕的设计。

  • x:= Vector(t)(v0) 这是两个函数调用吗?

  • 尝试借用使用C++的Concepts对类型参数的约束。

(二)Generalized Types (2011) by Ian Lance Taylor

gen [T] type Greater interface {
   IsGreaterThan(T) bool
}
gen [T Greater[T]] func Max(arg0, arg1 T) T {
   if arg0.IsGreaterThan(arg1) {
      return arg0
   }
   return arg1
}


gen [T1, T2] (
  type Pair struct { first T1; second T2 }


  func MakePair(first T1, second T2) Pair {
  return &amp;Pair{first, second}
}
)

关键设计

  • 使用gen [T]来声明一个类型参数

  • 使用接口对类型进行约束

  • 使用gen [T] ( … )来复用类型参数的名称

评述

  • 没有脱离糟糕设计的命运。

  • gen [T] ( … )引入了作用域的概念 需要缩进吗?

  • 除了注释还有更好的方式快速定位作用域的结束吗?

  • 复杂的类型参数声明。

(三)Type Parameters (Dec. 2013) by Ian Lance Taylor

type [T] Greater interface {
   IsGreaterThan(T) bool
}
func [T] Max(arg0, arg1 T) T {
   if arg0.IsGreaterThan(arg1) {
       return arg0
   }
   return arg1
}
type Int int
func (i Int) IsGreaterThan(j Int) bool {
   return i > j
}
func F() {
   _ = Max(0, Int(1)) // 推导为 Int
}

关键设计

  • 直接在类型、接口、函数名前使用 [T] 表示类型参数

  • 进一步细化了类型推导作为约束的可能性

评述

  • 目前为止最好的设计

  • 无显式类型参数的类型推导非常复杂

  • 常量究竟应该被推导为什么类型?

  • [T] 的位置很诡异,声明在左,使用在右,例如:type[T1, T2]Pair struct{ … }、varvPair[T1, T2]

(五)go:generate (2014) by Rob Pike

import "github.com/cheekybits/genny/generic"** 


// cat 201401.go | genny gen "T=NUMBERS" > 201401_gen.go** 


type T generic.Type 


func MaxT(fn func(a, b T) bool, a, b T) T {** 
    if fn(a, b) { 
        return a 
     } 
     return b 
  }

关键设计

  • 通过//go:generate编译器指示来自动生成代码

  • 利用这一特性比较优秀的实现是cheekybits/genny(https://github.com/cheekybits/genny)

评述

  • 维护成本

  • 需要重新生成代码

  • 没有类型检查,需要程序员自行判断

(六)First Class Types (2015) by Bryan C. Mills

const func AsWriterTo(reader gotype) gotype {
  switch reader.(type) {
  case io.WriterTo:
    return reader
  default:
    type WriterTo struct {
      reader
    }
    func (t *WriterTo) WriteTo(w io.Writer) (n int64, err error) {
      return io.Copy(w, t.reader)
    }
    return WriterTo (type)
  }
}


const func MakeWriterTo(reader gotype) func(reader) AsWriterTo(reader) {
  switch reader.(type) {
  case io.WriterTo:
    return func(r reader) AsWriterTo(reader) {
      return r
    }
  default:
    return func(r reader) AsWriterTo(reader) {
      return AsWriterTo(reader) { r }
    }
  }
}

关键设计

  • 引入gotype内建类型

  • 扩展(type)的编译期特性

  • const前缀强化函数的编译期特性

  • 灵感来源C++SFINAE

评述

  • 设计上需要额外思考SFINAE

  • 只有泛型函数的支持,泛型结构需要通过函数来构造

  • 不太可能实现可类型推导

(七)Contracts (2018) by Ian Lance Taylor and Robert Griesemer

contract comparable(x T) {
       x == x
}


func Contains(type T comparable)(s []T, e T) bool {
        for _, v := range s {
                if v == e { // now valid
                        return true
                }
        }
        return false
}

合约是一个描述了一组类型且不会被执行的函数体。 

关键设计

  • 在合约中写Go语句对类型进行保障

  • 甚至写出条件、循环、赋值语句

评述

  • 复杂的合约写法(合约内的代码写法可以有多少种?)

  • 「一个不会执行的函数体」太具迷惑性 实现上估计是一个比较麻烦的问题

(八)Contracts (2019) by Ian Lance Taylor and Robert Griesemer

contract Ordered(T) {
    T int, int8, int16, int32, int64,
        uint, uint8, uint16, uint32, uint64, uintptr,
        float32, float64,
        string
}


func Min (type T Ordered) (a, b T) T {
    if a < b {
        return a
    }
    return b
}

合约描述了一组类型的必要条件。 

关键设计

  • 使用方法及穷举类型来限制并描述可能的参数类型

  • comparable/arithmetic等内建合约

评述

  • 没有算符函数、重载

参考资料:

1.The go2go Playground

2.关于Go泛型 (Generics)

3.Contracts—Draft Design

4.Compile-time Functions and First Class Types

 作者简介

喻佳鑫

腾讯后台开发工程师

腾讯后台开发工程师,毕业于中南大学,腾讯看点频道推荐文章索引构建等后端开发工作。

 推荐阅读

揭秘!用标准Go语言能写脚本吗?

大咖共探万物智联时代风云!Techo TVP物联网开发者峰会圆满落幕

Node.js内存泄漏的原因竟然是……?

超详细教程!手把手带你使用Raft分布式共识性算法


以上是关于终于!12年后Golang支持泛型了!(内含10个实例)的主要内容,如果未能解决你的问题,请参考以下文章

Go1.18都出泛型了,不来试试看?

Go1.18都出泛型了,不来试试看?

Win10在列,20年后微软终于修复Windows系统BadTunne

Golang 中"泛型"的支持

go1.18beta1 泛型demo: hashmap

Go 语言泛型编程之切片