泛型
Tip
2022年3月15日,go1.18发布,带来了泛型、模糊测试、工作区等新特性
多了几个关键字:comparable、any,一个符号 ~
泛型是啥
简单说,泛型就是某种类型,我们可以对“某种类型”加以限定。例如当一个函数接收一个参数,这个参数的类型只要支持相加即可,这在弱类型语言中很好解决,但是在强类型语言中就不太好确定,你可能需要写N个函数接收N中类型。
在以前 Go 语言的实践中,通常是用 interface{} + 在函数中进行类型断言
实现,现在有了泛型之后就简单多了。
泛型函数
非泛型的栗子
Example
package main
func SumInt ( params [] int ) int {
var sum int
for _ , v := range params {
sum += v
}
return sum
}
func SumFloat ( params [] float64 ) float64 {
var sum float64
for _ , v := range params {
sum += v
}
return sum
}
func main () {
ints := [] int { 1 , 2 , 4 }
floats := [] float64 { 1.1 , 2.2 , 4.4 }
resultInt := SumInt ( ints )
resultFloat := SumFloat ( floats )
fmt . Printf ( "Not-Generic: resultInt- %v, resultFloat- %v" , resultInt , resultFloat )
}
// Output:
Not - Generic : resultInt - 7 , resultFloat - 7.7
上面的栗子要做的事情是接收一个切片,切片类型需要是可以相加的,但是只能写成两个函数去调用。
用泛型函数可以只写一个。
泛型的栗子
Example
package main
func Sum [ T int | float64 ]( params [] T ) T {
var sum T
for _ , v := range params {
sum += v
}
return sum
}
func main () {
ints := [] int { 1 , 2 , 4 }
floats := [] float64 { 1.1 , 2.2 , 4.4 }
// 显式,指明类型的调用
resultInt := Sum [ int ]( ints )
resultFloat := Sum [ float64 ]( floats )
// 隐式, 通过编译器自动推导类型
// resultInt := Sum(ints)
// resultFloat := Sum(floats)
fmt . Printf ( "Generic: resultInt- %v, resultFloat- %v" , resultInt , resultFloat )
}
改造步骤,在原有的基础上:
在 函数名 后 参数列表 前,通过 [泛型名 类型列表]
的方式指定此函数接受哪种类型,例如上面的 [T int | float64]
表示 Sum() 函数接受 int 和 float64 两种类型,并取名为 T。
只要一个变量是 int 或 float64,那在这个函数里就是 T 类型,那么这个函数的作用域内都可以使用这个 T 类型去指代某一种类型。
然后在形参和返回值两个地方,使用 T 去替代原本的类型,表示接收 int 或 float64 类型的参数,并返回 int 或 float64 类型的返回值。
在调用的时候,放心大胆的传递符合类型定义的实参即可。
在调用处,可以 显式 的写出传入的实参是类型列表中的哪种类型,如 Sum [ int ]( ints )
中的 [int]
;
也可以不写(隐式 ),编译器会自动推导类型。例如:resultInt := Sum ( ints )
、resultFloat := Sum ( floats )
。
小结
非泛型函数:
声明:func 函数名 ( 形参列表 ) 返回值
调用:函数名 ( 实参列表 )
泛型函数:
声明:func 函数名 [ 泛型名 类型列表 ] ( 形参列表 ) 返回值
调用:函数名 [ 类型 ]( 实参列表 )
或 函数名 ( 实参列表 )
自动推导不是万能的,如果泛型函数的形参列表为空,那么在调用时需要显式写明类型。
func Foo [ T int | foat64 ]() T {
i := getNumber ()
return i
}
...
num := Foo [ int ]()
// num := Foo(), 这种不行,没有形参编译器无法推导
...
泛型数据结构
泛型切片
泛型切片是这么定义的:
type 自定义泛型切片名 [ 泛型名 类型列表 ] [] 泛型名
Example
type vector [ T any ] [] T
type vector [ T int | float64 ] [] T
那么在声明的时候是这么定义的:
Example
type vector [ T any ] [] T
func main () {
var v1 vector [ int ]
v1 = vector [ int ]{ 1 , 2 , 4 }
v2 := vector [ string ]{ "a" , "b" , "d" }
}
泛型 map
泛型 map 是这么定义的:
type 自定义泛型map名 [ 泛型名 类型列表 ] map [ 泛型名 ] 泛型名
Example
type M [ T any ] map [ string ] T
type M [ T comparable ] map [ T ] T
type M [ K comparable , V any ] map [ K ] V
要注意一点就是,map 的 key 要求是可哈希的(Hashable),或者说可比较的,所以不可能出现这种泛型 map:type M[T any] map[T]T
。这种是非法的,因为 any 表示任何类型,而 map 的 key 不是啥类型都可以。
那么在声明的时候是这样的:
var 变量名 自定义泛型map名 [ key的类型 , value的类型 ]
Example
type M [ K comparable , V any ] map [ K ] V
var m1 M [ string , float64 ]
m1 = make ( M [ string , float64 ])
m1 [ "a" ] = 1.1
m1 [ "b" ] = 2.2
泛型通道
泛型通道这么定义:
type 自定义泛型通道名 [ 泛型名 类型列表 ] chan 泛型名
Example
type C [ T any ] chan T
type C [ T int | bool ] chan T
type C [ T comparable ] chan T
在使用的时候是这样的:
Example
type C [ T any ] chan T
c1 := make ( C [ int ], 2 )
c1 <- 1
c2 <- 2
c2 := make ( C [ byte ], 2 )
c2 <- 'a'
c2 <- 'b'
泛型结构体
泛型结构体是这么定义的:
type 自定义泛型结构体名 [ 泛型名 类型列表 ] struct {
字段名 字段类型
...
}
Example
type Tree [ T any ] struct {
data T
l , r * Tree [ T ]
}
这样就定义了一个接受任意类型的二叉树的结点。
在使用的时候是这样的:
Example
type Tree [ T any ] struct {
data T
l , r * Tree [ T ]
}
var leaf1 , leaf2 Tree [ int ]
root := Tree [ int ]{
data : 1 ,
l : & leaf1 ,
r : & leaf2 ,
}
leaf1 = Tree [ int ]{
data : 2 ,
l : nil ,
r : nil ,
}
leaf2 = Tree [ int ]{
data : 3 ,
l : nil ,
r : nil ,
}
小结
可以发现,定义泛型数据结构的时候,都是先是自定义的数据结构名,然后旁边加上类型约束;在声明泛型数据结构的时候,都是自定义的数据结构名,然后旁边加上指定类型。
type 自定义泛型切片名 [ 泛型名 类型列表 ] [] 泛型名
type 自定义泛型map名 [ 泛型名 类型列表 ] map [ 泛型名 ] 泛型名
type 自定义泛型通道名 [ 泛型名 类型列表 ] chan 泛型名
type 自定义泛型结构体名 [ 泛型名 类型列表 ] struct {
...
字段名 字段类型
}
var 变量名 自定义泛型切片名 [ 类型 ]
var 变量名 自定义泛型map名 [ key的类型 , value的类型 ]
var 变量名 自定义泛型通道名 [ 类型 ]
var 变量名 自定义泛型结构体名 [ 类型 ]
泛型约束
上面举了泛型函数,还有泛型的数据结构怎么定义和声明使用,下面我们关注泛型的约束。
对比泛型函数和非泛型函数,就是差了个类型列表而已,他们的定义大概是这样的:
[ X , Y constraint1 , Z constraint2 ]
这个叫 类型参数列表(type paramter list) ,
用中括号 [ ]
包裹
X,Y,Z 都是类型参数,或者叫泛型名,建议采用大驼峰命名法,体现它是一个类型
constraint1,constraint2 都是类型约束(type constraint),或者叫类型列表、类型集
在使用的时候,除了上面那些用法,还有其他用法,下面慢慢说。
举个栗子,现在要写一个 max 函数,
非泛型写法:
func max ( a , b int ) int {
if a > b {
return a
}
return b
}
这样只接收 int 类型,如果是 int8、int64 这些也不能传进来
泛型写法:
func max [ T int | int8 | int16 | int32 | int64 ]( a , b T ) T {
if a > b {
return a
}
return b
}
这样就可以传 int 系列类型的参数进去。
但是有个问题,如果还要加上 uint 系列呢,接着列出来?很丑!!!
那么可以用 interface 的方式去定义。
interface 的方式
type Num interface {
int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64
}
func max [ T Num ]( a , b T ) T {
if a > b {
return a
}
return b
}
func min [ T Num ]( a , b T ) T {
if a < b {
return a
}
return b
}
// 调用
func main () {
max ( 1 , 2 )
max ( 1.0 , 2.2 )
}
可以看到泛型函数的定义一下子变得简洁,而且 Num 这个 interface 还能复用给 min 函数。
但是有个问题,如果我基于 int 类型自定义了一个类型,底层类型也是 int,但是编译不通过呀。
没事,用 ~
符号,这个符号跟类型一起,表示底层类型是该类型的都能接受。例如 ~int
表示 int 和底层类型是 int 的所有类型。
~
符号
type Num interface {
~ int | ~ int8 | ~ int16 | ~ int32 | ~ int64 |
~ uint | ~ uint8 | ~ uint16 | ~ uint32 | ~ uint64 |
~ float32 | ~ float64
}
func max [ T Num ]( a , b T ) T {
if a > b {
return a
}
return b
}
func min [ T Num ]( a , b T ) T {
if a < b {
return a
}
return b
}
// 调用
type MyInt int
func main () {
var a , b MyInt = 1 , 2
max ( a , b )
max ( 1 , 2 )
}
在调用处,先是定义了一个基于 int 的自定义类型 MyInt,然后声明了两个自定义类型的参数,一样可以传进去。
但是我懒,我不想手动写,那就用 constraints 包。
constraints 包
import (
"golang.org/x/exp/constraints"
...
)
func max [ T constraints . Order ]( a , b T ) T {
if a > b {
return a
}
return b
}
func min [ T constraints . Order ]( a , b T ) T {
if a < b {
return a
}
return b
}
constraints 是官方给出的一个很简单的包,主要就是定义了几个泛型,他们的关系如下图。不过这个包在 Go 1.18 正式发布时并没有带上。使用命令 go get golang.org/x/exp/constraints
可以下载到。
上面的栗子引出许多个跟泛型相关的点:
使用 |
定义一个类型集
使用 ~T
接受所有底层类型为 T 的类型
使用 interface 方式使得泛型约束可以复用
使用 constraints 包可以省去定义一些基础类型的泛型的工作
下面继续补充一下 interface 方式、关键字 comparable 和 any、类型约束字面值。
interface 方式
简单的 interface 约束
type Signed interface {
~ int | ~ int8 | ~ int16 | ~ int32 | ~ int64
}
Go 1.18以前定义接口
type Stringer interface {
String ()
}
在定义 interface 约束的时候是可以加上方法集的,因为 Go 中 interface 定义的是一种类型,那么上栗子中,
Signed 是基础类型为 int 系列类型的一种类型;
Writer 是实现了 Write 方法的一种类型;
两个都是类型,它们可以同时用:
type Foo interface {
~ int | ~ int8 | ~ int16 | ~ int32 | ~ int64
String ()
}
func AddAndPrint [ T Foo ]( param T ) {
param ++
param . String ()
}
关键字 comparable 和 any
Go 1.18 新增了两个关键字 comparable 和 any,他们定义如下:
// any is an alias for interface{} and is equivalent to interface{} in all ways.
type any = interface {}
// 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 }
any 只是 interface{} 的别名,所以以后写代码时如果需要用到 interface{} 这种类型时,可以直接用 any 替代。
comparable 是一些可以用 ==
和 !=
比较的类型(不包括 >
、>=
、<
、<=
这些哦),可以是布尔类型、数值类型、字符串类型、指针类型、通道类型、comparable类型的数组、可比较的结构体(字段中没有slice、map类型)。
comparable 只能用在泛型约束中,不能用作声明一个变量的类型。
类型约束字面值
[ S interface { ~ [] E }, E interface {}]
[ S ~ [] E , E interface {}]
[ S ~ [] E , E any ]
类型约束可以提前定义好,也可以在约束列表中定义,例如上面的三个类型约束,虽然读取到 S 的时候不知道 E 是什么类型,在类型约束结束前就读取到 E 是 interface{},所以这样的类型约束是可以的,而 E 就是**类型约束字面值**。
在类型限制的位置,interface{E}
也可以直接写为E
,因此就可以理解interface{~[]E}
可以写为~[]E
。
使用场景
需要使用 slice、map、channel 类型时,但是 slice、map、channel 中的元素可能有多种;
定义一些比较通用的数据结构时,比如通用的链表、树等等;
当一个方法的实现对所有某些类型都一样时。
不要为了泛型而使用泛型
// good
func foo ( w io . Writer ) {
b := getBytes ()
_ , _ = w . Write ( b )
}
// bad
func foo [ T io . Writer ]( w T ) {
b := getBytes ()
_ , _ = w . Write ( b )
}
单纯是调用io.Writer
的Write
方法,把内容写到指定地方。使用interface
作为参数更合适,可读性更强。
Avoid boilerplate.
Corollary: Don't use type parameters prematurely; wait until you are about to write boilerplate code.
@Ian 给的建议是:当你发现针对不同类型,会写出同样的代码逻辑时,才去使用泛型。也就是 Avoid boilerplate code
。