泛型函数
概念说明
泛型让函数可以在保持类型安全的前提下复用逻辑。
它最适合解决“逻辑一样,只是类型不同”的重复代码问题。
例如“两个数相加”这件事,对 int、uint、float64 的写法几乎一样。
Go 1.18 之前通常只能分别写多个函数;Go 1.18 之后可以用泛型把它们合并成一个函数。
类型参数写在函数名后面的 [] 中。
约束用于限制哪些类型可以传入这个类型参数。
语法/规则
- 泛型函数写法是
func Name[T Constraint](参数) 返回值。 T是类型参数名,可以在参数和返回值中使用。- 多类型参数写法是
func Name[T Constraint1, K Constraint2](参数) 返回值。 any表示不限制具体类型。- 如果函数内部要使用
>、+等运算符,约束必须允许这些运算。 ~int表示底层类型是int的类型也满足约束。
为什么需要泛型
1. Go 1.18 以前:相同逻辑往往要写多份
下面这个例子只是做“两个数相加”,但因为类型不同,要分别写 AddInt 和 AddUint:
| |
输出结果:
| |
这段代码的问题是:
- 两个函数的逻辑完全一样。
- 只是参数和返回值类型不同。
- 如果还要支持
int64、float64,又要继续写AddInt64、AddFloat64。 - 类型越多,重复代码越多。
2. Go 1.18 以后:把重复逻辑合并成一个泛型函数
有了泛型后,可以把“加法逻辑”抽象成一份代码:
| |
输出结果:
| |
这时的变化是:
Add只写一次。T表示“先不写死类型,调用时再决定”。Number约束表示T只能是int或uint。- 因为
int和uint都支持+,所以return a + b合法。
这就是泛型的核心意义:
把“相同逻辑、不同类型”的重复代码,收敛为一份类型安全的通用实现。
多类型参数示例
泛型函数不只能有一个类型参数,也可以同时声明多个。
例如下面这个例子中:
T只能是intK可以是int或string
| |
输出结果:
| |
这段代码要重点看函数头:
| |
[T int, K int | string]表示这里有两个类型参数:T和K。T int表示T只能是int。K int | string表示K可以是int或string。count T表示参数count的类型由T决定。tag K表示参数tag的类型由K决定。
所以:
| |
近似可以理解成:
| |
而:
| |
近似可以理解成:
| |
这说明不同的类型参数可以有不同的约束,
一个函数里可以同时处理多种“位置不同、限制不同”的类型。
泛型最大值示例
上面的 Add 例子说明了“为什么需要泛型”。
下面再看一个更常见的泛型函数:求最大值。
| |
输出结果:
| |
函数解析
1. 先看 Ordered
| |
interface这里是“约束接口”,不是“方法接口”。|表示“或者”。~int表示“底层类型是int的类型”。Ordered表示:int、float64、string,以及它们的同底层自定义类型。
2. 再看函数头
| |
Max函数名。[T Ordered]声明类型参数T,并限制T必须属于Ordered。(a, b T)参数a、b的类型都是T。- 最后一个
T返回值类型也是T。
3. 调用时发生了什么
| |
- 实参是
int - 推断
T = int - 近似看成
Max[int](3, 5)
| |
- 实参是
string - 推断
T = string - 近似看成
Max[string]("go", "java")
4. 为什么 a > b 可以写
- 因为
T被限制为Ordered Ordered里都是支持>的类型- 所以
a > b合法
5. Add 和 Max 这两类泛型函数有什么共同点
- 都先声明类型参数
T。 - 都给
T指定了约束,避免传入不合适的类型。 - 都把原本需要写多份的逻辑,合并成了一份函数实现。
- 调用时通常可以自动推断类型,不需要每次手动写成
Add[int](1, 2)。
常见错误
- 使用泛型后在函数体内做了约束不允许的运算,导致编译失败。
- 忘了多个类型参数可以分别写约束,例如
[T int, K string]。 - 为了“高级”而滥用泛型,让简单函数变难读。
- 约束写得过宽,函数内部实际又依赖特定类型能力。
- 约束写得过窄,导致本来可复用的类型无法传入。