go 语言中的泛型(一)https://developer.aliyun.com/article/1391732
基于泛型的队列
队列是一种先入先出的数据结构,它和现实中排队一样,数据只能从队尾放入、从队首取出,先放入的数据优先被取出来
// 这里类型约束使用了空接口,代表的意思是所有类型都可以用来实例化泛型类型 Queue[T] (关于接口在后半部分会详细介绍) type Queue[T interface{}] struct { elements []T } // 将数据放入队列尾部 func (q *Queue[T]) Put(value T) { q.elements = append(q.elements, value) } // 从队列头部取出并从头部删除对应数据 func (q *Queue[T]) Pop() (T, bool) { var value T if len(q.elements) == 0 { return value, true } value = q.elements[0] q.elements = q.elements[1:] return value, len(q.elements) == 0 } // 队列大小 func (q Queue[T]) Size() int { return len(q.elements) }
💡 为了方便说明,上面是队列非常简单的一种实现方法,没有考虑线程安全等很多问题
Queue[T] 因为是泛型类型,所以要使用的话必须实例化,实例化与使用方法如下所示:
var q1 Queue[int] // 可存放int类型数据的队列 q1.Put(1) q1.Put(2) q1.Put(3) q1.Pop() // 1 q1.Pop() // 2 q1.Pop() // 3 var q2 Queue[string] // 可存放string类型数据的队列 q2.Put("A") q2.Put("B") q2.Put("C") q2.Pop() // "A" q2.Pop() // "B" q2.Pop() // "C" var q3 Queue[struct{Name string}] var q4 Queue[[]int] // 可存放[]int切片的队列 var q5 Queue[chan int] // 可存放int通道的队列 var q6 Queue[io.Reader] // 可存放接口的队列 // ......
动态判断变量的类型
使用接口的时候经常会用到类型断言或 type swith 来确定接口具体的类型,然后对不同类型做出不同的处理,如:
var i interface{} = 123 i.(int) // 类型断言 // type switch switch i.(type) { case int: // do something case string: // do something default: // do something } }
泛型函数
在介绍完泛型类型和泛型receiver之后,我们来介绍最后一个可以使用泛型的地方——泛型函数。有了上面的知识,写泛型函数也十分简单。假设我们想要写一个计算两个数之和的函数:
func Add(a int, b int) int { return a + b }
这个函数理所当然只能计算int的和,而浮点的计算是不支持的。这时候我们可以像下面这样定义一个泛型函数:
func Add[T int | float32 | float64](a T, b T) T { return a + b }
上面就是泛型函数的定义。
这种带类型形参的函数被称为泛型函数它和普通函数的点不同在于函数名之后带了类型形参。这里的类型形参的意义、写法和用法因为与泛型类型是一模一样的,就不再赘述了。
和泛型类型一样,泛型函数也是不能直接调用的,要使用泛型函数的话必须传入类型实参之后才能调用。
Add[int](1,2) // 传入类型实参int,计算结果为 3 Add[float32](1.0, 2.0) // 传入类型实参float32, 计算结果为 3.0 Add[string]("hello", "world") // 错误。因为泛型函数Add的类型约束中并不包含string
或许你会觉得这样每次都要手动指定类型实参太不方便了。所以Go还支持类型实参的自动推导:
Add(1, 2) // 1,2是int类型,编译请自动推导出类型实参T是int Add(1.0, 2.0) // 1.0, 2.0 是浮点,编译请自动推导出类型实参T是float32
自动推导的写法就好像免去了传入实参的步骤一样,但请记住这仅仅只是编译器帮我们推导出了类型实参,实际上传入实参步骤还是发生了的。
既然支持泛型函数,那么泛型方法呢?
既然函数都支持泛型了,那你应该自然会想到,方法支不支持泛型?很不幸,目前Go的方法并不支持泛型,如下:
type A struct { } // 不支持泛型方法 func (receiver A) Add[T int | float32 | float64](a T, b T) T { return a + b }
但是因为receiver支持泛型, 所以如果想在方法中使用泛型的话,目前唯一的办法就是曲线救国,迂回地通过receiver使用类型形参:
type A[T int | float32 | float64] struct {
}
// 方法可以使用类型定义中的形参 T func (receiver A[T]) Add(a T, b T) T { return a + b } // 用法: var a A[int] a.Add(1, 2) var aa A[float32] aa.Add(1.0, 2.0)
变得复杂的接口
有时候使用泛型编程时,我们会书写长长的类型约束,如下:
// 一个可以容纳所有int,uint以及浮点类型的泛型切片
type Slice[T int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64] []T理所当然,这种写法是我们无法忍受也难以维护的,而Go支持将类型约束单独拿出来定义到接口中,从而让代码更容易维护:
type IntUintFloat interface { int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64 } type Slice[T IntUintFloat] []T
这段代码把类型约束给单独拿出来,写入了接口类型 IntUintFloat 当中。需要指定类型约束的时候直接使用接口 IntUintFloat 即可。
不过这样的代码依旧不好维护,而接口和接口、接口和普通类型之间也是可以通过 | 进行组合:
type Int interface { int | int8 | int16 | int32 | int64 } type Uint interface { uint | uint8 | uint16 | uint32 } type Float interface { float32 | float64 }
type Slice[T Int | Uint | Float] []T // 使用 ‘|’ 将多个接口类型组合上面的代码中,我们分别定义了 Int, Uint, Float 三个接口类型,并最终在 Slice[T] 的类型约束中通过使用 | 将它们组合到一起。
同时,在接口里也能直接组合其他接口,所以还可以像下面这样:
type SliceElement interface { Int | Uint | Float | string // 组合了三个接口类型并额外增加了一个 string 类型 } type Slice[T SliceElement] []T