前言
在编程的广袤世界中,技术的演进如同璀璨星辰不断闪烁。Go 语言,以其简洁高效而备受开发者青睐。然而,令人惊讶的是,Go 泛型自推出以来已有一段时间,可当我们环顾四周,却发现许多同事竟然还未尝试过这一强大的特性。这究竟是为何呢?是忙碌的工作让大家无暇探索新领域,还是对新特性的陌生让大家望而却步?今天,就让我们一同深入了解 Go 泛型,看看它能为我们的编程之旅带来怎样的惊喜与变革。
泛型
泛型程序设计(generic programming)是程序设计语言的一种风格或范式。泛型允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型。
泛型允许你编写可应用于多种类型的代码,而无需为每种类型重复编写相同的逻辑。这提高了代码的重用性、灵活性和类型安全性。
在Go中,泛型是通过类型参数来实现的。类型参数是一种特殊的参数,用于表示可以是任何类型的一个占位符。它们在函数、方法和类型的定义中使用,并在具体调用时被具体类型替换。
没有泛型前
考虑这么一个需求,实现一个函数,这个函数接受2个int的入参,返回两者中数值较小的。需求是非常简单的,我们可以不假思索的写下如下的代码:
func Min(a,b int) int {
if a < b {
return a
}
return b
}
看起来很美好,但是这个函数有局限性,入参只能用int类型,如果需求做了拓展,需要支持对两个float64的入参做判断,返回两者中较小的。
func Min(a,b int) int {
if a < b {
return a
}
return b
}
func MinFloat64(a,b float64) float64 {
if a < b {
return a
}
return b
}
不知道大家有没有发现,一旦需求做了拓展,我们都需要也跟着做一些变更,一直做着重复事情,泛型刚好解决这种问题。
import "golang.org/x/exp/constraints"
func Min[T constraints.Ordered](x, y T) T {
if x < y {
return x
}
return y
}
泛型基本语法
// 函数定义
func F[T any](p T){...}
// 类型定义
type M[T any] []T
// Constraint代表具体的类型约束,如any,comparable
func F[T Constraint](p T){..}
// “~” 用于表示底层类型(underlying type)约束
type E interface {
~string
}
// 指定某几种类型
type UnionElem interface {
int | int8 | int32 | int64
}
~符号
在 Go 语言的泛型中,“~” 用于表示底层类型(underlying type)约束。
例如,~int表示接受任何底层类型为int的类型,包括自定义类型,如果有一个自定义类型MyInt底层类型是int,那么这个约束就可以接受MyInt类型。
type MyInt int
type Ints[T int | int32] []T
func main() {
a := Ints[int]{1, 2} //正确
b := Ints[MyInt]{1, 2} //编译出错
println(a)
println(b)
}
MyInt does not satisfy int | int32 (possibly missing ~ for int in int | int32)compilerInvalidTypeArg
修改如下即可
type Ints[T ~int | ~int32] []T
类型约束
- any:接受任何类型
- comparable:支持==和!=操作
- ordered:支持比较大小
>
<
其他类型参考 https://pkg.go.dev/golang.org/x/exp/constraints
何时使用泛型
何时使用泛型
- 对语言定义的容器类型操作时:当编写对切片、映射和通道等语言定义的特殊容器类型进行操作的函数,且函数代码对元素类型没有特定假设时,使用类型参数可能有用。例如,一个返回任何类型映射中所有键的切片的函数。
- 通用数据结构:对于通用数据结构,如链表或二叉树,使用类型参数可以产生更通用的数据结构,或更有效地存储数据、避免类型断言并在构建时进行完全类型检查。
- 实现通用方法:当不同类型需要实现一些通用方法,且这些实现看起来完全相同时,使用类型参数可能有用。例如,为任何切片类型实现
sort.Interface
的通用类型。 - 优先使用函数而非方法:当需要类似比较函数的东西时,优先使用函数而不是方法。对于通用数据类型,更倾向于使用函数而不是编写需要方法的约束。
// SliceFn implements sort.Interface for a slice of T.
type SliceFn[T any] struct {
s []T
less func(T, T) bool
}
func (s SliceFn[T]) Len() int {
return len(s.s)
}
func (s SliceFn[T]) Swap(i, j int) {
s.s[i], s.s[j] = s.s[j], s.s[i]
}
func (s SliceFn[T]) Less(i, j int) bool {
return s.less(s.s[i], s.s[j])
}
// SortFn sorts s in place using a comparison function.
func SortFn[T any](s []T, less func(T, T) bool) {
sort.Sort(SliceFn[T]{s, less})
}
何时不使用泛型
- 不替换接口类型:如果只需要调用某个类型的值的方法,应使用接口类型而不是类型参数。例如,不应将使用接口类型的函数改为使用类型参数的函数。
- 方法实现不同时不使用类型参数:如果方法的实现对于每个类型都不同,则使用接口类型并编写不同的方法实现,而不是使用类型参数。
- 适当使用反射:如果某些操作必须支持甚至没有方法的类型,并且操作对于每个类型都不同,则使用反射。例如,
encoding/json
包使用反射。
简单准则
如果发现自己多次编写完全相同的代码,唯一的区别是代码使用不同的类型,可以考虑使用类型参数。换句话说,应该避免使用类型参数,直到注意到即将多次编写完全相同的代码。
八卦
为什么选用了方括号[]
而不是其他语言里常见的尖括号<>
。
https://github.com/golang/proposal/blob/master/design/15292/2013-12-type-params.md
Use angle brackets, as in Vector. This has the advantage of being familiar to C++ and Java programmers. Unfortunately, it means that f(true) can be parsed as either a call to function f or a comparison of f<T (an expression that tests whether f is less than T) with (true). While it may be possible to construct complex resolution rules, the Go syntax avoids that sort of ambiguity for good reason.
使用尖括号,如Vector。这对于 C++ 和 Java 程序员来说具有熟悉的优势。不幸的是,这意味着f(true)可以被解析为对函数f的调用,或者是将f(一个测试f是否小于T的表达式)与(true)进行比较。虽然可能构建复杂的解析规则,但 Go 语法出于充分的理由避免了这种歧义。
参考资料
- https://go.dev/blog/intro-generics
- https://go.dev/blog/when-generics
- https://go.dev/doc/tutorial/generics
- https://github.com/golang/proposal/blob/master/design/15292/2013-12-type-params.md