章节

13.1 泛型函数

本篇通过 Go 1.18 前后的对比,学习泛型函数的类型参数、约束与多类型参数写法。

泛型函数

概念说明

泛型让函数可以在保持类型安全的前提下复用逻辑。
它最适合解决“逻辑一样,只是类型不同”的重复代码问题。

例如“两个数相加”这件事,对 intuintfloat64 的写法几乎一样。
Go 1.18 之前通常只能分别写多个函数;Go 1.18 之后可以用泛型把它们合并成一个函数。

类型参数写在函数名后面的 [] 中。
约束用于限制哪些类型可以传入这个类型参数。

语法/规则

  1. 泛型函数写法是 func Name[T Constraint](参数) 返回值
  2. T 是类型参数名,可以在参数和返回值中使用。
  3. 多类型参数写法是 func Name[T Constraint1, K Constraint2](参数) 返回值
  4. any 表示不限制具体类型。
  5. 如果函数内部要使用 >+ 等运算符,约束必须允许这些运算。
  6. ~int 表示底层类型是 int 的类型也满足约束。

为什么需要泛型

1. Go 1.18 以前:相同逻辑往往要写多份

下面这个例子只是做“两个数相加”,但因为类型不同,要分别写 AddIntAddUint

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

import "fmt"

func AddInt(a, b int) int {
	return a + b
}

func AddUint(a, b uint) uint {
	return a + b
}

func main() {
	fmt.Println(AddInt(10, 20))
	fmt.Println(AddUint(10, 20))
}

输出结果:

1
2
30
30

这段代码的问题是:

  1. 两个函数的逻辑完全一样。
  2. 只是参数和返回值类型不同。
  3. 如果还要支持 int64float64,又要继续写 AddInt64AddFloat64
  4. 类型越多,重复代码越多。

2. Go 1.18 以后:把重复逻辑合并成一个泛型函数

有了泛型后,可以把“加法逻辑”抽象成一份代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

import "fmt"

type Number interface {
	int | uint
}

func Add[T Number](a, b T) T {
	return a + b
}

func main() {
	fmt.Println(Add(10, 20))             // 推断 T = int
	fmt.Println(Add(uint(10), uint(20))) // 推断 T = uint
}

输出结果:

1
2
30
30

这时的变化是:

  1. Add 只写一次。
  2. T 表示“先不写死类型,调用时再决定”。
  3. Number 约束表示 T 只能是 intuint
  4. 因为 intuint 都支持 +,所以 return a + b 合法。

这就是泛型的核心意义:
把“相同逻辑、不同类型”的重复代码,收敛为一份类型安全的通用实现。

多类型参数示例

泛型函数不只能有一个类型参数,也可以同时声明多个。
例如下面这个例子中:

  1. T 只能是 int
  2. K 可以是 intstring
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

import "fmt"

func ShowPair[T int, K int | string](count T, tag K) {
	fmt.Printf("count=%v, tag=%v\n", count, tag)
}

func main() {
	ShowPair(3, "go")
	ShowPair(5, 100)
}

输出结果:

1
2
count=3, tag=go
count=5, tag=100

这段代码要重点看函数头:

1
func ShowPair[T int, K int | string](count T, tag K)
  1. [T int, K int | string] 表示这里有两个类型参数:TK
  2. T int 表示 T 只能是 int
  3. K int | string 表示 K 可以是 intstring
  4. count T 表示参数 count 的类型由 T 决定。
  5. tag K 表示参数 tag 的类型由 K 决定。

所以:

1
ShowPair(3, "go")

近似可以理解成:

1
ShowPair[int, string](3, "go")

而:

1
ShowPair(5, 100)

近似可以理解成:

1
ShowPair[int, int](5, 100)

这说明不同的类型参数可以有不同的约束,
一个函数里可以同时处理多种“位置不同、限制不同”的类型。

泛型最大值示例

上面的 Add 例子说明了“为什么需要泛型”。
下面再看一个更常见的泛型函数:求最大值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

type Ordered interface {
	~int | ~float64 | ~string // 约束接口:底层类型是 int / float64 / string 的类型都可以
}

func Max[T Ordered](a, b T) T { // 声明类型参数 T,并要求 T 满足 Ordered
	if a > b {
		return a
	}
	return b
}

func main() {
	fmt.Println(Max(3, 5))         // 推断 T = int
	fmt.Println(Max("go", "java")) // 推断 T = string
}

输出结果:

1
2
5
java

函数解析

1. 先看 Ordered

1
2
3
type Ordered interface {
	~int | ~float64 | ~string
}
  1. interface 这里是“约束接口”,不是“方法接口”。
  2. | 表示“或者”。
  3. ~int 表示“底层类型是 int 的类型”。
  4. Ordered 表示:intfloat64string,以及它们的同底层自定义类型。

2. 再看函数头

1
func Max[T Ordered](a, b T) T
  1. Max 函数名。
  2. [T Ordered] 声明类型参数 T,并限制 T 必须属于 Ordered
  3. (a, b T) 参数 ab 的类型都是 T
  4. 最后一个 T 返回值类型也是 T

3. 调用时发生了什么

1
Max(3, 5)
  1. 实参是 int
  2. 推断 T = int
  3. 近似看成 Max[int](3, 5)
1
Max("go", "java")
  1. 实参是 string
  2. 推断 T = string
  3. 近似看成 Max[string]("go", "java")

4. 为什么 a > b 可以写

  1. 因为 T 被限制为 Ordered
  2. Ordered 里都是支持 > 的类型
  3. 所以 a > b 合法

5. AddMax 这两类泛型函数有什么共同点

  1. 都先声明类型参数 T
  2. 都给 T 指定了约束,避免传入不合适的类型。
  3. 都把原本需要写多份的逻辑,合并成了一份函数实现。
  4. 调用时通常可以自动推断类型,不需要每次手动写成 Add[int](1, 2)

常见错误

  1. 使用泛型后在函数体内做了约束不允许的运算,导致编译失败。
  2. 忘了多个类型参数可以分别写约束,例如 [T int, K string]
  3. 为了“高级”而滥用泛型,让简单函数变难读。
  4. 约束写得过宽,函数内部实际又依赖特定类型能力。
  5. 约束写得过窄,导致本来可复用的类型无法传入。
本文禁止转载
使用 Hugo 构建
主题 StackJimmy 设计 由 Hobin 魔改
最近构建时间:2026-04-17 19:07:48 CST
载入天数...载入时分秒...
发表了 1 篇文章 · 发表了 152 篇笔记 · 总计 18 万 0 千字