bigywater.tech
Go · 29

类型约束:comparable、联合类型、类型推断的边界,以及 Go 1.27 新增的泛型方法

泛型的约束写得对不对,编译器会立刻告诉你。这篇逐个用真实的编译器报错确认规则:comparable、联合类型与 ~、运算符、类型推断的成功与失败,以及这个版本里新出现的“泛型方法”。

~
MyInt 要满足 int | float64 必须写成 ~int | ~float64
cannot infer T
类型参数只出现在返回值里时,必须显式实例化 Zero[int]()
go1.27
泛型方法:go.mod 写 go 1.26 会报 generic method requires go1.27 or later

comparable

实测 · 本机执行输出
  Index 用于 int / string / 结构体 / 数组 / 指针: 编译通过,运行输出:2 1 1 0 0
  Index 用于 []int 的切片(切片不可比较):
    main.go:12:32: []int does not satisfy comparable
  Index 用于 map[string]int:
    main.go:12:32: map[string]int does not satisfy comparable
  Index 用于 func():
    main.go:12:32: func() does not satisfy comparable
  含切片字段的结构体:
    main.go:13:32: Bad does not satisfy comparable
  1 (any 作为实参:编译通过,比较 1 与 "x" 没问题)
    接口类型实参 any:Index([]any{[]int{1}}, []int{1}) 运行时 panic: runtime error: comparing uncomparable type []int

comparable 要求类型的值可以用 == 比较。切片、map、函数和含这些的结构体不满足,编译器直接拒绝。接口类型(比如 any)作为实参是允许的——这样 Index[any] 能编译,但如果装进去的动态类型不可比较,比较时会在运行时 panic(上面的 comparing uncomparable type []int)。

联合类型与波浪号

实测 · 本机执行输出
  SumA[MyInt](约束没有 ~):
    main.go:12:31: MyInt does not satisfy NumNoTilde (possibly missing ~ for int in NumNoTilde)
  SumB[MyInt](约束有 ~int): 编译通过,运行输出:3 1.5

int | float64 只接受恰好是这些类型的实参;~int 表示“底层类型是 int 的所有类型”,所以自定义的 MyInt 要靠 ~ 才能满足。编译器的提示已经把原因写出来了:possibly missing ~ for int。

约束与运算符

实测 · 本机执行输出
  T any 上用 >:
    main.go:8:35: invalid operation: a > b (type parameter T cannot use operator >)
  T comparable 上用 <:
    main.go:3:47: invalid operation: a < b (type parameter T cannot use operator <)
  cmp.Ordered 上用 <: 编译通过,运行输出:2 b 1.5
  cmp.Ordered 的定义(标准库源码):
    type Ordered interface {
    	~int | ~int8 | ~int16 | ~int32 | ~int64 |
    		~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
    		~float32 | ~float64 |
    		~string
    }

泛型函数体里只能使用约束保证存在的操作。any 什么操作都不保证,comparable 只保证 == 与 !=,要 < 就用 cmp.Ordered(标准库里它就是一个联合类型的约束)。

类型推断的边界

实测 · 本机执行输出
  Max(1, 2.5)(无类型常量混用): 编译通过,运行输出:  float64 2.5
  Max(x, 2.5),x 是 int 变量:
    main.go:13:42: cannot use 2.5 (untyped float constant) as int value in argument to Max (truncated)
  Zero()(只能从返回值推断):
    main.go:13:31: in call to Zero, cannot infer T (declared at ./main.go:9:11)
  Zero[int]() 显式实例化: 编译通过,运行输出:  0 ""
  Map(xs, strconv.Itoa) 同时推断 T 与 U: 编译通过,运行输出:  []string [1 2]
  First(IDs{3, 4}) 用 ~[]E 约束保留具名切片类型: 编译通过,运行输出:  3
  f := Max(不实例化就当值用):
    main.go:13:20: cannot use generic function Max without instantiation

能推断:从函数实参(包括无类型常量混用,Max(1, 2.5) 是 float64)、从函数类型实参(Map(xs, strconv.Itoa) 推出 T 与 U)、从带 ~[]E 的约束里提取元素类型。不能推断:只在返回值里出现的类型参数,以及不实例化就把泛型函数当值使用。

Go 1.27 的泛型方法

实测 · 本机执行输出
  go.mod 写 go 1.26:编译失败
    main.go:7:24: generic method requires go1.27 or later (-lang was set to go1.26; check go.mod)
  go.mod 写 go 1.27:编译通过,运行输出:[10 20 30] |   func(func(int) float64) main.List[float64]
  Box[int] 有泛型方法 Map,能实现 interface{ Map(func(int) string) string } 吗:
    main.go:10:17: cannot use Box[int]{…} (value of struct type Box[int]) as Mapper value in variable declaration: Box[int] does not implement Mapper (wrong type for method Map)
  接口方法声明类型参数:
    main.go:3:27: interface method must have no type parameters
    main.go:3:45: undefined: U
  不实例化就把泛型方法当值:
    main.go:9:7: cannot use generic function b.Map without instantiation

读规范(doc/go_spec.html 里的 “generic method [Go 1.27]”)可以看到:方法声明自己的类型参数,实例化后才能调用或当值使用。实验证实它受语言版本控制(go.mod 写 1.26 就报错),并且有两条限制:带类型参数的方法与接口里的普通方法签名不相同,所以不能用来实现那个接口;接口自己的方法不能再声明类型参数。我这里只验证了编译与简单运行,没有展开其它规则。

分步演示

类型约束
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew);用 go run / go build / go test 直接编译运行,需要第三方库时经代理从 proxy.golang.org 下载;计时用 time.Now / testing.B,数字每次运行会有小幅抖动。每个例子都是一个独立的小程序,编译错误是 go build 的原样输出;读过的资料:标准库 cmp/cmp.go(Ordered 的定义)、doc/go_spec.html 里关于泛型方法与接口方法的两段
    • 没有验证:约束里带方法的联合类型与核心类型(core type)的细则、类型推断的完整算法(只验证了几个典型成功与失败)、泛型方法与反射、泛型方法在 1.27 之前版本中的旧行为(这里只有 -lang 门槛的报错)、泛型类型的别名
    京ICP备2026067502号-1京公网安备11010502063487号