编程 泛型方法终于能挂在类型上了:List.Map 的写法、method expression 与接口方法的边界

2026-10-24 21:02:20

Go 1.27 给具体方法加了类型参数:List.Map 能链式写了,接口方法仍然不行

Go 1.27 的语言变更记录:go.dev/blog/generic-methods(Mark Freeman,2026 年 8 月 26 日)。

Go 1.18 引入泛型时,类型和函数都拿到了类型参数,方法没有。当时的泛型提案给的理由是:泛型接口方法难以高效实现,因此连具体(非接口)方法也一并被排除在外;方法主要被当作实现接口的手段。Go 1.27 改变了这一取舍,具体方法可以声明自己的类型参数。

链表上的转换

type List[E any] struct {
	elem E
	next *List[E]
}

func NewList[E any](elems ...E) List[E]
func (List[E]) String() string

把 List[int] 转成 List[string],方法签名是:

func (List[E]) ToString(f func(E) string) List[string]

传入 strconv.Itoa,或者对字节序列用 hex.EncodeToString / base64 编码。

这里的问题是:参数化的是 List,泛化的是来源类型 E,目标类型 R 并没有被参数化。Go 1.18 只能写成包级泛型函数:

func MapList[E, R any](l List[E], f func(E) R) List[R]

MapList 会进入包作用域。当包里多种数据类型都需要 map 操作时,包命名空间会变得拥挤。链式调用也只能由内向外写:

MapList(MapList(NewList(0, 2, 4), add(2)), divideBy(2))

方法版 Map

Go 1.27 可以把它写成方法:

func (List[E]) Map[R any](f func(E) R) List[R]

调用就变成从左到右阅读:

NewList(0, 2, 4).Map(add(2)).Map(divideBy(2))

接收者类型 List[E] 绑定来源元素类型,方法自己的 [R any] 绑定目标类型,两者互不干扰。可读性上的差别在于:操作挂在数据结构上,不再散落在包级命名空间里;阅读顺序和书写顺序一致。

用 method expression 还原函数式写法

如果确实需要函数式的写法,可以取方法表达式:

f := List[int].Map[int]
f(f(NewList(0, 2, 4), add(2)), divideBy(2))

泛型方法和其他泛型实体一样,必须先显式或隐式实例化,才能被调用,或转换成函数值。

接口方法仍然不能带类型参数

下面这种声明是不允许的:

type I interface {
	M[P any]() // 编译错误
}

因此,T 的泛型方法无法参与接口实现。接口实现是某个(可能已经实例化的)类型的属性,而不是某个具体方法的属性:T 声明的是泛型(未实例化)的 T.M,而不是 T.M[int]。

原因在编译模型。接口值是一个盒子,里面装的是任意实现了该接口的类型。包 p 里的:

func F(i I) { i.M[int]() }

单独编译时,编译 main 包的一方无从知道 p.F 会对 T{} 做什么,包括会把 T.M 实例化成哪些类型实参。要覆盖任意用法,编译器就得用所有可能的类型实参去实例化 T.M,这不现实。

另一条路是把方法参数装箱,按约束接口传递,让所有实例共享同一份代码。代价是间接调用开销,而且即使是直接调用一个已实例化的方法,也要承担这笔开销。

结论就一句:Go 1.27 支持具体方法的类型参数,不支持泛型接口方法。

复制全文 生成海报 Go 泛型 语言特性

推荐文章

程序员茄子在线接单