为什么使用Go泛型?参数多态性简介

简化代码:泛型如何解决重复逻辑问题

image.png|300

写在之前

最近一段时间,工作忙碌没有太多时间用于写作。而闲暇时候,我沉湎于股市,一定程度上浪费了一些时间。在 AI 的时代,利用 AI 来学习是一种正确的方式,后续我会尽量花时间学习一些东西,并及时分享出来,希望能保持常态化的更新频率。

在 AI 时代,被 AI 影响是不可避免的,我选择拥抱、适应而不是对抗。在如今的潮流中如何保持个人的 storytelling 能力、持续进步是一个很大挑战,也是人类的挑战 —- 或许这样自身才不会退化。就目前而言,我觉得 AI 做到了很多事情,但是还是以人为本,为人服务。

替代人类或许就在不远的将来,谁知道呢🤷‍♂️?还是先聚焦于当下的事吧。

前言

我的工作生活中很少用到 Golang 的泛型,我想是时候一点点主动学习掌握它了。

泛型编程(Generic Programming),也称为参数化多态(Parametric Polymorphism),是一种通过编写“通用函数”来减少函数重复的编程方法。这类函数可以支持多个类型参数或参数值。

在 Go 编程语言的语境中,泛型曾是一项备受期待的特性,并最终在 Go 1.18 版本中正式加入语言核心。引入泛型的总体目标是避免样板代码(boilerplate code)和逻辑重复,使开发者能够编写可以作用于任意类型或值的函数和库。


1. 问题背景:没有泛型之前的编程方式

在泛型引入之前,像 Go 这样静态类型语言中的每一个函数,都必须明确地作用于某一种具体类型

如果开发者希望对不同的数据类型(例如 intint64string)实现完全相同的逻辑,就不得不为每种类型分别实现一个几乎一模一样的函数。

在大型代码库中,这种做法往往会导致成千上万行重复代码,即便只是一些非常基础的工具函数,比如在数组中查找元素、或对切片进行反转等。这种实现方式明显违背了 DRY(Don’t Repeat Yourself,不要重复自己) 原则。


示例代码:没有泛型时的代码重复问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 反转一个整数切片
func ReverseInts(s []int) {
first, last := 0, len(s)-1
for first < last {
s[first], s[last] = s[last], s[first]
first++
last--
}
}

// 反转一个字符串切片,需要重新实现一份几乎完全相同的函数
func ReverseStrings(s []string) {
first, last := 0, len(s)-1
for first < last {
s[first], s[last] = s[last], s[first]
first++
last--
}
}

在上述示例中,函数的核心逻辑完全一致,但由于函数签名中的类型不同,代码被迫重复实现

在泛型出现之前,开发者通常会尝试使用接口(interface)和类型断言(type assertion)来支持多种类型,但这种方式往往显得笨重,需要手动进行类型判断,同时还牺牲了编译期类型安全性这一重要优势。


2. 引入泛型的动机

引入泛型的核心动机,在于能够将元素类型(element type)从算法和数据结构中抽离出来。这样一来,开发者可以:

  • 一次编写,到处使用(Write once, use everywhere):
    MapFilterReduce 这类通用工具函数,只需编写一次,就可以作为通用包,适用于任意类型的值。

  • 类型安全的容器(Type-Safe Containers):
    泛型允许创建诸如集合(Set)、树(Tree)等容器,并且在编译期就能保证类型安全,无需再依赖空接口(interface{})。

  • 减少样板代码(Reduced Boilerplate):
    不再需要为每一种结构体或基础类型分别实现一套逻辑,代码体量显著缩小,也更容易维护。


3. 泛型如何解决这些问题

泛型通过引入类型参数(Type Parameters)来解决上述问题。类型参数本质上是类型的占位符,真正的类型会在函数调用时被具体化。

这些类型参数通常会受到约束(Constraints)的限制。约束本质上是一种接口类型,用来规定泛型类型必须支持哪些操作。


示例代码:使用泛型实现可复用逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 一个适用于任意切片类型的泛型函数
func Reverse[T any](s []T) {
first, last := 0, len(s)-1
for first < last {
s[first], s[last] = s[last], s[first]
first++
last--
}
}

func main() {
ints := []int{1, 2, 3}
strings := []string{"a", "b", "c"}

// 同一个函数可以安全地处理不同类型
Reverse(ints)
Reverse(strings)
}

在这个示例中,T 是一个类型参数,any 是它的约束,表示 T 可以代表任意类型

Go 编译器会使用类型推断(Type Inference)机制,因此在大多数情况下,调用者甚至不需要显式地指定类型参数。

4. 何时该使用(以及不该使用)泛型

尽管泛型功能强大,但它也为语言引入了一定的复杂度。因此,Go 团队建议谨慎、克制地使用泛型

  • 适合使用泛型的场景:
    当你需要对不同的数据类型实现完全相同的行为时,例如:

    • 作用于容器类型(mapslicechannel)的通用函数
    • 通用的数据结构(如集合、队列、树等)
  • 不适合使用泛型的场景:

    • 你只是对某个类型参数调用方法时(此时应使用接口类型
    • 你需要针对不同的具体类型执行不同的逻辑时(应使用反射(reflection)API

总结

泛型通过允许开发者编写可复用、已充分调试并针对多种类型优化的库,使 Go 这样的静态类型语言变得更安全、更高效、也更强大

它将复杂性从库的使用者转移到了库的编写者身上,从而保证日常业务代码依然保持简洁、可读,同时又获得了显著的灵活性。

可以将泛型理解为一个高质量的厨房模具

如果没有泛型,你需要为每一种材料准备一个独立、专用的模具——
比如一个专门做巧克力的、一个做果冻的、还有一个做冰块的——即便它们最终的形状都是同样的五角星。

而有了泛型,你只需要一个通用的五角星模具,无论你往里倒入的是哪种“材料”(类型),只要这种材料满足“可以被倒入并凝固”的条件(约束),这个模具就能稳定地生产出完美的五角星,而你无需重复发明工具。

更多内容

最近文章:

随机文章:


更多该系列文章,参考medium链接:

https://wesley-wei.medium.com/list/you-should-know-in-golang-e9491363cd9a

English post: https://programmerscareer.com/go-generics-01/
作者:微信公众号,Medium,LinkedIn,Twitter
发表日期:原文在 2026-01-01 10:37 时创作于 https://programmerscareer.com/zh-cn/go-generics-01/
版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证

Go中的泛型语法:声明函数和类型 Rob Pike对GenAI的反应:简单与复杂的碰撞

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×