Go 1.18 版本之后正式引入泛型,它被称作类型参数(type parameters),本文初步介绍 Go 中泛型的使用。长期以来 go 都没有泛型的概念,只有接口 interface 偶尔类似的充当泛型的作用,然而接口终究无法满足一些基本的泛型需求,比如这篇文章里,我们会尝试用 Go 的泛型循序渐进地实现一些常见的函数式特性,从而探索 Go 泛型的优势和不足。
在 Go1.18 可以通过如下命令安装体验:
go install golang.org/dl/go1.18@latest
go1.18 download
import (
"golang.org/x/exp/constraints"
)
func Sum[T constraints.Integer](values ...T) T {
var sum T
for _, v := range values {
sum += v
}
return sum
}
constraints 原本是放在标准库的包,但是近期被移除了,改到了 x/exp 中,参见 #50792
这个版本实现了对任意多个同类型的整数求和。Sum 后面的中括号 [] 内就是定义类型参数的地方,其中 T 为类型参数名,constraints.Integer 是对该类型参数的约束,即 T 应该满足的条件,在这里我们要求 T 是一个整数。剩下的代码就和普通没有泛型的代码一致了,只不过后面 T 可以当作一个类型来使用。
func F[T any](p T) { ... }
type M[T any] []T
type Integer1 interface {
int
}
type Integer2 interface {
int | int8 | int16 | int32 | int64
}
type Integer3 interface {
~int
}
filter 操作是高阶函数的经典应用,它接受一个函数 f(func (T) bool)
和一个线性表 l([] T)
,对 l 中的每个元素应用函数 f
,如结果为 true
,则将该元素加入新的线性表里,否则丢弃该元素,最后返回新的线性表。
func Filter[T any](f func(T) bool, src []T) []T {
var dst []T
for _, v := range src {
if f(v) {
dst = append(dst, v)
}
}
return dst
}
func main() {
src := []int{-2, -1, -0, 1, 2}
dst := Filter(func(v int) bool { return v >= 0 }, src)
fmt.Println(dst)
}
// Output:
// [0 1 2]
众所周知Go语言不支持三元运算符操作,现在有了泛型,让我们来模拟一个:
// IFF if yes return a else b
func IFF[T any](yes bool, a, b T) T {
if yes {
return a
}
return b
}
// IFN if yes return func, a() else b().
func IFN[T any](yes bool, a, b func() T) T {
if yes {
return a()
}
return b()
}
func main() {
a := -1
assert.Equal(t, utils.IFF(a > 0, a, 0), 0)
assert.Equal(t, utils.IFN(a > 0, func() int { return a }, func() int { return 0 }), 0)
}
众多函数式特性的实现依赖于一个强大类型系统,Go 的类型系统显然不足以胜任, 在 Go 语言中引入泛型之后,类型系统有哪些水土不服的地方。
当我们在写一段泛型代码里的时候,有时候会需要根据 T 实际上的类型决定接下来的流程,可 Go 的完全没有提供在编译期操作类型的能力。运行期的 workaround 当然有,怎么做呢:将 T 转化为 interface{}
,然后做一次 type assertion, 比如我想实现一个通用的字符串类型到数字类型的转换函数:
import "strconv"
type Number interface {
int | int32 | int64 | uint32 | uint64 | float64
}
func Str2Number[N Number](strNumber string) (N, error) {
var num N
switch (interface{})(num).(type) {
case int:
cn, err := strconv.Atoi(strNumber)
return N(cn), err
case int32:
cn, err := strconv.ParseInt(strNumber, 10, 32)
return N(cn), err
case int64:
cn, err := strconv.ParseInt(strNumber, 10, 64)
return N(cn), err
case uint32:
cn, err := strconv.ParseUint(strNumber, 10, 32)
return N(cn), err
case uint64:
cn, err := strconv.ParseUint(strNumber, 10, 64)
return N(cn), err
case float64:
cn, err := strconv.ParseFloat(strNumber, 64)
return N(cn), err
}
return 0, nil
}
在类型约束中可以用 ~T 的语法约束所有 基础类型为 T 的类型,这是 Go 在语法层面上首次暴露出「基础类型」的概念,在之前我们只能通过 reflect.(Value).Kind 获取。而在 type assertion 和 type switch 里并没有对应的语法处理「基础类型」:
type Int interface {
~int | ~uint
}
func IsSigned[T Int](n T) {
switch (interface{})(n).(type) {
case int:
fmt.Println("signed")
default:
fmt.Println("unsigned")
}
}
func main() {
type MyInt int
IsSigned(1)
IsSigned(MyInt(1))
}
// Output:
// signed
// unsigned
乍一看很合理,MyInt 确实不是 int。那我们要如何在函数不了解 MyInt 的情况下把它当 int 处理呢, 比较抱歉的是目前在1.18中没办法对这个进行处理。
一个直观的想法是单独定义一个 Signed 约束,然后判断 T 是否满足 Signed:
type Signed interface {
~int
}
func IsSigned[T Int](n T) {
if _, ok := (interface{})(n).(Signed); ok {
fmt.Println("signed")
} else {
fmt.Println("unsigned")
}
}
但很可惜,类型约束不能用于 type assertion/switch,编译器报错如下:
interface contains type constraints
尽管让类型约束用于 type assertion 可能会引入额外的问题,但牺牲这个支持让 Go 的类型表达能力大大地打了折扣。