初识自定义泛型
- 在自定义泛型的世界中。可能会与被定义成泛型类型的类型,泛型函数的函数。此外泛型类型也是会被定义成类型的,所有它们也可能会有相应的方法。
- 关于泛型类型,泛型函数以及相应的方法的定义都会包含一种称为类型参数列表的部分,这是与普通方法函数以及方法最大的不同之处。
一个泛型类型的例子
-
首先,让我们先看一个展示泛型类型长什么样的例子,这个例子可能并不完美,但是它确实展示了自定义泛型是多么的有用。
package main import "sync" type Lockable[T any] struct { sync.Mutex Data T } func main() { var n Lockable[uint32] n.Lock() n.Data++ n.Unlock() var f Lockable[float64] f.Lock() f.Data += 1.23 f.Unlock() var b Lockable[bool] b.Lock() b.Data = !b.Data b.Unlock() var bs Lockable[[]byte] bs.Lock() bs.Data = append(bs.Data, "Go"...) bs.Unlock() }
-
在上述例子中,Lockable 是一种泛型类型。对比非泛型类型,它有个与众不同的地方,一个类型参数列表,在这个泛型声明中。它的类型参数列表的是[T any]。
一个类型参数列表可能会包含一个或多个类型参数声明,并且这些类型的声明会用用方括号包含,且用竖线分隔。每个参数声明包含一个类型参数名称和类型约束。例如,在上述例子中,T 是类型参数的名称,any[1]是 T 的类型约束。[2]
我们可以将约束视为类型的类型(类型参数)。所有的约束都是一种接口类型。约束是泛型的核心,并会在下一章详细阐明。
T 起到了形参类型的作用,它的作用域开始于声明好的泛型名称,结束于泛型的说明[3]。在这个例子中,T 被用来表示 Data 的类型。
-
自从 Go1.18 版本以来,变量的类型就会分成以下两个大类:
- 类型参数类型
- 普通类型
而在 1.18 版本之前,所有变量的类型都是普通类型
-
泛型是一种被定义的类型,它必须被实例化从而可以便用来作为一种变量类型。符号串
Lockable[uint32]
被称为一个实例化的类型(从Lockable泛型类型中实例而来)。在这个表达式中,[uint32]
被称为类型实参列表,同时其中的uint32
是一个会被传递给相应T类型的类型参数。这意味着,通过Lockable[uint32]
实例化后的Data字段类型是uint32。类型实参必须满足这个与之对应的类型形参的约束。any类型约束是最宽松的约束,任何变量类型都可以被传递给T的类型形参。在上述例子中使用的类型实参还有:
float64, bool以及[]byte
。每个被实例化的类型都是一种命名类型[4]和一种普通类型。例如:
Lockable[uint32]
和Lockable[[]byte]
都是命令类型。
- 上述例子展现了如何通过自定义泛型类型从而避免为了定义类型导致代码大量重复。如果没有自定义泛型,就会有许多结构类型需要声明,在下述代码中就得到了充分的体现。
这份没有泛型的代码包含了大量代码重复,我们可以通过使用泛型从而避免上面展示的问题。package main import "sync" type LockableUint32 struct { sync.Mutex Data uint32 } type LockableFloat64 struct { sync.Mutex Data float64 } type LockableBool struct { sync.Mutex Data bool } type LockableBytes struct { sync.Mutex Data []byte } func main() { var n LockableUint32 n.Lock() n.Data++ n.Unlock() var f LockableFloat64 f.Lock() f.Data += 1.23 f.Unlock() var b LockableBool b.Lock() b.Data = !b.Data b.Unlock() var bs LockableBytes bs.Lock() bs.Data = append(bs.Data, "Go"...) bs.Unlock() }
一个泛型方法的例子
-
许多人可能并不赞成上述泛型的实现。相反,他们更愿意去使用下面代码展示的实现方式。对比在上一部分中的Lockable实现方式,新的泛型实现对外部软件包用户隐藏了结构体字段。
package main import "sync" type Lockable[T any] struct { mu sync.Mutex data T } func (l *Lockable[T]) Do(f func(*T)) { l.mu.Lock() defer l.mu.Unlock() f(&l.data) } func main() { var n Lockable[uint32] n.Do(func(v *uint32) { *v++ }) var f Lockable[float64] f.Do(func(v *float64) { *v += 1.23 }) var b Lockable[bool] b.Do(func(v *bool) { *v = !*v }) var bs Lockable[[]byte] bs.Do(func(v *[]byte) { *v = append(*v, "Go"...) }) }
-
在上述代码中,
Do
方法用来服务Lockable
泛型类型。其中接收器是一个指针类型并且它的类型是Lockable
泛型类型。与普通基础类型方法的申明不同的是,跟在接收器泛型类型后面多了个类型形参列表。在这个例子中类型形参列表是[T]
。泛型方法声明中的类型形参列表实际上是,泛型接收器基本类型规范中指定的类型形参列表的复制。为了使得代码整洁,类型形参列表中的约束会被(且必须被)忽略。这就是为什么这里的类型形参列表是
[T]
,而不是[T any]
。 -
其中,T也被用来表示形参变量的类型,
func(*T)
。- 实例为
Lockable[uint32]变量
,它的Do
方法是func(f func(*uint32))
. - 实例为
Lockable[float64]变量
,它的Do
方法是func(f func(*float64))
. - 实例为
Lockable[bool]
变量,它的Do
方法是func(f func(*bool))
. - 实例为
Lockable[[]byte]
变量,它的Do
方法是func(f func(*[]byte]))
.
请注意到,类型形参的名字并不需要和与之相关的泛型定义中的名字相同。例如,在上述方法的声明等同于下述重写的方法:
func (l *Lockable[Foo]) Do(f func(*Foo)) { ... }
尽管如此,不保持名称一致的行为十分糟糕。
顺便说一下,如果没有使用泛型,类型参数的名称甚至可以是空白标识符 _(泛型类型和函数声明中的类型参数也是如此)。例如:
func (l *Lockable[_]) DoNothing() { }
- 实例为