泛型函数
下面两个函数做的是同一件事,一个给 int,一个给 float64。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 就像别的类型一样用:
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 会从实参推断出来。也可以自己写在方括号里:
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:
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,所有类型都满足它:
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}
T 可以是任何类型,包括没有 > 的,所以 Go 拒绝编译 a > b。约束得保证 > 能用:cmp.Ordered。
自己的约束
约束是一个列出类型的接口,类型之间用 | 隔开。前面加 ~ 的意思是「这个类型,或者以它为底的类型」,比如 type Celsius float64:
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,是编译错误,错误里会说出是哪个类型:
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 课):
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 包里还有很多,对什么类型的切片都管用:
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 里是不是已经有了。
练一练
写好代码,点检查。提示随便看,实在想不出来再看答案。
什么切片都能找最小的
✓写好 MinOf,返回 items 里最小的元素,对能比大小的类型都管用。程序用数和单词各试一次。(自己写循环,不用 slices.Min。)
7 apple
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}
数一数符合的有几个
✓写好泛型函数 Count:返回 items 里让 match 返回 true 的元素有几个。程序用 int 和字符串各调用一次。
3 2
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}
修好它:只收 int 的 Sum
✓这段程序编译不过:约束 Number 只允许 int。改一下约束,让 Sum 也能用在 float64 上。
60 7.5
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}
这一课学会了
- 泛型函数在方括号里写类型参数:
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)是用泛型写的。