go

Go 系列 泛型

Posted by lichao modified on September 14, 2022

一句话总结就是:定义一类通用的模板变量,可以传入不同类型的变量,使得逻辑更加通用,代码更加精简。

在 Golang 1.18 版本中利用泛型来实现 Sum 函数的:

1
2
3
4
5
6
func Sum[T int|float64](a,b T) T {
  return a + b
}

fmt.Println(Sum[int](1, 2))  //3
fmt.Println(Sum[float64](1.23, 2.54))  //3.77

泛型针对的是类型变量,在 Golang 中,类型是贯穿整个语法生态的,比如:变量、函数、方法、接口、通道等等。先从单独的泛型变量类型说起。

泛型变量

泛型切片变量

定义一个泛型切片,切片里的值类型,既可以是 int,也可以是 float64,也可以是 string:

1
type Slice1 [T int|float64|string] []T

定义泛型和定义其他类型一样,也是使用 type 关键字,后面的 Slice1 就是泛型变量名,后面紧接一个中括号[]

重点看下 Slice1[] 里面的内容,它是定义泛型的核心:

  • T 表示提炼出来的通用类型参数(Type parameter),是用来表示不同类型的模板,T 只是取的一个通用的名字,可以取任意其他名字。
  • 后面的 int|float64|string 叫类型约束(Type constraint),也就是约束了 T 的取值范围,只能从(int、float64、string)中取值。中间的 表示的是或的关系,等于语法”   “,所以可以根据类型的使用场景定义更多的类型约束。
  • [] 里面的 T int|float64|string,叫类型参数列表(type parameter list),表示的是定义了几个泛型的参数。例子当中只有 1 个。
  • 最后面的 [] T 就是申请一个切片类型,比如常见的:[]int,[]string 等等,只不过这里的类型是 T,也就是参数列表里面定义的变量值。

把这整个类型,就叫做Slice1[T],它是一个切片泛型变量。

所以,总结一下:把需要用到的类型参数,提前在[]里进行定义,然后在后面实际的变量类型中进行使用,必须要先定义,后使用。上面的写法,按照它的类型约束的范围,拆开后,就等同这样:

1
2
3
type SliceInt []int
type SliceFloat []float64
type SliceInt []string

(二)泛型map变量

同理,可以定义其他类型的泛型变量,定义 Map1[KEY, VALUE]泛型变量,它是一个map类型的,其中类型参数KEY的类型约束是int|string,类型参数VALUE的类型约束为string|float64。它的类型参数列表有 2 个,是:KEY int|string, VALUE string| float64

1
type Map1 [KEY int|string, VALUE string| float64] map[KEY]VALUE

拆开来看,它等同于下面的集合:

1
2
3
4
type Map2 map[int]string
type Map3 map[int]float64
type Map4 map[string]string
type Map5 map[string]float64

(三)泛型结构体变量

创建结构体的泛型变量。其中的泛型参数T,有 3 个类型约束。

1
2
3
4
type Struct1 [T string|int|float64] struct {
  Title string
  Content  T
}

拆开来看,它等于下面的集合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Struct3 struct {
  Title string
  Content  string
}

type Struct4 struct {
  Title string
  Content  int
}

type Struct5 struct {
  Title string
  Content  float64
}

(四)泛型变量实例化

在泛型里面,如果去要实例化一个泛型变量,需要去显示的申明实际传入的变量(也就是实参)是什么类型,用它去替换 T。所以这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 申明一个泛型切片
type Slice1 [T int|float64|string] []T

// 实例化成 int 型的切片,并赋值,T 的类型和后面具体值的类型保持一致。
var MySlice1 Slice1[int] = []int{1,2,3}

// 或者简写
MySlice2 := Slice1[int]{1, 2, 3}

// 实例化成 string 型的切片,并赋值, T的类型和后面具体值的类型保持一致。
var MySlice3 Slice1[string] = []string{"hello", "small", "yang"}

// 或者简写
MySlice4 := Slice1[string]{"hello", "small", "yang"}


// 实例化成 float64 型的切片,并赋值, T 的类型和后面具体值的类型保持一致。
var MySlice5 Slice1[float64] = []float64{1.222, 3.444, 5.666}

// 或者简写
MySlice6 := Slice1[float64]{1.222, 3.444, 5.666}

map 类型的泛型变量实例化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//申明
type Map1[KEY int | string, VALUE string | float64] map[KEY] VALUE

//实例化:KEY和VALUE要替换成具体的类型。map里面的也要保持一致
var MyMap1 Map1[int, string] = map[int]string{
  1: "hello",
  2: "small",
}

//或者这简写
MyMap2 := Map1[int, string]{
  1: "hello",
  2: "small",
}

fmt.Println(MyMap1, MyMap2) // map[1:hello 2:small]

//实例化:KEY 和 VALUE 要替换成具体的类型。map 里面的也要保持一致
var MyMap3 Map1[string, string] = map[string]string{
  "one": "hello",
  "two": "small",
}

//或者这样简写
MyMap4 := Map1[string, string]{
  "one": "hello",
  "two": "small",
}

fmt.Println(MyMap3, MyMap4) // map[one:hello two:small]

结构体泛型变量实例化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//定义1个结构体泛型变量
type Struct1 [T string|int|float64] struct {
  Title string
  Content  T
}

//先实例化成float64
var MyStruct1 Struct1[float64]

//再赋值
MyStruct1.Title = "hello"
MyStruct1.Content = 3.149

//或者这样简写
var MyStruct2 = Struct1[string]{
  Title:   "hello",
  Content: "small",
}

fmt.Println(MyStruct1, MyStruct2) //hello 3.149} {hello small}

go无法识别这个匿名写法,不支持匿名泛型结构体

(五)泛型变量嵌套

像常量申明的变量类型支持嵌套一样,泛型变量也是支持嵌套的。把上面几种情况结合一下,来一个复杂点的例子:

在泛型参数列表中,定义了 2 个泛型变量,1 个是 S,另一个是嵌套了 S 的 map 泛型变量 P

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
type MyStruct[S int | string, P map[S]string] struct {
  Name    string
  Content S
  Job     P
}

// 值得注意的是,T 和 S 要保持实参的一致。

//实例化 int 的实参
var MyStruct1 = MyStruct[int, map[int]string]{
  Name:    "small",
  Content: 1,
  Job:     map[int]string{1: "ss"},
}

fmt.Printf("%+v", MyStruct1) // {Name:small Content:1 Job:map[1:ss]}

//实例化string的实参
var MyStruct2 = MyStruct[string, map[string]string]{
  Name:    "small",
  Content: "yang",
  Job:     map[string]string{"aa": "ss"},
}

fmt.Printf("%+v", MyStruct2)  //{Name:small Content:yang Job:map[aa:ss]}

再来看一下稍复杂的例子,2 个泛型变量之间的嵌套使用,Struct1 这个结构体切片,它的第二个泛型参数的类型是 Slice1

1
2
3
4
5
6
7
8
// 切片泛型
type Slice1[T int | string] []T

// 结构体泛型,它的第二个泛型参数的类型是第一个切片泛型。
type Struct1[P int | string, V Slice1[P]] struct {
  Name  P
  Title V
}

这种情况,如何实例化呢?好像有点复杂的样子,无法下手。但是,万变不离其宗,请始终记住:在泛型里面,如果去要实例化一个泛型变量,需要去用实际传入的变量类型去替换 T

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 实例化切片
mySlice1 := Slice1[int]{1, 2, 3}

// 用 int 去替换 P, 用 Slice1 去替换 Slice1[p]
myStruct1 := Struct1[int, Slice1[int]]{
  Name:  123,
  Title: []int{1, 2, 3},
}

// 用 int 去替换 P, 用 Slice1 去替换 Slice1[p]
myStruct2 := Struct1[string, Slice1[string]]{
  Name:  "hello",
  Title: []string{"hello", "small", "yang"},
}

fmt.Println(mySlice1, myStruct1, myStruct2) //[1 2 3]  {123 [1 2 3]}  {hello [hello small yang]}

最后再来看另一种嵌套的方式,看起来更复杂。直接来看这个例子:

1
2
type Slice1[T int|float64|string] []T
type Slice2[T int|string] Slice1[T]

当然这个例子本身是没有任何的意义,只是抱着学习的角度去这样尝试,那么如何实例化呢?通过上面的学习,应该就很简单了:

1
2
3
mySlice1 := Slice1[int]{1, 2, 3, 4}
mySlice2 := Slice2[string]{"hello", "small"}
fmt.Println(mySlice1, mySlice2) //[1 2 3 4] [hello small]

泛型函数

(一)泛型函数的申明**

计算2个数之和

1
2
3
func Sum[T int|float64](a,b T) T {
  return a + b
}

写法与泛型变量写法基本类似:

  • Sum 是函数名,这个和普通的函数一样。
  • Sum 后面紧接着一个[],这个就是申明泛型参数的地方,和泛型变量一样,例子中只申请了 1 个参数类型 T
  • T 后面接着的 int | float64 就是这个参数T的类型约束,也就是取值范围,这个和泛型变量一致。
  • []后面的(a,b T)是函数的调用参数,表示有 2 个参数,类型都是T
  • ()后面T则表示函数的返回值的类型,和普通函数的返回值写法一样,不过这里表示返回值的类型是T

(二)泛型函数的调用

可以这样去调用一下这个函数:

1
2
3
4
5
6
7
8
//传入int的实参,返回值类型也是int
intSum := Sum[int](1, 2)

//传入float64的实参,返回值类型也是float64
float64Sum := Sum[float64](1.23, 2.45)

fmt.Println(intSum, float64Sum) //3 3.68

泛型函数的调用和泛型变量实例化一样,就是得显示的申明一下实际的这个 T,到底是什么类型的。

但是,这种调用写法也太奇怪了,完全不像是 go 语言,反倒是像是一门新语言一样,所以,贴心的 go 官方,允许这样写:

1
2
3
4
5
intSum := Sum(1, 2)

float64Sum := Sum(1.23, 2.45)

fmt.Println(intSum, float64Sum) //3 3.68

类型约束

1
2
3
4
5
6
7
8
9
type MyNumber interface {
  int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64
}

func Foreach[T MyNumber](list []T) {
  for _, t := range list {
    fmt.Println(t)
  }
}

—;

并集:

因为接口类型是支持嵌套使用的。可以继续拆分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type myInt interface {
    int | int8 | int16 | int32 | int64
}

type myUint interface {
    uint | uint8 | uint16 | uint32
}

type myFloat interface {
    float32 | float64
}

func Foreach[T myInt | myUint | myFloat](list []T) {
  for _, t := range list {
    fmt.Println(t)
  }
}

这样就进一步解耦了,3 个类型独立分开,然后在函数Foreach的类型列表中,再用|进行集合,有点像是几个集合取并集。或者,可以进一步的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type myInt interface {
    int | int8 | int16 | int32 | int64
}

type myUint interface {
    uint | uint8 | uint16 | uint32
}

type myFloat interface {
    float32 | float64
}

type myNumber interface {
  myInt | myUint | myFloat
}

func Foreach[T myNumber](list []T) {
  for _, t := range list {
    fmt.Println(t)
  }
}

这样就可以单独控制了,虽然代码量大了一些,但是总体的可读性和美观度以及后续的迭代都强了不少。

—;

交集:

上面的各个自定义的约束类似都是采用交集的形式合并的,那么,它同样也可以采用交集的方式,只不过写法有一点区别,需要换行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type myInt interface {
    int | int8 | int16 | int32 | int64
}

type myInt2 interface {
    int | int64
}

type myFloat interface {
    float32 | float64
}

//每一个自定义约束类型单独一行
type myNumber interface {
  myInt
  myInt2
}

交集如果为空的话,没有意义。传任何类型都不行。因为 go 里面的任何值类型都不是空集,都是有类型的。

any\comparable\Ordered 约束类型

你或多或少从一些文章或者文档里,看到过any这个约束类型。听这个单词的意思,好像是代表任何,比如下面这个例子:

1
2
3
func add[T any] (a, b T) {
  
}

通过上面的一系列分析,已经知道any就是代表一个类型约束,但是并没有定义过它,说明它是系统提供的,是一个全局可用的。可以通过编辑器的跳转功能,查看下这个any的源码是怎么定义的。

/usr/local/go/src/builtin/builtin.go 里可以看到:

1
2
// any is an alias for interface{} and is equivalent to interface{} in all ways.
type any = interface{}

```go
//相等
type MySmall interface{}
type MySmall any

//相等
scans := make([]interface{}, 6)
scans := make([]any, 6)

甚至可以通过全文搜索替换的方式,将老的代码中的interface{} 替换成any

所以,总结一下,当申明1个约束类似为any的时候,它表示的就是任意类型。

但是有时候,any并不是万能可用的,比如,计算2个数之和,如果使用any约束的话,编辑器就会直接报错了:

1
2
3
4
5
6
7
func Sum[T any] (a, b T) T {
return a+b
}

//报错:
invalid operation: operator + not defined on a (variable of type T constrained by any)

分析一下,为啥会报错呢?因为 go 里面有些类型是不能进行+操作的。比如2bool值,就无法进行+操作。那可能会说实际传值的时候,我规避掉这些不能+的字符类型,不就可以了嘛?那当然不行。因为既然申请 1 个泛型变量,就相当于创建了一个通用的模板,是必须得满足所有的变量类型的。

鉴于这种情况,官方给出了 2 个约束类型关键词:comparableconstraints.Ordered。从字母意思可以看得出来,前者是约束了可比较(==、!==),后者约束了可排序 (<、<=、>=、>)。

所以这两者结合起来,就可以实现比较 2 个数字的大小和相等关系了。

值得注意的是:Go 官方团队在 Go1.18 Beta1 版本的标准库里因为泛型设计而引入了 ontraints 包。但是由于大家都泛滥的使用了,所以在 go1.18 正式版本中又将这个包又移除了,放入到扩展/x/exp里面了,想用的话,可以自行下载:

1
2
3
4
5
go get golang.org/x/exp/constraints


go: downloading golang.org/x/exp v0.0.0-20220518171630-0b5c67f07fdf
go: added golang.org/x/exp v0.0.0-20220518171630-0b5c67f07fdf

看下怎么去申明一个可排序的泛型函数例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//导入constraints包
import (
  "fmt"
  "golang.org/x/exp/constraints"
)

//T的约束类型是:constraints.Ordered
func Max[T constraints.Ordered](a, b T) T {
  if a > b {
    return a
  } else {
    return b
  }
}

这样,就约束好了,传入的T 的实参,必须是可排序,也就是满足这几个:(<、<=、>=、>)。才能去调用实例化这个函数。源码看下Orderd是怎么定义的:

1
2
3
type Ordered interface {
  Integer | Float | ~string
}

可以很清晰的看出,它采用了自定义约束类型嵌套的方式,嵌套了好几个自定义的约束类型。最后的这个~string是啥意思呢?我们接下来会讲。

这样,我们就可以实例化调用这个Max函数了:

1
2
3
4
5
6
fmt.Println(Max[int](1, 2))  // 2
fmt.Println(Max[float64](1.33, 2.44))  //2.44
fmt.Println(Max[string]("hello", "small"))  //small

//省去传入的泛型变量的类型,由系统自行推导:
fmt.Println(Max("4", "5")) // 5

说完了Orderd,我们快速的来看下comparable约束类型,这个目前是内置的,可通过编辑器调整看这个约束是如何定义的,可以看出比较的类型还挺多。

1
2
3
4
5
6
7
// comparable is an interface that is implemented by all comparable types
// (booleans, numbers, strings, pointers, channels, arrays of comparable types,
// structs whose fields are all comparable types).
// The comparable interface may only be used as a type parameter constraint,
// not as the type of a variable.
type comparable interface{ comparable }

值得注意的是,这个comparable,是比较==或者!==,不能比较大小,别和Orderd搞混淆了,可以这样使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 比较bool
fmt.Println(Match(true, true)) // ture

// 比较number
fmt.Println(Match(1, 2))  //false
fmt.Println(Match(1.45, 2.67)) //false

// 比较string
fmt.Println(Match("hello", "hello"))  //true

// 比较指针
var age int = 28
var sex int = 1
p1 := &age
p2 := &sex
fmt.Println(Match(p1, p2))  //false

// channel 的比较
ch1 := make(chan int, 1)
ch2 := make(chan int, 1)
fmt.Println(Match(ch1, ch2)) // true

//比较数组,注意!不能是切片
fmt.Println(Match([2]int{1, 2}, [2]int{3, 4}))  //false



//结构体的比较
type MyStruct struct {
  Name string
  Age  int
}
s1 := MyStruct{"yang", 18}
s2 := MyStruct{"small", 18}
fmt.Println(Match(s1, s2))  //false

约束类型

在 go 泛型中,它表示一个类型的超集。举个例子:

1
2
3
type MyInt interface {
  ~int | ~int64
}

则表示,这个约束的范围,不仅仅是 int 和 int64 本身,也包含只要最底层的是这 2 种类型的,都包含。那么啥时候会碰到这种情况呢?其实就是嵌套或者自定义类型的时候。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 申明1个约束范围
type IntAll interface {
  int | int64 | int32
}

// 定义1个泛型切片
type MySliceInt[T IntAll] []T

// 正确:
var MyInt1 MySliceInt[int]

// 自定义一个int型的类型
type YourInt int

// 错误:实例化会报错
var MyInt2 MySliceInt[YourInt]

运行后会发现,第二个会报错,因为 MySliceInt允许的是int作为类型实参,而不是YourInt, 虽然YourInt类型底层类型是int,但它依旧不是int类型)。

这个时候~就排上用处了,我们可以这样写就可以了,表示底层的超集类型。

1
2
3
type IntAll interface {
  ~int | ~int64 | ~int32
}

泛型方法

接下来,我们来看下 go 中如何泛型方法,首先需要指出的是 go 里面的方法指的是接收器类型(receiver type),我们经常会用这种方式来实现其他语言中类的作用。比如下面这个例子:

1
2
3
4
5
type DemoInt int

func (t DemoInt) methodName(param string) string {

}

我们看这种类型,不管是前面的(t DemoInt) 还是方法名后面参数 (param string) 里面都会涉及到具体的类型变量,所以都可以改造成泛型。我们先来看下接收器(t DemoInt) 如何改照成泛型。

接收器泛型

我们先定义 1 个泛型变量,然后在这个变量上加上 1 个方法,试着写一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 申请一个自定义的泛型约束类型
type NumberAll interface {
  ~int|~int64|~int32|~int16|~int8|~float64|~float32
}

// 申请一个泛型切片类型,泛型参数是T,约束的类型是 NumberAll
type SliceNumber[T NumberAll] []T


// 给泛型切片加上1个接收器方法
func (s SliceNumber[T]) SumIntsOrFloats() T {
  var sum T
  for _, v := range s {
    sum += v
  }
  return sum
}

注意一下 (s SliceNumber[ T]) 这个写法,T 后面是不用带上它的约束类型 NumberAll 的。然后返回值也是 T 类型。这样之后就完成了一个泛型接收器方法。

那么如何去调用呢?其实和普通的接收器方法是一样的,只不过得先去实例化泛型切片。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//实例化成int
var ss1 SliceNumber[int] = []int{1, 2, 3, 4}

//简化
//ss1 := SliceNumber[int]{1, 2, 34}

ss1.SumIntsOrFloats() // 10

//实例化成float64
var ss2 SliceNumber[float64] = []float64{1.11, 2.22, 3.33} 

//简化
//ss2 := SliceNumber[float64]{1.11, 2.22, 3.33}

ss2.SumIntsOrFloats()   //6.66

这种泛型方法的运用,在实际上的很多场景都是很好用的的,比如不同类型的堆栈的入栈和出栈,这也是一个很经典的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 自定义一个类型约束
type Number interface{
  int | int32 | int64 | float64 | float32 
}


// 定义一个泛型结构体,表示堆栈
type Stack[V Number] struct {
  size  int
  value []V
}

// 加上 Push 方法
func (s *Stack[V]) Push(v V) {
  s.value = append(s.value, v)
  s.size++
}

// 加上 Pop 方法
func (s *Stack[V]) Pop() V {
  e := s.value[s.size-1]
  if s.size != 0 {
    s.value = s.value[:s.size-1]
    s.size--
  }
  return e
}

可以传入不同的类型数据去实例化调用一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//实例化成一个int型的结构体堆栈
s1 := &Stack[int]{}

//入栈
s1.Push(1)
s1.Push(2)
s1.Push(3)
fmt.Println(s1.size, s1.value)  // 3 [1 2 3]

//出栈
fmt.Println(s1.Pop())  //3
fmt.Println(s1.Pop())  //2
fmt.Println(s1.Pop())  //1

// 实例化成一个float64型的结构体堆栈
s2 := &Stack[float64]{}
s2.Push(1.1)
s2.Push(2.2)
s2.Push(3.3)
fmt.Println(s2.Pop())  //3.3
fmt.Println(s2.Pop())  //2.2
fmt.Println(s2.Pop())  //1.1

方法的参数泛型

说完接收器泛型之后,我们来看下第二种泛型的方式,就是方法的参数泛型,就是接收器是一个普通的类型,在方法的参数里面来设置泛型变量。我们尝试着写一下:

1
2
3
4
5
type DemoSlice []int

func (d DemoSlice) FindOne[T int](a T) bool {
  
}

你会发现,你方法里面的逻辑都还没开始写,编辑器就会标红报错了:Method cannot have type parameters。方法不能有类型参数,即:方法是不支持泛型的。至少目前的1.18版本是不支持的。看后续版本会不会支持的。

既然,函数是支持泛型的,接收器也是支持函数的,所以我们把他们结合起来,稍加改造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type DemoSlice[T int | float64] []T

func (d DemoSlice[T]) FindOne(a T) bool {
  for _, t := range d {
    if t == a {
      return true
    }
  }
  return false
}

s1 := DemoSlice[int]{1, 2, 3, 4}
fmt.Println(s1.FindOne(1))

s2 := DemoSlice[float64]{1.2, 2.3, 3.4, 4.5}
fmt.Println(s2.FindOne(1.2))

泛型接口

1
2
3
4
5
6
7
8
type error interface {
  Error() string
}


type DemoNumber interface {
  int | float64
}

上面 2 个都采用 interface 申明,1 个是传统的接口类型,1 个是约束类型,有啥区别呢?一个叫:方法集,另一个叫:类型集。其实本质上是一样的。传统的接口类型,只要实现了接口里面定义的方法,那就是实现了这个接口。而约束类型,其实也是一样,只要传入的值的类型,在约束范围内,就是符合要求的。

所以,go 在 1.18 版本后,对 interface 的定义改了,改成了:

接口类型定义了一个类型集合。接口类型的变量可以存储这个接口类型集合的任意一种类型的实例值。这种类型被称之为实现了这个接口。接口类型的变量如果未初始化则它的值为 nil。

那如果把这 2 者结合起来呢?

1
2
3
4
type MyError interface {
  int | float64
  Error() string
}

这种写法看着好陌生,里面既有约束类型,又有方法,这是 go1.18 中新增的写法,这种接口叫做:一般接口(General interface)。原先 1.18 之前的接口定义类型叫做:基本接口(Basic interfaces)。

所以总结一下:

  • 如果,1 个接口里面只有方法,也就是老的语法写法,这个接口叫:基本接口。
  • 如果,1 个接口里面,有约束类型的,有或者没有方法的,这个接口叫:一般接口。

(一)基本泛型接口

继续看下如何定义一个泛型接口呢?它的写法和泛型变量是类似的:

1
2
3
4
type MyInterface[T int | string] interface {
  WriteOne(data T) T
  ReadOne() T
}

但是值得注意的是,别写反了,别把泛型参数写到了方法的层面,这样是错误的语法:

1
2
3
4
5
//会提示错误:interface method must have no type parameters
type MyInterface interface {
  WriteOne[T int | string] (data T) T
  ReadOne[T int | string] () T
}

当定义好了上面这个泛型接口,因为里面只有方法,没有约束类型的定义,所以它是个基本接口。那我们看下如何去实现这个基本泛型接口。

先定义 1 个普通的结构体类型,然后通过接收器方式绑定上 2 个方法:

1
2
3
4
5
6
7
8
9
10
11
type Note struct {

}

func (n Note) WriteOne(one string) string {
  return "hello"
}

func (n Note) ReadOne() string {
  return "small"
}

然后,如何实例化泛型接口,并且实现接口。这种写法和普通的实现接口的方式是一直的,只不过要显示的的传入 T 的值是什么。

1
2
3
var one MyInterface[string] = Note{}
fmt.Println(one.WriteOne("hello"))
fmt.Println(one.ReadOne())

值得注意的是泛型参数的值的类型,要和被实现的方法的参数值要保证一致,不然会报错:

1
2
3
4
// 接口实例化用的是int,但是实现的方法里面都是string类型,并不匹配,无法被实现。
var one MyInterface[int] = Note{}
fmt.Println(one.WriteOne("hello"))
fmt.Println(one.ReadOne())

报错如下:

1
2
3
4
cannot use Note{} (value of type Note) as type MyInterface[int] in variable declaration:
        Note does not implement MyInterface[int] (wrong type for ReadOne method)
                have ReadOne() string
                want ReadOne() int

(二)一般泛型接口 我们现在再来定义一个一般泛型接口,也就是说接口里面,有约束类型。看下怎么写:

1
2
3
4
5
6
type MyInterface2[T int | string] interface {
  int|string

  WriteOne(data T) T
  ReadOne() T
}

那这种一般泛型接口如何实例化呢?我们试一试看看:

1
2
3
4
5
6
7
8
9
10
11
12
type Note2 int

func (n Note2) WriteOne(one string) string {
  return "hello"
}

func (n Note2) ReadOne() string {
  return "small"
}


var one MyInterface2[string] = Note{}

编辑器直接标红报错了。提示:

接口包含约束元素int和string,只能用作类型参数。

简而言之,一般泛型接口,只能被当做类型参数来使用,无法被实例化。

1
type myInterface [T MyInterface2[int]] []T

但是这种这么变态的写法,如何实例化呢?这个有待研究,反正至少没报错了。

泛型的利和弊