本文最后更新于:2024年11月10日 下午
Go 泛型
泛型参数
类型形参、类型实参、类型约束和泛型类型
1 2 3 4 type IntSlice []int var a IntSlice = []int {1 , 2 , 3 } var b IntSlice = []float32 {1.0 , 2.0 , 3.0 }
这里定义了一个新的类型 IntSlice
,它的底层类型是 []int
,理所当然只有 int 类型的切片能赋值给 IntSlice
类型的变量。
接下来如果我们想要定义一个可以容纳 float32
或 string
等其他类型的切片的话该怎么办?很简单,给每种类型都定义个新类型:
1 2 3 type StringSlice []string type Float32Slie []float32 type Float64Slice []float64
但是这样做的问题显而易见,它们结构都是一样的只是成员类型不同就需要重新定义这么多新类型。那么有没有一个办法能只定义一个类型就能代表上面这所有的类型呢?答案是可以的,这时候就需要用到泛型了:
1 type Slice[T int |float32 |float64 ] []T
不同于一般的类型定义,这里类型名称 Slice
后带了中括号,对各个部分做一个解说就是:
T
就是上面介绍过的类型形参(Type parameter) ,在定义 Slice 类型的时候 T 代表的具体类型并不确定,类似一个占位符。
int|float32|float64
这部分被称为类型约束(Type constraint) ,中间的 |
的意思是告诉编译器,类型形参 T 只可以接收 int 或 float 32 或 float 64 这三种类型的实参
中括号里的 T int|float32|float64
这一整串因为定义了所有的类型形参(在这个例子里只有一个类型形参 T),所以我们称其为 类型形参列表(type parameter list)
这里新定义的类型名称叫 Slice[T]
这种类型定义的方式中带了类型形参,很明显和普通的类型定义非常不一样,所以我们将这种类型定义中带 类型形参 的类型,称之为 泛型类型(Generic type)
泛型类型不能直接拿来使用,必须传入类型实参(Type argument) 将其确定为具体的类型之后才可使用。而传入类型实参确定具体类型的操作被称为 实例化(Instantiations) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 var a Slice[int ] = []int {1 , 2 , 3 } fmt.Printf("Type Name: %T" ,a) var b Slice[float32 ] = []float32 {1.0 , 2.0 , 3.0 } fmt.Printf("Type Name: %T" ,b) a = b var c Slice[string ] = []string {"Hello" , "World" } var x Slice[T] = []int {1 , 2 , 3 }
对于上面的例子,我们先给泛型类型 Slice[T]
传入了类型实参 int
,这样泛型类型就被实例化为了具体类型 Slice[int]
,被实例化之后的类型定义可近似视为如下:
我们用实例化后的类型 Slice[int]
定义了一个新的变量 a
,这个变量可以存储 int 类型的切片。之后我们还用同样的方法实例化出了另一个类型 Slice[float32]
,并创建了变量 b
。
因为变量 a 和 b 就是具体的不同类型了(一个 Slice[int] ,一个 Slice[float 32]),所以 a = b
这样不同类型之间的变量赋值是不允许的。
同时,因为 Slice[T] 的类型约束限定了只能使用 int 或 float 32 或 float 64 来实例化自己,所以 Slice[string]
这样使用 string 类型来实例化是错误的。
上面只是个最简单的例子,实际上类型形参的数量可以远远不止一个,如下:
1 2 3 4 5 6 7 8 9 type MyMap[KEY int | string , VALUE float32 | float64 ] map [KEY]VALUE var a MyMap[string , float64 ] = map [string ]float64 { "jack_score" : 9.6 , "bob_score" : 8.4 , }
KEY 和 VALUE 是类型形参
int|string
是 KEY 的类型约束 , float32|float64
是 VALUE 的类型约束
KEY int|string, VALUE float32|float64
整个一串文本因为定义了所有形参所以被称为类型形参列表
Map[KEY, VALUE] 是泛型类型 ,类型的名字就叫 Map[KEY, VALUE]
var a MyMap[string, float64] = xx
中的 string 和 float 64 是类型实参 ,用于分别替换 KEY 和 VALUE,实例化 出了具体的类型 MyMap[string, float64]
其他的泛型类型
所有类型定义都可使用类型形参,所以下面这种结构体以及接口的定义也可以使用类型形参:
1 2 3 4 5 6 7 8 9 10 11 12 13 type MyStruct[T int | string ] struct { Name string Data T }type IPrintData[T int | float32 | string ] interface { Print(data T) }type MyChan[T int | string ] chan T
类型形参的互相套用
1 2 3 4 5 type WowStruct[T int | float32 , S []T] struct { Data S MaxValue T MinValue T }
任何泛型类型都必须传入类型实参实例化才可以使用。所以我们这就尝试传入类型实参看看:
1 2 var ws WowStruct[int , []int ]
上面的代码中,我们为 T 传入了实参 int
,然后因为 S 的定义是 []T
,所以 S 的实参自然是 []int
。经过实例化之后 WowStruct[T, S] 的定义类似如下:
1 2 3 4 5 6 type WowStruct[int , []int ] struct { Data []int MaxValue int MinValue int }
因为 S 的定义是 []T ,所以 T 一定决定了的话 S 的实参就不能随便乱传了,下面这样的代码是错误的:
1 2 3 4 5 6 ws := WowStruct[int , []float32 ]{ Data: []float32 {1.0 , 2.0 , 3.0 }, MaxValue: 3 , MinValue: 1 , }
几种语法错误
定义泛型类型的时候,基础类型不能只有类型形参 ,如下:
1 2 type CommonType[T int |string |float32 ] T
当类型约束的一些写法会被编译器误认为是表达式时会报错。如下:
1 2 3 4 5 6 7 8 9 10 type NewType[T *int ] []Ttype NewType [T * int ][]T type NewType2[T *int |*float64 ] []T type NewType2 [T (int )] []T
为了避免这种误解,解决办法就是给类型约束包上 interface{}
或加上逗号消除歧义(关于接口具体的用法会在后半篇提及)
1 2 3 4 5 6 7 8 type NewType[T interface {*int }] []Ttype NewType2[T interface {*int |*float64 }] []T type NewType3[T *int ,] []Ttype NewType4[T *int |*float32 ,] []T
因为上面逗号的用法限制比较大,这里推荐统一用 interface{} 解决问题。
特殊的泛型类型
1 2 3 4 5 type Wow[T int | string ] int var a Wow[int ] = 123 var b Wow[string ] = 123 var c Wow[string ] = "hello"
这里虽然使用了类型形参,但因为类型定义是 type Wow[T int|string] int
,所以无论传入什么类型实参,实例化后的新类型的底层类型都是 int 。所以 int 类型的数字 123 可以赋值给变量 a 和 b,但 string 类型的字符串 “hello” 不能赋值给 c
这个例子没有什么具体意义,但是可以让我们理解泛型类型的实例化的机制。
泛型类型的嵌套
泛型和普通的类型一样,可以互相嵌套定义出更加复杂的新类型,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 type Slice[T int |string |float32 |float64 ] []Ttype UintSlice[T uint |uint8 ] Slice[T] type FloatSlice[T float32 |float64 ] Slice[T] type IntAndStringSlice[T int |string ] Slice[T] type IntSlice[T int ] IntAndStringSlice[T] type WowMap[T int |string ] map [string ]Slice[T]type WowMap 2 [T Slice[int ] | Slice[string ]] map [string ]T
类型约束的两种选择
1 2 3 4 5 6 7 8 9 type WowStruct[T int |string ] struct { Name string Data []T }type WowStruct 2 [T []int |[]string ] struct { Name string Data T }
仅限于这个例子,这两种写法和实现的功能其实是差不多的,实例化之后结构体相同。但是像下面这种情况的时候,我们使用前一种写法会更好:
1 2 3 4 5 type WowStruct 3 [T int | string ] struct { Data []T MaxValue T MinValue T }
匿名结构体不支持泛型
我们有时候会经常用到匿名的结构体,并在定义好匿名结构体之后直接初始化:
1 2 3 4 5 6 7 8 9 testCase := struct { caseName string got int want int }{ caseName: "test OK" , got: 100 , want: 100 , }
那么匿名结构体能不能使用泛型呢?答案是不能,下面的用法是错误的:
1 2 3 4 5 6 7 8 9 testCase := struct [T int |string ] { caseName string got T want T }[int ]{ caseName: "test OK" , got: 100 , want: 100 , }
所以在使用泛型的时候我们只能放弃使用匿名结构体,对于很多场景来说这会造成麻烦。
泛型 receiver
1 2 3 4 5 6 7 8 9 type MySlice[T int | float 32 ] []Tfunc (s MySlice[T]) Sum () T { var sum T for _, value := range s { sum += value } return sum }
这个例子为泛型类型 MySlice[T]
添加了一个计算成员总和的方法 Sum ()
。泛型类型无论如何都需要先用类型实参实例化,所以用法如下:
1 2 3 4 5 var s MySlice[int ] = []int {1 , 2 , 3 , 4 } fmt.Println (s.Sum ()) var s 2 MySlice[float 32 ] = []float 32 {1.0 , 2.0 , 3.0 , 4.0 } fmt.Println (s 2. Sum ())
该如何理解上面的实例化?首先我们用类型实参 int 实例化了泛型类型 MySlice[T]
,所以泛型类型定义中的所有 T 都被替换为 int,最终我们可以把代码看作下面这样:
1 2 3 4 5 6 7 8 9 10 type MySlice[int ] []int func (s MySlice[int ]) Sum () int { var sum int for _, value := range s { sum += value } return sum }
用 float 32 实例化和用 int 实例化同理。
通过泛型 receiver,泛型的实用性一下子得到了巨大的扩展。在没有泛型之前如果想实现通用的数据结构,诸如:堆、栈、队列、链表之类的话,我们的选择只有两个:
而有了泛型之后,我们就能非常简单地创建通用数据结构了。
基于泛型的队列
队列是一种先入先出的数据结构,它和现实中排队一样,数据只能从队尾放入、从队首取出,先放入的数据优先被取出来。
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 type Queue[T interface {}] struct { elements []T }func (q *Queue[T]) Put (value T) { q.elements = append (q.elements, value) }func (q *Queue[T]) Pop () (T, bool ) { var value T if len (q.elements) == 0 { return value, true } value = q.elements[0 ] q.elements = q.elements[1 :] return value, len (q.elements) == 0 }func (q Queue[T]) Size () int { return len (q.elements) }
Queue[T]
因为是泛型类型,所以要使用的话必须实例化,实例化与使用方法如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 var q 1 Queue[int ] q 1. Put (1 ) q 1. Put (2 ) q 1. Put (3 ) q 1. Pop () q 1. Pop () q 1. Pop () var q 2 Queue[string ] q 2. Put ("A" ) q 2. Put ("B" ) q 2. Put ("C" ) q 2. Pop () q 2. Pop () q 2. Pop () var q 3 Queue[struct {Name string }] var q 4 Queue[[]int ] var q 5 Queue[chan int ] var q 6 Queue[io. Reader]
动态判断变量的类型
使用接口的时候经常会用到类型断言或 type swith 来确定接口具体的类型,然后对不同类型做出不同的处理,如:
1 2 3 4 5 6 7 8 9 10 11 12 13 var i interface {} = 123 i.(int ) switch i.(type ) { case int : case string : default : } }
那么你一定会想到,对于 valut T
这样通过类型形参定义的变量,我们能不能判断具体类型然后对不同类型做出不同处理呢?答案是不允许的,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func (q *Queue[T]) Put (value T) { value. (int ) switch value. (type ) { case int : case string : default : } }
虽然 type switch 和类型断言不能用,但我们可通过反射机制达到目的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func (receiver Queue[T]) Put (value T) { fmt.Printf ("%T" , value) v := reflect.ValueOf (value) switch v.Kind () { case reflect. Int: case reflect. String: } }
这看起来达到了我们的目的,可是当你写出上面这样的代码时候就出现了一个问题:你为了避免使用反射而选择了泛型,结果到头来又为了一些功能在在泛型中使用反射。
当出现这种情况的时候你可能需要重新思考一下,自己的需求是不是真的需要用泛型(毕竟泛型机制本身就很复杂了,再加上反射的复杂度,增加的复杂度并不一定值得)
当然,这一切选择权都在你自己的手里,根据具体情况斟酌。
泛型函数
在介绍完泛型类型和泛型 receiver 之后,我们来介绍最后一个可以使用泛型的地方——泛型函数。有了上面的知识,写泛型函数也十分简单。假设我们想要写一个计算两个数之和的函数:
1 2 3 func Add (a int , b int ) int { return a + b }
这个函数理所当然只能计算 int 的和,而浮点的计算是不支持的。这时候我们可以像下面这样定义一个泛型函数:
1 2 3 func Add [T int | float 32 | float 64](a T, b T) T { return a + b }
这种带类型形参的函数被称为泛型函数 ,它和普通函数的点不同在于函数名之后带了类型形参。这里的类型形参的意义、写法和用法因为与泛型类型是一模一样的,和泛型类型一样,泛型函数也是不能直接调用的,要使用泛型函数的话必须传入类型实参之后才能调用。
1 2 3 4 Add [int ](1 ,2 ) Add[float 32 ](1.0 , 2.0 ) Add[string ]("hello" , "world" )
或许你会觉得这样每次都要手动指定类型实参太不方便了。所以 Go 还支持类型实参的自动推导:
1 2 Add (1 , 2 ) Add (1.0 , 2.0 )
自动推导的写法就好像免去了传入实参的步骤一样,但请记住这仅仅只是编译器帮我们推导出了类型实参,实际上传入实参步骤还是发生了的。
匿名函数不支持泛型
1 2 3 4 5 fn := func (a, b int ) int { return a + b } fmt.Println (fn (1 , 2 ))
1 2 3 4 5 6 fnGeneric := func [T int | float 32](a, b T) T { return a + b } fmt.Println (fnGeneric (1 , 2 ))
1 2 3 4 5 6 7 8 9 func MyFunc [T int | float 32 | float 64](a, b T) { fn 2 := func (i T, j T) T { return i*2 - j*2 } fn 2 (a, b) }
变得复杂的接口
有时候使用泛型编程时,我们会书写长长的类型约束,如下:
1 2 type Slice[T int | int 8 | int 16 | int 32 | int 64 | uint | uint 8 | uint 16 | uint 32 | uint 64 | float 32 | float 64 ] []T
理所当然,这种写法是我们无法忍受也难以维护的,而 Go 支持将类型约束单独拿出来定义到接口中,从而让代码更容易维护:
1 2 3 4 5 type IntUintFloat interface { int | int 8 | int 16 | int 32 | int 64 | uint | uint 8 | uint 16 | uint 32 | uint 64 | float 32 | float 64 }type Slice[T IntUintFloat] []T
这段代码把类型约束给单独拿出来,写入了接口类型 IntUintFloat
当中。需要指定类型约束的时候直接使用接口 IntUintFloat
即可。
不过这样的代码依旧不好维护,而接口和接口、接口和普通类型之间也是可以通过 |
进行组合:
1 2 3 4 5 6 7 8 9 10 11 12 13 type Int interface { int | int 8 | int 16 | int 32 | int 64 }type Uint interface { uint | uint 8 | uint 16 | uint 32 }type Float interface { float 32 | float 64 }type Slice[T Int | Uint | Float] []T
上面的代码中,我们分别定义了 Int, Uint, Float 三个接口类型,并最终在 Slice[T] 的类型约束中通过使用 |
将它们组合到一起。
同时,在接口里也能直接组合其他接口,所以还可以像下面这样:
1 2 3 4 5 type SliceElement interface { Int | Uint | Float | string }type Slice[T SliceElement] []T
~
:指定底层类型
上面定义的 Slie[T] 虽然可以达到目的,但是有一个缺点:
1 2 3 4 var s 1 Slice[int ] type MyInt int var s 2 Slice[MyInt]
这里发生错误的原因是,泛型类型 Slice[T] 允许的是 int 作为类型实参,而不是 MyInt (虽然 MyInt 类型底层类型是 int ,但它依旧不是 int 类型)。
为了从根本上解决这个问题,Go 新增了一个符号 ~
,在类型约束中使用类似 ~int
这种写法的话,就代表着不光是 int ,所有以 int 为底层类型的类型也都可用于实例化。
使用 ~ 对代码进行改写之后如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 type Int interface { ~int | ~int 8 | ~int 16 | ~int 32 | ~int 64 }type Uint interface { ~uint | ~uint 8 | ~uint 16 | ~uint 32 }type Float interface { ~float 32 | ~float 64 }type Slice[T Int | Uint | Float] []T var s Slice[int ] type MyInt int var s 2 Slice[MyInt] type MyMyInt MyIntvar s 3 Slice[MyMyInt] type MyFloat 32 float 32 var s 4 Slice[MyFloat 32 ]
限制 :使用 ~
时有一定的限制:
~后面的类型不能为接口。
~后面的类型必须为基本类型。
1 2 3 4 5 6 7 type MyInt int type _ interface { ~[]byte ~MyInt ~error }
从方法集(Method set)到类型集(Type set)
上面的例子中,我们学习到了一种接口的全新写法,而这种写法在 Go 1.18 之前是不存在的。如果你比较敏锐的话,一定会隐约认识到这种写法的改变这也一定意味着 Go 语言中 接口(interface)
这个概念发生了非常大的变化。
是的,在 Go 1.18 之前,Go 官方对 接口(interface)
的定义是:接口是一个方法集(method set)
就如下面这个代码一样, ReadWriter
接口定义了一个接口(方法集),这个集合中包含了 Read ()
和 Write ()
这两个方法。所有同时定义了这两种方法的类型被视为实现了这一接口。
1 2 3 4 type ReadWriter interface { Read (p []byte ) (n int , err error) Write (p []byte ) (n int , err error) }
但是,我们如果换一个角度来重新思考上面这个接口的话,会发现接口的定义实际上还能这样理解:我们可以把 ReaderWriter
接口看成代表了一个 类型的集合 ,所有实现了 Read ()
Writer ()
这两个方法的类型都在接口代表的类型集合当中。
通过换个角度看待接口,在我们眼中接口的定义就从 方法集(method set)
变为了 类型集(type set)
。而 Go 1.18 开始就是依据这一点将接口的定义正式更改为了 类型集(Type set)
你或许会觉得,这不就是改了下概念上的定义实际上没什么用吗?是的,如果接口功能没变化的话确实如此。但是还记得下面这种用接口来简化类型约束的写法吗:
1 2 3 4 5 type Float interface { ~float 32 | ~float 64 }type Slice[T Float] []T
这就体现出了为什么要更改接口的定义了。用 类型集 的概念重新理解上面的代码的话就是:接口类型 Float 代表了一个 类型集合 ,所有以 float 32 或 float 64 为底层类型的类型,都在这一类型集之中。
而 type Slice[T Float] []T
中, 类型约束 的真正意思是**:类型约束** 指定了类型形参可接受的类型集合,只有属于这个集合中的类型才能替换形参用于实例化。
1 2 var s Slice[int ] var s Slice[chan int ]
接口实现(implement)定义的变化
既然接口定义发生了变化,那么从 Go 1.18 开始 接口实现(implement)
的定义自然也发生了变化:
当满足以下条件时,我们可以说 类型 T 实现了接口 I ( type T implements interface I) :
T 不是接口时:类型 T 是接口 I 代表的类型集中的一个成员(T is an element of the type set of I)
T 是接口时: T 接口代表的类型集是 I 代表的类型集的子集(Type set of T is a subset of the type set of I)
类型的并集
并集我们已经很熟悉了,之前一直使用的 |
符号就是求类型的并集( union
)
1 2 3 type Uint interface { ~uint | ~uint 8 | ~uint 16 | ~uint 32 | ~uint 64 }
类型的交集
接口可以不止书写一行,如果一个接口有多行类型定义,那么取它们之间的 交集
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 type AllInt interface { ~int | ~int 8 | ~int 16 | ~int 32 | ~int 64 | ~uint | ~uint 8 | ~uint 16 | ~uint 32 | ~uint 32 }type Uint interface { ~uint | ~uint 8 | ~uint 16 | ~uint 32 | ~uint 64 }type A interface { AllInt Uint }type B interface { AllInt ~int }
1 2 3 4 type C interface { ~int int }
很显然,~int 和 int 的交集只有 int 一种类型,所以接口 C 代表的类型集中只有 int 一种类型。
空集
当多个类型的交集如下面 Bad
这样为空的时候, Bad
这个接口代表的类型集为一个空集 :
1 2 3 4 type Bad interface { int float 32 }
没有任何一种类型属于空集 。虽然 Bad 这样的写法是可以编译的,但实际上并没有什么意义。
空接口和 any
上面说了空集,接下来说一个特殊的类型集——空接口 interface{}
。因为,Go 1.18 开始接口的定义发生了改变,所以 interface{}
的定义也发生了一些变更:空接口代表了所有类型的集合。
所以,对于 Go 1.18 之后的空接口应该这样理解:
虽然空接口内没有写入任何的类型,但它代表的是所有类型的集合,而非一个 空集
类型约束中指定 空接口 的意思是指定了一个包含所有类型的类型集,并不是类型约束限定了只能使用 空接口 来做类型形参。
1 2 3 4 5 6 7 type Slice[T interface {}] []Tvar s 1 Slice[int ] var s 2 Slice[map [string ]string ] var s 3 Slice[chan int ] var s 4 Slice[interface {}]
因为空接口是一个包含了所有类型的类型集,所以我们经常会用到它。于是,Go 1.18 开始提供了一个和空接口 interface{}
等价的新关键词 any
,用来使代码更简单:
实际上 any
的定义就位于 Go 语言的 builtin. go
文件中(参考如下), any
实际上就是 interaface{}
的别名(alias),两者完全等价。
所以从 Go 1.18 开始,所有可以用到空接口的地方其实都可以直接替换为 any,如:
1 2 3 4 5 6 var s []any var m map [string ]any func MyPrint (value any) { fmt.Println (value) }
如果你高兴的话,项目迁移到 Go 1.18 之后可以使用下面这行命令直接把整个项目中的空接口全都替换成 any。当然因为并不强制,所以到底是用 interface{}
还是 any
全看自己喜好。
1 gofmt -w -r 'interface{} -> any' ./...
comparable (可比较)和可排序(ordered)
对于一些数据类型,我们需要在类型约束中限制只接受能 !=
和 ==
对比的类型,如 map:
1 2 type MyMap[KEY any, VALUE any] map [KEY]VALUE
所以 Go 直接内置了一个叫 comparable
的接口,它代表了所有可用 !=
以及 ==
对比的类型:
1 type MyMap[KEY comparable, VALUE any] map [KEY]VALUE
comparable
比较容易引起误解的一点是很多人容易把他与可排序搞混淆。可比较指的是可以执行 !=``==
操作的类型,并没确保这个类型可以执行大小比较( >,<,<=,>=
)。如下:
1 2 3 4 5 6 7 8 9 10 type OhMyStruct struct { a int }var a, b OhMyStruct a == b a != b a > b
而可进行大小比较的类型被称为 Orderd
。目前 Go 语言并没有像 comparable
这样直接内置对应的关键词,所以想要的话需要自己来定义相关接口,比如我们可以参考 Go 官方包golang. org/x/exp/constraints
如何定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 type Ordered interface { Integer | Float | ~string }type Integer interface { Signed | Unsigned }type Signed interface { ~int | ~int 8 | ~int 16 | ~int 32 | ~int 64 }type Unsigned interface { ~uint | ~uint 8 | ~uint 16 | ~uint 32 | ~uint 64 | ~uintptr }type Float interface { ~float 32 | ~float 64 }
接口两种类型
我们接下来再观察一个例子,这个例子是阐述接口是类型集最好的例子:
1 2 3 4 5 6 type ReadWriter interface { ~string | ~[]rune Read (p []byte ) (n int , err error) Write (p []byte ) (n int , err error) }
最开始看到这一例子你一定有点懵不太理解它代表的意思,但是没关系,我们用类型集的概念就能比较轻松理解这个接口的意思:接口类型 ReadWriter 代表了一个类型集合,所有以 string 或 []rune 为底层类型,并且实现了 Read () Write ()这两个方法的类型都在 ReadWriter 代表的类型集当中。
如下面代码中,StringReadWriter 存在于接口 ReadWriter 代表的类型集中,而 BytesReadWriter 因为底层类型是 []byte(既不是 string 也是不[]rune) ,所以它不属于 ReadWriter 代表的类型集。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 type StringReadWriter string func (s StringReadWriter) Read (p []byte ) (n int , err error) { }func (s StringReadWriter) Write (p []byte ) (n int , err error) { }type BytesReadWriter []byte func (s BytesReadWriter) Read (p []byte ) (n int , err error) { ... }func (s BytesReadWriter) Write (p []byte ) (n int , err error) { ... }
基本接口(Basic interface)
接口定义中如果只有方法的话,那么这种接口被称为基本接口(Basic interface) 。这种接口就是 Go 1.18 之前的接口,用法也基本和 Go 1.18 之前保持一致。基本接口大致可以用于如下几个地方:
最常用的,定义接口变量并赋值。
1 2 3 4 5 6 type MyError interface { Error () string }var err MyError = fmt.Errorf ("hello world" )
基本接口因为也代表了一个类型集,所以也可用在类型约束中。
1 2 type MySlice[T io. Reader | io. Writer] []Slice
一般接口(General interface)
如果接口内不光只有方法,还有类型的话,这种接口被称为 一般接口(General interface) ,如下例子都是一般接口:
1 2 3 4 5 6 7 8 9 10 type Uint interface { ~uint | ~uint 8 | ~uint 16 | ~uint 32 | ~uint 64 }type ReadWriter interface { ~string | ~[]rune Read (p []byte ) (n int , err error) Write (p []byte ) (n int , err error) }
一般接口类型不能用来定义变量,只能用于泛型的类型约束中 。所以以下的用法是错误的:
1 2 3 4 5 type Uint interface { ~uint | ~uint 8 | ~uint 16 | ~uint 32 | ~uint 64 }var uintInf Uint
这一限制保证了一般接口的使用被限定在了泛型之中,不会影响到 Go 1.18 之前的代码。
泛型接口
所有类型的定义中都可以使用类型形参,所以接口定义自然也可以使用类型形参,观察下面这两个例子:
1 2 3 4 5 6 7 8 9 10 11 type DataProcessor[T any] interface { Process (oriData T) (newData T) Save (data T) error }type DataProcessor 2 [T any] interface { int | ~struct { Data interface {} } Process (data T) (newData T) Save (data T) error }
因为引入了类型形参,所以这两个接口是泛型类型。而泛型类型要使用的话必须传入类型实参实例化才有意义 。所以我们来尝试实例化一下这两个接口。因为 T
的类型约束是 any,所以可以随便挑一个类型来当实参(比如 string):
1 2 3 4 5 6 7 DataProcessor[string ]type DataProcessor[string ] interface { Process (oriData string ) (newData string ) Save (data string ) error }
经过实例化之后就好理解了, DataProcessor[string]
因为只有方法,所以它实际上就是个 基本接口(Basic interface) ,这个接口包含两个能处理 string 类型的方法。像下面这样实现了这两个能处理 string 类型的方法就算实现了这个接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 type CSVProcessor struct { }func (c CSVProcessor) Process (oriData string ) (newData string ) { .... }func (c CSVProcessor) Save (oriData string ) error { ... }var processor DataProcessor[string ] = CSVProcessor{} processor.Process ("name, age\nbob, 12\njack, 30" ) processor.Save ("name, age\nbob, 13\njack, 31" )var processor 2 DataProcessor[int ] = CSVProcessor{}
再用同样的方法实例化 DataProcessor 2[T]
:
1 2 3 4 5 6 7 8 9 DataProcessor 2 [string ]type DataProcessor 2 [T string ] interface { int | ~struct { Data interface {} } Process (data string ) (newData string ) Save (data string ) error }
DataProcessor 2[string]
因为带有类型并集所以它是 一般接口(General interface) ,所以实例化之后的这个接口代表的意思是:
只有实现了 Process (string) string
和 Save (string) error
这两个方法,并且以 int
或 struct{ Data interface{} }
为底层类型的类型才算实现了这个接口。
一般接口(General interface) 不能用于变量定义只能用于类型约束,所以接口 DataProcessor 2[string]
只是定义了一个用于类型约束的类型集。
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 36 37 38 39 40 41 42 43 type XMLProcessor []byte func (c XMLProcessor) Process (oriData string ) (newData string ) { }func (c XMLProcessor) Save (oriData string ) error { }type JsonProcessor struct { Data interface {} }func (c JsonProcessor) Process (oriData string ) (newData string ) { }func (c JsonProcessor) Save (oriData string ) error { }var processor DataProcessor 2 [string ]type ProcessorList[T DataProcessor 2 [string ]] []Ttype StringProcessor interface { DataProcessor 2 [string ] PrintString () }type StringProcessor interface { DataProcessor 2 [string ] | DataProcessor 2 [[]byte ] PrintString () }
接口定义的限制规则
Go 1.18 从开始,在定义类型集(接口)的时候增加了非常多十分琐碎的限制规则,其中很多规则都在之前的内容中介绍过了,但剩下还有一些规则因为找不到好的地方介绍,所以在这里统一介绍下:
用 |
连接多个类型的时候,类型之间不能有相交的部分(即必须是不交集):
1 2 3 4 5 6 type MyInt int type _ interface { ~int | MyInt }
1 2 3 4 5 6 7 8 9 10 11 12 13 type MyInt int type _ interface { ~int | interface { MyInt } }type _ interface { interface { ~int } | MyInt }type _ interface { interface { ~int } | interface { MyInt } }
1 2 3 4 5 6 7 type MyInf[T ~int | ~string ] interface { ~float 32 | T }type MyInf 2 [T ~int | ~string ] interface { T }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 type Bad interface { Bad }type Bad 2 interface { Bad 1 }type Bad 1 interface { Bad 2 }type Bad 3 interface { ~int | ~string | Bad 3 }
接口的并集成员个数大于一的时候不能直接或间接并入 comparable
接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 type OK interface { comparable } type Bad 1 interface { []int | comparable } type CmpInf interface { comparable } type Bad 2 interface { chan int | CmpInf } type Bad 3 interface { chan int | interface{comparable} }
带方法的接口(无论是基本接口还是一般接口),都不能写入接口的并集中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 type _ interface { ~int | ~string | error }type DataProcessor[T any] interface { ~string | ~[]byte Process (data T) (newData T) Save (data T) error }type _ interface { ~int | ~string | DataProcessor[string ] }type Bad[T any] interface { ~int | ~string | DataProcessor[T] }