Skip to content

泛型机制

为什么需要泛型

在学习泛型之前,先看一个简单的函数示例。

go
func Sum(a, b int) int {
	return a + b
}

这个函数的功能是计算两数之和,参数是两个 int 类型相加并且返回的结果也是 int 类型。此时想要计算两个 float64 类型,那么 Sum() 函数就不能满足这个要求了。

如果要解决上述问题,通常有两种解决办法。

  1. 增加一个新的函数
go
func SumFloat64(a, b float64) float64 {
	return a + b
}
  1. 使用反射
go
func Sum(a, b interface{}) interface{} {
	switch a.(type) {
	case int:
		return a.(int) + b.(int)
	case float64:
		return a.(float64) + b.(float64)
	default:
		return nil
	}
}

上述两种解决办法的缺点是:

  • 方法1:新增函数,如果需要支持更多类型的话,需要维护更多的函数。
  • 方法2:使用了反射,性能会有所影响。

如果不想新增函数或者使用反射来实现一个功能逻辑一样的 Sum() 函数,这个时候就可以使用 Go 1.18 的新特性:泛型

go
func Sum[T int | float64](a, b T) T {
	return a + b
}

泛型函数

泛型函数语法,如下:

go
func FuncName[T Type](params) returnType {
	// Function body
}
  • T:表示泛型类型参数。
  • Type:表示具体的类型。
  • params:表示函数的参数。
  • returnType:表示函数的返回值类型。

下面是一个简单的泛型函数,它可以接受任意类型的参数,并返回一个切片。

go
func ToSlice[T any](value T) []T {
	return []T{value}
}

例子中,T 的类型是任意类型,args 表示不定参数,函数返回一个由不定参数构成的切片。在函数调用时,可以传递任何类型的参数,例如:

go
nums := ToSlice(1, 2, 3, 4, 5) // 返回:[]int{1, 2, 3, 4, 5}
strings := ToSlice("go", "java", "python") // 返回:[]string{"go", "java", "python"}

泛型类型

除了泛型函数之外,Go 1.18 版本还引入了泛型类型。泛型类型的语法如下:

go
type TypeName[T Type] struct {
	// Fields
}
  • TypeName:表示泛型类型名称。
  • T:表示泛型类型参数。
  • Type:表示具体的类型。

下面是一个泛型栈类型的定义,它可以存储任意类型的数据。

go
// Stack 定义一个泛型栈
type Stack[T any] struct {
	data []T
}

// Push 将元素推入栈
func (s *Stack[T]) Push(value T) {
	s.data = append(s.data, value)
}

// Pop 从栈中弹出元素
func (s *Stack[T]) Pop() T {
	if len(s.data) == 0 {
		var zero T // 返回类型的零值
		return zero
	}
	x := s.data[len(s.data)-1]
	s.data = s.data[:len(s.data)-1]
	return x
}

上述代码中,T 的类型是任意类型,data 是一个存储泛型类型参数 T 的切片,Push() 方法用于将元素推入栈,Pop() 方法用于从栈中弹出元素。

在函数调用时,可以传递任何类型的参数,例如:

go
// 示例1:使用整数栈
var intStack Stack[int]
intStack.Push(1)
intStack.Push(2)
intStack.Push(3)
fmt.Println("整数栈弹出:", intStack.Pop()) // 输出: 3
fmt.Println("整数栈弹出:", intStack.Pop()) // 输出: 2
fmt.Println("整数栈弹出:", intStack.Pop()) // 输出: 1
fmt.Println("整数栈弹出:", intStack.Pop()) // 输出: 0(零值)

// 示例2:使用字符串栈
var stringStack Stack[string]
stringStack.Push("hello")
stringStack.Push("world")
fmt.Println("字符串栈弹出:", stringStack.Pop()) // 输出: world
fmt.Println("字符串栈弹出:", stringStack.Pop()) // 输出: hello
fmt.Println("字符串栈弹出:", stringStack.Pop()) // 输出: ""(零值)

在示例中,创建了两个栈,一个是整数栈,一个是字符串栈,然后分别将元素推入栈中,然后依次弹出栈中的元素。

泛型约束

在使用泛型时,有时需要对泛型类型进行一定的约束。例如,我们希望某个泛型函数或类型只能接受特定类型的参数,或者特定类型的参数必须实现某个接口。在 Go 中,可以使用泛型约束来实现这些需求。

类型约束

类型约束可以让泛型函数或类型只接受特定类型的参数。在 Go 中,类型约束可以使用 interface{} 类型和类型断言来实现。例如,下面是一个泛型函数,它可以接受实现了 fmt.Stringer 接口的类型。

go
func Print[T fmt.Stringer](x T) {
	fmt.Println(x.String())
}
  • T:表示实现了 fmt.Stringer 接口的任意类型
  • x:表示函数接受一个类型为 T 的参数。

约束语法

类型约束可以使用在类型参数后加上一个约束类型来实现。例如,下面是一个泛型函数,它可以接受实现了 fmt.Stringerio.Reader 接口的类型。

go
func Print[T fmt.Stringer, U io.Reader](x T, y U) {
	fmt.Println(x.String())
	_, _ = io.Copy(os.Stdout, y)
}
  • T:表示实现了 fmt.Stringer 接口的任意类型。
  • U:表示实现了 io.Reader 接口的任意类型。
  • x:表示函数接受一个类型为 T 的参数。
  • y:表示函数接受一个类型为 U 的参数。

接口约束

除了使用 interface{} 类型进行类型约束之外,Go 还支持使用接口来约束泛型类型。例如,下面是一个泛型类型,它要求其泛型类型参数实现了 fmt.Stringer 接口。

go
type MyType[T fmt.Stringer] struct {
	data T
}

func (m *MyType[T]) String() string {
	return m.data.String()
}
  • T:表示实现了 fmt.Stringer 接口的任意类型。
  • 类型 MyType[T] 保存了一个泛型类型参数 T 的值,实现了 fmt.Stringer 接口的 String() 方法。

泛型结构

这是一个泛型切片,类型约束为 int | int32 | int64

go
type GenericSlice[T int | int32 | int64] []T

这里使用时就不能省略掉类型实参

go
GenericSlice[int]{1, 2, 3}

这是一个泛型哈希表,键的类型必须是可比较的,所以使用 comparable 接口,值的类型约束为V int | string | byte

go
type GenericMap[K comparable, V int | string | byte] map[K]V

使用 GenericMap 类型

go
gmap1 := GenericMap[int, string]{1: "hello world"}
gmap2 := make(GenericMap[string, byte], 0)

这是一个泛型结构体,类型约束为T int | string

go
type GenericStruct[T int | string] struct {
   Name string
   Id   T
}

使用 GenericStruct 结构体

go
GenericStruct[int]{
   Name: "Jacker",
   Id:   1024,
}
GenericStruct[string]{
   Name: "Martin",
   Id:   "1024",
}

这是一个泛型切片形参的例子

go
type Company[T int | string, S []T] struct {
   Name  string
   Id    T
   Stuff S
}

//也可以如下
type Company[T int | string, S []int | string] struct {
	Name  string
	Id    T
	Stuff S
}

使用 Company 结构体

go
Company[int, []int]{
   Name:  "lili",
   Id:    1,
   Stuff: []int{1},
}

提示

在泛型结构体中,更推荐这种写法

go
type Company[T int | string, S int | string] struct {
	Name  string
	Id    T
	Stuff []S
}

泛型结构注意点

泛型不能作为一个类型的基本类型

以下写法是错误的,泛型形参T是不能作为基础类型的。

go
type GenericType[T int | int32 | int64] T

虽然下列的写法是允许的,不过毫无意义而且可能会造成数值溢出的问题,虽然并不推荐。

go
type GenericType[T int | int32 | int64] int

泛型类型无法使用类型断言

对泛型类型使用类型断言将会无法通过编译,泛型要解决的问题是类型无关的,如果一个问题需要根据不同类型做出不同的逻辑,那么就根本不应该使用泛型,应该使用 interface{} 或者 any

go
func Sum[T int | float64](a, b T) T {
   ints,ok := a.(int) // 不被允许
   switch a.(type) { // 不被允许
   case int:
   case bool:
      ...
   }
   return a + b
}

匿名结构不支持泛型

匿名结构体是不支持泛型的,如下的代码将无法通过编译。

go
testStruct := struct[T int | string] {
   Name string
   Id T
}[int]{
   Name: "jack",
   Id: 1  
}

匿名函数不支持自定义泛型

以下两种写法都将无法通过编译。

go
var sum[T int | string] func (a, b T) T
sum := func[T int | string](a,b T) T{
    ...
}

但是可以使用已有的泛型类型,例如闭包中。

go
func Sum[T int | float64](a, b T) T {
	sub := func(c, d T) T {
		return c - d
	}
	return sub(a,b) + a + b
}

不支持泛型方法

方法是不能拥有泛型形参的,但是 receiver 可以拥有泛型形参。如下的代码将会无法通过编译。

go
type GenericStruct[T int | string] struct {
   Name string
   Id   T
}

func (g GenericStruct[T]) name[S int | float64](a S) S {
   return a
}

类型集

在 Go 1.18以后,接口的定义变为了类型集(Type Set),含有类型集的接口又称为General interfaces即通用接口。

类型集主要用于类型约束,不能用作类型声明,既然是集合,就会有空集,并集,交集,接下来将会讲解这三种情况。

并集

非空接口的类型集是其所有元素的类型集的交集,翻译成人话就是:如果一个接口包含多个非空类型集,那么该接口就是这些类型集的交集。

go
type SignedInt interface {
   int8 | int16 | int | int32 | int64
}

type Integer interface {
   int8 | int16 | int | int32 | int64 | uint8 | uint16 | uint | uint32 | uint64
}

type Number interface {
	SignedInt
	Int
}

例子中的交集肯定就是 SignedInt

go
func Do[T Number](n T) T {
   return n
}

Do[int](2)
DO[uint](2) //无法通过编译

空集

空集就是没有交集,例子如下,下面例子中的 Integer 就是一个类型空集。

go
type SignedInt interface {
	int8 | int16 | int | int32 | int64
}

type UnsignedInt interface {
	uint8 | uint16 | uint | uint32 | uint64
}

type Integer interface {
	SignedInt
	UnsignedInt
}

因为无符号整数和有符号整数两个肯定没有交集,所以交集就是个空集,下方例子中不管传什么类型都无法通过编译。

go
Do[Integer](1)
Do[Integer](-100)

空接口

空接口与空集并不同,空接口是所有类型集的集合,即包含所有类型。

go
func Do[T interface{}](n T) T {
   return n
}

func main() {
   Do[struct{}](struct{}{})
   Do[any]("abc")
}

底层类型

当使用 type 关键字声明了一个新的类型时,即便其底层类型包含在类型集内,当传入时也依旧会无法通过编译。

go
type Int interface {
   int8 | int16 | int | int32 | int64 | uint8 | uint16 | uint | uint32 | uint64
}

type TinyInt int8

func Do[T Int](n T) T {
   return n
}

func main() {
   Do[TinyInt](1) // 无法通过编译,即便其底层类型属于Int类型集的范围内
}

有两种解决办法,第一种是往类型集中并入该类型,但是这毫无意义,因为TinyInt与int8底层类型就是一致的,所以就有了第二种解决办法。

go
type Int interface {
   int8 | int16 | int | int32 | int64 | uint8 | uint16 | uint | uint32 | uint64 | TinyInt
}

使用 ~ 符号,来表示底层类型,如果一个类型的底层类型属于该类型集,那么该类型就属于该类型集,如下所示

go
type Int interface {
   ~int8 | ~int16 | ~int | ~int32 | ~int64 | ~uint8 | ~uint16 | ~uint | ~uint32 | ~uint64
}

修改过后就可以通过编译了。

go
func main() {
   Do[TinyInt](1) // 可以通过编译,因为TinyInt在类型集Int内
}

类型集注意点

带有方法集的接口无法并入类型集

只要是带有方法集的接口,不论是基本接口,泛型接口,又或者是通用接口,都无法并入类型集中,同样的也无法在类型约束中并入。以下两种写法都是错误的,都无法通过编译。

go
type Integer interface {
	Sum(int, int) int
	Sub(int, int) int
}

type SignedInt interface {
   int8 | int16 | int | int32 | int64 | Integer
}

func Do[T Integer | float64](n T) T {
	return n
}

类型集无法当作类型实参使用

只要是带有类型集的接口,都无法当作类型实参。

go
type SignedInt interface {
	int8 | int16 | int | int32 | int64
}

func Do[T SignedInt](n T) T {
   return n
}

func main() {
   Do[SignedInt](1) // 无法通过编译
}

类型集中的交集问题

对于非接口类型,类型并集中不能有交集,例如下例中的 TinyInt~int8 有交集。

go
type Int interface {
   ~int8 | ~int16 | ~int | ~int32 | ~int64 | ~uint8 | ~uint16 | ~uint | ~uint32 | ~uint64 | TinyInt // 无法通过编译
}

type TinyInt int8

但是对于接口类型的话,就允许有交集,如下例

go
type Int interface {
   ~int8 | ~int16 | ~int | ~int32 | ~int64 | ~uint8 | ~uint16 | ~uint | ~uint32 | ~uint64 | TinyInt // 可以通过编译
}

type TinyInt interface {
	int8
}

类型集不能直接或间接的并入自身

以下示例中,Floats 直接的并入了自身,而 Double 又并入了 Floats,所以又间接的并入了自身。

go
type Floats interface {  // 代码无法通过编译
   Floats | Double
}

type Double interface {
   Floats
}

comparable 接口无法并入类型集。

同样的,也无法并入类型约束中,所以基本上都是单独使用。

go
func Do[T comparable | Integer](n T) T { //无法通过编译
   return n
}

type Number interface { // 无法通过编译
	Integer | comparable
}

type Comparable interface { // 可以通过编译但是毫无意义
	comparable
}

参考资料

根据 MIT 许可证发布