泛型是go在1.18版本引入的新特性,泛型的引入使得在某些场景下,可以极大的简化代码的编写,提高了代码的复用性。有必要掌握泛型,可以减少很多重复的代码。
一、为什么需要泛型?
为什么我们需要泛型?在前面我们已经提到了简化代码的编写,提高代码的复用,这里我们举例详细说明? 假设我们需要实现一个函数,它的主要功能是做加法计算,比如计算a + b
的值。
对于整数类型,我们可以使用如下的代码:
go
复制代码
func Add(a, b int) int {
return a + b
}
对于浮点数类型,我们可以使用如下的代码:
go
复制代码
func Add(a, b float64) float64 {
return a + b
}
看到了吧,我们发现,对于整数类型和浮点数类型,我们实现的函数是相同的,只是参数类型不同而已。究其原因在于,go作为静态类型语言,为了应对不同类型的变量,需要编写不同的函数做相应的计算。这正是泛型所要解决的问题。
下面我们看看,范型是如何解决这个问题的呢?
二、怎么用?
1. 用法
直接看代码
go
复制代码
package main
import "fmt"
func main() {
a := Add(1, 2)
b := Add(1.2, 2.3)
fmt.Printf("int add sum: %d\n", a)
// int add sum: 3
fmt.Printf("float64 add sum: %f\n", b)
// float64 add sum: 3.500000
}
// 泛型函数
// [] 中放的是类型参数
// T int | float64 类型约束为 int/float64
func Add[T int | float64](a, b T) T {
return a + b
}
我们通过泛型的使用,将原来的int
和float64
Add函数,合并成一个函数了。 在使用时,本质是我们将类型提取成参数,类型也是一种参数(类型参数),这样就可以做到忽略某个具体类型,而编写通用的代码逻辑。
我们在使用时,无需显示传递类型参数,这是由于编译器会偷偷的在背后帮我们做类型推导的原因,实际上你显示传递Add[float64](1.2, 2.3)
也是ok的。
2. 类型约束
前面我们提到类型参数,类型约束就是用于约束支持哪些类型的。
那么类型约束有哪些写法呢?
- 直接在使用位置写明
go
复制代码
// 直接在使用位置写明
func Add[T int | float64](a, b T) T {
return a + b
}
- 定义接口
go
复制代码
type addable interface {
// 前面带~ 表示只要底层类型为int或float64即可 场景是自定义类型
~int | ~float64
}
// 这里引入addable约束
func Add[T addable](a, b T) T {
return a + b
}
- 内置类型
名称 | 使用场景 | 示例 |
any | 任意类型 | any |
comparable | 包括基本类型(如整数、浮点数、字符串)和一些复合类型(如数组),但不包括切片、映射、函数、通道等 | a == b 或 a != b |
constraints.Ordered |
做顺序比较,所有整数类型、浮点数类型和字符串类型 | <, >, <=, >= 比较操作 |
3. 使用举例
除了我们前面示例函数中使用泛型外,在其它地方也能使用比如结构体,在结构体使用举例。
go
复制代码
package main
import "fmt"
type Cache[T any] struct {
cache map[string]T
}
func (c *Cache[T]) Set(key string, value T) {
c.cache[key] = value
}
func (c *Cache[T]) Get(key string) (T, bool) {
value, exists := c.cache[key]
return value, exists
}
func main() {
cache := &Cache[string]{
cache: make(map[string]string),
}
cache.Set("hello", "world")
value, _ := cache.Get("hello")
fmt.Println("缓存中hello值为:", value)
// 缓存中hello值为: world
}
4. 什么时候考虑使用范型?
当我们发现代码逻辑都一致,唯一不同的地方是类型不同时,考虑使用泛型。
三、注意的坑?
在使用泛型操作自定义类型时,需要注意它的返回值是底层类型还是自定义类型,下面我们看一个例子。
go
复制代码
package main
import "fmt"
// 自定义int 切片类型
type Point []int
// point 打印方法
func (p Point) print() {
fmt.Printf("Point(%d, %d)", p[0], p[1])
}
// 用泛型的函数 缩放切片
func ScaleSlice[T int | float64](slice []T, scale T) []T {
for i, val := range slice {
slice[i] = val * scale
}
return slice
}
func main() {
// 创建一个切片
slice := Point{1, 2, 3, 4, 5}
// 调用泛型函数
scaledSlice := ScaleSlice(slice, 2)
// 打印结果
fmt.Println(scaledSlice)
// [2 4 6 8 10]
fmt.Printf("slice is %T\n", scaledSlice)
// slice is []int
// 这里会提示没有找到print方法 因为当前返回的scaledSlice是[]int类型 而非point
scaledSlice.print()
}
我们发现,泛型函数返回的切片类型是底层类型,而不是自定义类型。 那要怎么解决呢?我们在类型参数上再组合一次使用~[]T
构造原始类型。
代码如下:
go
复制代码
package main
import "fmt"
// 自定义int 切片类型
type Point []int
// point 打印方法
func (p Point) print() {
fmt.Printf("Point(%d, %d)", p[0], p[1])
}
// 用泛型的函数 缩放切片
// 引入S 类型保证返回自定义类型
func ScaleSlice[S ~[]T, T int | float64](slice S, scale T) S {
for i, val := range slice {
slice[i] = val * scale
}
return slice
}
func main() {
// 创建一个切片
slice := Point{1, 2, 3, 4, 5}
// 调用泛型函数
scaledSlice := ScaleSlice(slice, 2)
// 打印结果
fmt.Println(scaledSlice)
// [2 4 6 8 10]
fmt.Printf("slice is %T\n", scaledSlice)
// slice is main.Point
scaledSlice.print()
// Point(2, 4)
}
四、总结
什么是泛型,也就是广泛的类型,在定义时候不具体指定某一个具体的类型,而是通过类型参数来表示。以此达到代码复用、简化代码的目的。