HeadlinesBriefing HeadlinesBriefing.com

Go 1.27 泛型方法详解

Hacker News •
×

当 Go 1.18 在 2022 年引入泛型时,它为函数和结构体带来了泛型类型参数,但方法却被排除在外。随着 Go 1.27 的发布,这一长期存在的限制终于被移除了。方法现在可以在不添加到接收结构体的情况下定义自己的类型参数。

为什么引入这一变化 如果你想创建一个保存泛型值的图节点,你可能会这样实现它:

type Node[T any] struct {

value T

}

想象一下添加一个 Map 方法,将类型 T 的节点转换为另一种类型 U 的节点。在 Go 1.27 之前,你别无选择,只能将 U 直接添加到 Node 结构体本身中:

type Node[T any, U any] struct {

value T

}

func (n *Node[T, U]) Map() Node[T, U] {

// ...

}

将 U 添加到结构体本身是一种糟糕的设计,因为 U 是特定于 Map 的类型参数。即使其他方法不会使用 U,它们仍然不得不在接收器声明中保留它。唯一的解决方法是将 Map 实现为包级别的函数,而不是方法,因为函数可以定义自己的类型参数。但方法不能,这导致了 awkward 和非惯用代码 API 设计。从 Go 1.27 开始,U 可以仅在 Map 方法上定义:

func (n *Node[T]) Map[U any]() Node[U] {

// ...

}

为什么花了这么长时间?答案就在于 Go 中泛型的实现方式。编译器主要使用单态化来处理泛型,这意味着它会为每种特定类型创建泛型结构体或函数的副本。因为在某些时候,抽象的泛型概念需要被转化为直接的机器代码。然而,Go 的接口系统在运行时运行。传递给接口参数的值的具体类型是在程序运行时解析的。这种动态调度与泛型在编译时被解析的事实发生了冲突。考虑一下如果我们想在接口中声明我们的 Map 方法会发生什么:

type Mapper interface {

Map T any, U any U

}

为了使其工作,运行时要么需要一个即时编译器(JIT)来动态生成特定类型的机器代码,要么需要提前为每种可能的类型创建这些副本,导致二进制文件急剧膨胀。最终,Go 团队决定将泛型方法与接口分开。Go 1.27 允许在具体类型上使用泛型方法,但不允许在接口上使用。这种区别就是为什么上面的示例无法编译以及为什么围绕它的讨论花了这么长时间。

来源: Hacker News · 由HeadlinesBriefing整理摘要