Kodewick
开始学 →
第 17 课,共 24 课目录

泛型函数

第 17 课,共 24 课 · 第 4 部分 · 出错和函数 · 约 15 分钟

下面两个函数做的是同一件事,一个给 int,一个给 float64。Go 不让一个函数同时收这两种类型,代码只好写两遍:

prog.go
1package main2 3import "fmt"4 5func maxInt(a, b int) int {6	if a > b {7		return a8	}9	return b10}11 12func maxFloat(a, b float64) float64 {13	if a > b {14		return a15	}16	return b17}18 19func main() {20	fmt.Println(maxInt(3, 7))21	fmt.Println(maxFloat(2.5, 1.5))22}
输出
7
2.5

要比字符串,还得再抄第三遍。泛型让你只写一遍。

类型参数

泛型函数在普通参数前面多一对方括号,里面是类型参数。[T cmp.Ordered] 的意思是:「T 代表某个类型,它得是能用 <、> 比大小的」。然后 T 就像别的类型一样用:

prog.go
1package main2 3import (4	"cmp"5	"fmt"6)7 8func Max[T cmp.Ordered](a, b T) T {9	if a > b {10		return a11	}12	return b13}14 15func main() {16	fmt.Println(Max(3, 7))17	fmt.Println(Max(2.5, 1.5))18	fmt.Println(Max("apple", "banana"))19}
输出
7
2.5
banana

cmp.Ordered 是一个约束:T 可以是哪些类型(所有整数和浮点数类型,还有 string)。每次调用,T 就变成其中一种:int、float64、string。

Go 自己推出 T

一般不用说 T 是什么,Go 会从实参推断出来。也可以自己写在方括号里:

prog.go
1package main2 3import (4	"cmp"5	"fmt"6)7 8func Max[T cmp.Ordered](a, b T) T {9	if a > b {10		return a11	}12	return b13}14 15func main() {16	fmt.Println(Max[float64](3, 4))17	fmt.Printf("%T\n", Max[float64](3, 4))18	fmt.Println(Max(2, 2.5))19}
输出
4
float64
2.5

Max(2, 2.5) 也行:直接写的数还没有固定的类型,Go 让两个都当 float64。变量的类型可是固定的,两个实参得对得上同一个 T:

prog.go
1package main2 3import (4	"cmp"5	"fmt"6)7 8func Max[T cmp.Ordered](a, b T) T {9	if a > b {10		return a11	}12	return b13}14 15func main() {16	price := 1017	rate := 1.518	fmt.Println(Max(price, rate))19}
输出
# command-line-arguments
./prog.go:18:25: in call to Max, type float64 of rate does not match inferred type int for T

发生了什么: T 只能是一种类型:前面的实参让它成了 int,rate 却是 float64。把参数转成同一种,或者写明 Max[float64](…)。

price 是 int,rate 是 float64,没有一个 T 能同时是两个:type float64 of rate does not match inferred type int for T(rate 的类型 float64 和推出来的 T(int)对不上)。Go 先从第一个实参定下 T,再发现第二个不一样。写成 Max(float64(price), rate) 就好了。

any:什么类型都行

函数不需要比较、也不需要相加时,约束可以写 any,所有类型都满足它:

prog.go
1package main2 3import "fmt"4 5func Last[T any](items []T) T {6	return items[len(items)-1]7}8 9func Repeat[T any](v T, n int) []T {10	result := make([]T, n)11	for i := range n {12		result[i] = v13	}14	return result15}16 17func main() {18	fmt.Println(Last([]string{"牛奶", "面包", "鸡蛋"}))19	fmt.Println(Last([]float64{36.6, 37.1}))20	fmt.Println(Repeat("好", 3))21	fmt.Println(Repeat(true, 2))22}
输出
鸡蛋
37.1
[好 好 好]
[true true]

可是用了 any,Go 就不让你对 T 用 > 或 +:有些类型(结构体、切片)没有这些运算。

先猜一猜

会怎样?

1package main2 3import "fmt"4 5func Biggest[T any](a, b T) T {6	if a > b {7		return a8	}9	return b10}11 12func main() {13	fmt.Println(Biggest(3, 9))14}

自己的约束

约束是一个列出类型的接口,类型之间用 | 隔开。前面加 ~ 的意思是「这个类型,或者以它为底的类型」,比如 type Celsius float64:

prog.go
1package main2 3import "fmt"4 5type Number interface {6	~int | ~float647}8 9type Celsius float6410 11func Sum[T Number](nums []T) T {12	var total T13	for _, n := range nums {14		total += n15	}16	return total17}18 19func main() {20	fmt.Println(Sum([]int{120, 35, 8}))21	fmt.Println(Sum([]float64{2.5, 0.25}))22	fmt.Println(Sum([]Celsius{20.5, 21}))23}
输出
163
2.75
41.5

var total T 是 T 的零值,所有数的类型都是 0。用不在名单里的类型调用 Sum,是编译错误,错误里会说出是哪个类型:

prog.go
1package main2 3import "fmt"4 5type Number interface {6	~int | ~float647}8 9func Sum[T Number](nums []T) T {10	var total T11	for _, n := range nums {12		total += n13	}14	return total15}16 17func main() {18	fmt.Println(Sum([]string{"甲", "乙"}))19}
输出
# command-line-arguments
./prog.go:18:17: string does not satisfy Number (string missing in ~int | ~float64)

发生了什么: string 不在约束 Number 允许的类型里(~int | ~float64)。

string does not satisfy Number:string 不满足 Number,它既不是 int 也不是 float64,也不是以它们为底的类型。

两个类型参数

一个函数可以有不止一个类型参数。Map 用你给的函数把 []T 变成 []U(函数值,第 16 课):

prog.go
1package main2 3import (4	"fmt"5	"strings"6)7 8func Map[T, U any](items []T, f func(T) U) []U {9	result := make([]U, 0, len(items))10	for _, item := range items {11		result = append(result, f(item))12	}13	return result14}15 16func main() {17	names := []string{"ana", "ben", "carla"}18	fmt.Println(Map(names, strings.ToUpper))19	fmt.Println(Map(names, func(s string) int { return len(s) }))20}
输出
[ANA BEN CARLA]
[3 3 5]

第一次是字符串变字符串;第二次是字符串变 int,所以 U 是 int。

slices 包

其实你已经用过泛型函数了:第 8 课的 slices.Sort 就是。slices 包里还有很多,对什么类型的切片都管用:

prog.go
1package main2 3import (4	"fmt"5	"slices"6)7 8func main() {9	scores := []int{72, 95, 58, 88}10	fmt.Println(slices.Max(scores), slices.Min(scores))11	fmt.Println(slices.Contains(scores, 58), slices.Index(scores, 88))12	words := []string{"pear", "fig", "apple"}13	fmt.Println(slices.Max(words), slices.Index(words, "kiwi"))14	slices.Reverse(words)15	fmt.Println(words)16}
输出
95 58
true 3
pear -1
[apple fig pear]

找不到时 slices.Index 返回 -1。要对切片写循环之前,先看看 slices 里是不是已经有了。

练一练

写好代码,点检查。提示随便看,实在想不出来再看答案。

练习 1

什么切片都能找最小的

写好 MinOf,返回 items 里最小的元素,对能比大小的类型都管用。程序用数和单词各试一次。(自己写循环,不用 slices.Min。)

要显示
7
apple
exercise1.go
1package main2 3import (4	"cmp"5	"fmt"6)7 8func MinOf[T cmp.Ordered](items []T) T {9	return items[0]10}11 12func main() {13	fmt.Println(MinOf([]int{42, 7, 19}))14	fmt.Println(MinOf([]string{"pear", "fig", "apple"}))15}
输出
点「运行」或按 Ctrl+Enter
练习 2

数一数符合的有几个

写好泛型函数 Count:返回 items 里让 match 返回 true 的元素有几个。程序用 int 和字符串各调用一次。

要显示
3
2
exercise2.go
1package main2 3import "fmt"4 5func Count[T any](items []T, match func(T) bool) int {6	return 07}8 9func main() {10	temps := []int{18, 25, 31, 22, 29}11	fmt.Println(Count(temps, func(t int) bool { return t > 24 }))12	names := []string{"小林", "老王", "小陈"}13	fmt.Println(Count(names, func(s string) bool { return []rune(s)[0] == '小' }))14}
输出
点「运行」或按 Ctrl+Enter
练习 3

修好它:只收 int 的 Sum

这段程序编译不过:约束 Number 只允许 int。改一下约束,让 Sum 也能用在 float64 上。

要显示
60
7.5
exercise3.go
1package main2 3import "fmt"4 5type Number interface {6	~int7}8 9func Sum[T Number](nums []T) T {10	var total T11	for _, n := range nums {12		total += n13	}14	return total15}16 17func main() {18	fmt.Println(Sum([]int{10, 20, 30}))19	fmt.Println(Sum([]float64{2.5, 5}))20}
输出
点「运行」或按 Ctrl+Enter

这一课学会了

  • 泛型函数在方括号里写类型参数:func Max[T cmp.Ordered](a, b T) T。
  • 约束说明 T 可以是哪些类型:cmp.Ordered(能用 < >)、any(什么类型都行)。
  • Go 一般从实参推出 T;Max[float64](3, 4) 是自己写明;类型不一样的变量凑不成一个 T。
  • 自己的约束列出类型:interface { ~int | ~float64 };~ 让以它们为底的类型也能用。
  • 一个函数可以有几个类型参数:Map[T, U any]。
  • slices 包(Sort、Max、Min、Contains、Index、Reverse)是用泛型写的。
这一课学完了吗?点一下标记。进度存在这个浏览器里,随时可以撤销。