1. 概述#
泛型是很多语言的标配,合理的使用泛型便于开发,然而,Golang 一开始并不支持泛型,因为开发 Golang 的人认为泛型重要但不是必须的,所以 Golang 一开始发布的时候没有泛型。今年,在 Go 社区的呼吁下,Go1.17 发布了泛型的体验版,为 Go1.18 的正式版泛型做了铺垫。本文介绍一下 Go 中的泛型和一些使用方法。
2. 什么是泛型#
在介绍泛型之前先了解一下多态
如:
动物类
有三种对象 -> 狗,猫,鸡
有一个相同的事件 -> 叫
有三种不同的表现 -> 汪汪,喵喵,咯咯达
多态分为两类:
- 临时性多态(根据实参类型调用对应的版本,支持调用的数量十分有限,如:函数重载)
- 参数化多态(根据实参类型生成不同的版本,支持任意数量的调用,这就是泛型)
3. 泛型困境#
泛型有好处但也有坏处,怎么平衡泛型的好处和坏处就成了泛型设计者要面临的问题了。
泛型的困境:
简单来说就是:编码效率低(程序员开发慢)、编译效率低(编译器编译慢)、运行效率低(用户体验差)
4. Go1.17 中泛型的要点#
-
函数可以通过 type 关键字引入额外的类型参数 (type parameters) 列表:func F (type T)(p T) { ... } 。
-
这些类型参数可以像一般的参数一样在函数体中使用。
-
类型也可以拥有类型参数列表:type M (type T) [] T。
-
每个类型参数可以拥有一个约束:func F (type T Constraint)(p T) { ... }。
-
使用 interface 来描述类型的约束。
-
被用作类型约束的 interface 可以拥有一个预声明类型列表,限制了实现此接口的类型的基础类型。
-
使用泛型函数或类型时需要传入类型实参。
-
一般情况下,类型推断允许用户在调用泛型函数时省略类型实参。
-
如果类型参数具有类型约束,则类型实参必须实现接口。
-
泛型函数只允许进行类型约束所规定的操作。
5. 如何使用泛型#
5.1 如何输出泛型#
package main
import (
"fmt"
)
//使用[]来保存额外的类型参数列表
//[T any]是类型参数,表示该函数支持任意T类型
func printSlice[T any](s []T) {
for _, v := range s {
fmt.Printf("%v ", v)
}
fmt.Print("\n")
}
func main() {
printSlice[int]([]int{1, 2, 3, 4, 5})
printSlice[float64]([]float64{1.01, 2.02, 3.03, 4.04, 5.05})
//调用printSlice[string]([]string{“Hello”,“World”})时,会被类型推导为string类型
//如果编译器可以实现类型推导,printSlice([]string{“Hello”,“World”})
printSlice([]string{"Hello", "World"})
printSlice[int64]([]int64{5, 4, 3, 2, 1})
}
使用泛型首先要 Go1.17 版本以上
如果直接运行上面的代码可能会报错
.\main.go:9:6: missing function body
.\main.go:9:16: syntax error: unexpected [, expecting (
要想顺利运行,需要加上参数:-gcflags=-G=3
5.2 如何约束泛型的类型范围#
每个类型都有一个类型约束,就像每个普通参数都有一个类型一样。
在 Go1.17 版本中,泛型函数只能使用类型参数所能实例化出的任意类型 都能 支持的操作
(对于下代码,add 函数中的 + 应是 int, int8, int16, int32, int64,uint, uint8, uint16, uint32, uint64,uintptr,float32,float64, complex64, complex128,string
都能支持的操作)
package main
import (
"fmt"
)
//限定泛型类型范围
type Addable interface {
type int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64, uintptr,
float32, float64, complex64, complex128,
string
}
func add[T Addable] (a, b T) T {
return a + b
}
func main() {
fmt.Println(add(1,2))
fmt.Println(add("hello","world"))
}
对于没有任何约束的类型参数实例,允许对其的操作有:
- 声明这些类型的变量。
- 使用相同类型的值为这些变量赋值。
- 将这些类型的变量以实参形式传给函数或从作为函数返回值。
- 取这些变量的地址。
- 将这些类型的值转换或赋值给 interface {} 类型变量。
- 通过类型断言将一个接口值赋值给这类类型的变量。
- 在 type switch 块中作为一个 case 分支。
- 定义和使用由该类型组成的复合类型,比如:元素类型为该类型的切片。
- 将该类型传递给一些内置函数,比如 new。
5.21 comparable 约束#
不是所有的类型都可以用 ==
进行比较,Go 内置了一个 comparable 约束,表示可比较
package main
import (
"fmt"
)
func findFunc[T comparable](a []T, v T) int {
for i, e := range a {
if e == v {
return i
}
}
return -1
}
func main() {
fmt.Println(findFunc([]int{1, 2, 3, 4, 5, 6}, 5))
}
5.3 泛型中的切片#
和泛型函数一样,使用泛型类型时,首要要对其进行实例化(显示为类型参数赋值类型)如:vs:=slice{5,4,2,1}
package main
import (
"fmt"
)
type slice[T any] []T
/*
type any interface {
type int, string
}*/
func printSlice[T any](s []T) {
for _, v := range s {
fmt.Printf("%v ", v)
}
fmt.Print("\n")
}
func main() {
// note1: cannot use generic type slice[T interface{}] without instantiation
// note2: cannot use generic type slice[T any] without instantiation
vs := slice[int]{5, 4, 2, 1}
printSlice(vs)
5.4 泛型中的指针#
package main
import (
"fmt"
)
func pointerOf[T any](v T) *T {
return &v
}
func main() {
sp := pointerOf("foo")
fmt.Println(*sp)
ip := pointerOf(123)
fmt.Println(*ip)
*ip = 234
fmt.Println(*ip)
}
5.5 泛型中的 map#
package main
import (
"fmt"
)
func mapFunc[T any, M any](a []T, f func(T) M) []M {
n := make([]M, len(a), cap(a))
for i, e := range a {
n[i] = f(e)
}
return n
}
func main() {
vi := []int{1, 2, 3, 4, 5, 6}
vs := mapFunc(vi, func(v int) string {
return "<" + fmt.Sprint(v*v) + ">"
})
fmt.Println(vs)
}
5.6 泛型中的队列#
package main
import (
"fmt"
)
type queue[T any] []T
func (q *queue[T]) enqueue(v T) {
*q = append(*q, v)
}
func (q *queue[T]) dequeue() (T, bool) {
if len(*q) == 0 {
var zero T
return zero, false
}
r := (*q)[0]
*q = (*q)[1:]
return r, true
}
func main() {
q := new(queue[int])
q.enqueue(5)
q.enqueue(6)
fmt.Println(q)
fmt.Println(q.dequeue())
fmt.Println(q.dequeue())
fmt.Println(q.dequeue())
}
6. 总结#
本文简单介绍了 Go 中的泛型用法,合理地使用泛型有利于提高开发效率。