【The Go Blog】2026-08-26_泛型方法

2026-08-26_泛型方法

泛型对 Go 来说是一个深远的变化——通过向语言添加类型参数,它从根本上扩展了 gopher 可以编写的程序类型。

现在可以在 Go 中表达泛型类型和函数。这些构造减少了对特定数据类型和函数的需求,从而降低了程序的冗长性,并在这些情况下提高了语言的易用性。

例如,不同类型的链表现在可以被归纳为一个单一的类型定义:

// before Go 1.18typeListOfIntsstruct{elemintnext*ListOfInts}typeListOfStringsstruct{elemstringnext*ListOfStrings}// ...// after Go 1.18typeList[E any]struct{elem E next*List[E]}

同样,对不同类型的有序数据进行排序可以表示为一个函数:

// before Go 1.18funcSortInts(s[]int){/* ... */}funcSortStrings(s[]string){/* ... */}// ...// after Go 1.18funcSort[E cmp.Ordered](s[]E){/* ... */}

然而,这种能力并没有出现在方法中。

通用类型提案的理由是,因为通用接口方法难以高效实现(稍后讨论),因此在非接口(或“具体”)方法中添加类型参数也没有意义。在这种观点下,方法主要是实现接口的手段。

Go 1.27 采取了不同的观点,因此在 Go 中添加了泛型方法。在本文中,我们将解释这种观点的变化,并演示这个新特性的使用方法。

组织方法

方法使围绕类型组织功能成为可能。为了说明,让我们重新考虑上面的链表(添加一些便利功能):

typeList[E any]struct{elem E next*List[E]}funcNewList[E any](elems...E)List[E]{/* ... */}func(List[E])String()string{/* ... */}

假设有人想把这个结构映射为保存某种其他类型的值,比如字符串。像 ToString 这样的方法可以做到:

func(List[E])ToString(ffunc(E)string)List[string]{/* ... */}

通过提供一个“transform”函数 f,ToString 可以使用“现成”的例程进行自定义。例如,strconv.Itoa 可能适用于 List[int]:

funcmain(){fmt.Println(NewList(1,2,3).ToString(strconv.Itoa))// [1 2 3]}

对于 List[[]byte],会想到更多选项(取决于使用情况):

funcmain(){l:=NewList([]byte("Hallo Welt"),[]byte("Helló világ"))fmt.Println(l.ToString(hex.EncodeToString))// [48616c6c6f2057656c74 48656c6cc3b32076696cc3a167]fmt.Println(l.ToString(base64.StdEncoding.EncodeToString))// [SGFsbG8gV2VsdA== SGVsbMOzIHZpbMOhZw==]}

请注意,对 List 进行参数化允许源类型的泛化,但不允许目标类型的泛化,因为这取决于正在应用的转换。对于单一目标类型,这可能是合理的,但如果有许多目标类型呢?

在 Go 1.18 中,可以使用泛型函数来实现这一点:

// after Go 1.18funcMapList[E,R any](l List[E],ffunc(E)R)List[R]{/* ... */}

但是使用函数的缺点是会将 MapList 移动到包作用域——如果许多数据类型支持映射操作,空间可能会变得拥挤。此外,链式调用必须以“反向”方式编写:

funcmain(){fmt.Println(MapList(MapList(NewList(0,2,4),add(2)),divideBy(2)))// [1 2 3]}

因为 Go 1.18 不支持泛型方法,我们需要一个泛型函数作为替代方法。这个问题在 Go 1.27 中已得到解决:

// after Go 1.27func(List[E])Map[R any](ffunc(E)R)List[R]{/* ... */}

这是紧凑的、富有表现力的,并且具有局部作用域。此外,链式调用更易读,因为它们可以更自然地从左到右书写:

funcmain(){fmt.Println(NewList(0,2,4).Map(add(2)).Map(divideBy(2)))// [1 2 3]}

更喜欢“由内而外”的形式吗?方法表达式可以将任何方法(包括泛型方法)转换为其函数等价形式。因此,如果需要,可以恢复函数调用结构:

funcmain(){f:=List[int].Map[int]fmt.Println(f(f(NewList(0,2,4),add(2)),divideBy(2)))// [1 2 3]}

通用方法,像 Go 中的其他泛型一样,必须在使用之前(调用或转换为函数时)被实例化(可以是显式的或隐式的)。

结论

Go 1.27 在具体方法中引入了类型参数——这个特性一直是 Go 社区高度期望的,因为它允许编写更符合人体工程学且可读性更高的代码。尽管我们无法支持泛型接口方法,但我们认为允许具体方法使用类型参数仍然是有价值的。

我们希望你喜欢使用 Go 的泛型方法,并能在你的项目中找到有用的使用方式!