CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Go 语言入门/课时/泛型入门
RawBlame
课时目录12 节

Go 语言入门

  1. 01Go 是什么
  2. 02基础语法与类型
  3. 03数组、切片与 map
  4. 04结构体与接口
  5. 05模块与包管理
  6. 06并发模型
  7. 07错误处理
  8. 08泛型入门
  9. 09I/O 与 CLI
  10. 10Web 与数据库
  11. 11测试与基准
  12. 12综合练习

泛型入门

generic functions/types、constraints、type inference。

30 分钟

泛型入门

这一课你将能做到

  1. 编写带类型参数的函数(如 Max[T cmp.Ordered]),并利用类型推断省略方括号
  2. 自定义类型约束(类型集语法、~ 波浪号),理解 comparable 的用途
  3. 定义泛型结构体(如 Stack[T]),并知道什么场景该用泛型、什么场景不该

为什么要学

Go 1.18 之前,想写一个「对任意类型生效」的函数只有两条烂路:为每个类型复制粘贴,或用 interface{} + 类型断言(运行时才报错)。Go 1.18(2022 年)引入泛型后,标准库新增了泛型版的 slices、maps 包,社区 API 也在全面升级。看懂并写出 [T any] 已是 Go 工程师的必备技能,也是近年 Go 面试的新增考点。

泛型的定位要摆正:它是消除重复代码的工具,不是「升级版继承」。这一课教你写出正确、克制、符合 Go 审美的泛型代码。

先建立图像

泛型 = 印章模板。函数逻辑是刻好的版,类型参数决定印在哪种纸上:

          ┌───────────── 泛型函数 Max[T] ─────────────┐
          │  if a > b { return a }  (逻辑只写一遍)    │
          └───────────────────────────────────────────┘
                 │           │            │
调用: Max(3, 5)  ▼   Max(1.5, 2.0) ▼   Max("a","b") ▼
实例化: Max[int]      Max[float64]      Max[string]
结果:   5             2.0               "b"

对比 interface{} 旧方案:装箱成 any → 运行时断言 → 类型错了运行时才炸
泛型方案:编译期检查 + 无装箱 + 调用方零断言

跟着做

步骤 1:先看没有泛型的痛

package main

import "fmt"

func MaxInt(a, b int) int {
	if a > b {
		return a
	}
	return b
}

func MaxFloat(a, b float64) float64 {
	if a > b {
		return a
	}
	return b
}

// any 版本:逻辑重复没了,但丢掉了类型安全
func MaxAny(a, b any) (any, error) {
	switch a.(type) {
	case int:
		if a.(int) > b.(int) {
			return a, nil
		}
		return b, nil
	default:
		return nil, fmt.Errorf("不支持的类型")
	}
}

func main() {
	fmt.Println(MaxInt(3, 5), MaxFloat(1.5, 2.5))
	v, _ := MaxAny(3, 5)
	n := v.(int) // 调用方被迫断言,忘了断言就是运行时 panic
	fmt.Println(n)
}

两份重复代码 + 一个丢类型安全的版本,这就是泛型要解决的世界。

步骤 2:第一个泛型函数

package main

import (
	"cmp"
	"fmt"
)

// [T cmp.Ordered] 声明类型参数 T,约束为「可排序类型」
// cmp.Ordered 是标准库约束(Go 1.21+),涵盖所有整数、浮点、字符串
func Max[T cmp.Ordered](a, b T) T {
	if a > b {
		return a
	}
	return b
}

func main() {
	fmt.Println(Max(3, 5))        // 类型推断:T = int,无需写 [int]
	fmt.Println(Max(1.5, 2.5))    // T = float64
	fmt.Println(Max("a", "b"))    // T = string
	fmt.Println(Max[int](3, 5))   // 显式指定也可以(推断不了时必须写)
}

语法拆解:func Max[T cmp.Ordered](a, b T) T —— 方括号里是「类型参数列表」,T 是形参名(惯用 T、K、V),cmp.Ordered 是约束(「T 必须长什么样」)。

步骤 3:自定义约束——类型集语法与 ~

约束就是接口,但内容从「方法集」扩展成了「类型集」:

package main

import "fmt"

// Number 是一个类型集:int、int64、float64 及其「底层类型」都满足
type Number interface {
	~int | ~int64 | ~float64
}

// ~int 读作「底层类型是 int」:type MyInt int 声明的 MyInt 也满足,
// 没有波浪号时 MyInt 就不满足(它不是字面上的 int)
type MyInt int

func Sum[T Number](nums []T) T {
	var total T // 泛型里的零值声明套路:var x T
	for _, n := range nums {
		total += n
	}
	return total
}

func main() {
	fmt.Println(Sum([]int{1, 2, 3}))        // 6
	fmt.Println(Sum([]float64{1.5, 2.5}))   // 4
	fmt.Println(Sum([]MyInt{10, 20}))       // 30,靠 ~int 放行

	// comparable:内置约束,= 支持 == 和 != 的类型(可当 map key)
	fmt.Println(Equal(1, 1), Equal("a", "a"))
}

func Equal[T comparable](a, b T) bool {
	return a == b
}

记住三个内置/常用约束:any(任意类型)、comparable(可比较,map key 的最低要求)、cmp.Ordered(可排序)。

步骤 4:多类型参数——Map 与 Filter

package main

import "fmt"

// 两个类型参数:输入 T,输出 R,各自独立推断
func Map[T, R any](in []T, f func(T) R) []R {
	out := make([]R, 0, len(in))
	for _, v := range in {
		out = append(out, f(v))
	}
	return out
}

func Filter[T any](in []T, keep func(T) bool) []T {
	var out []T
	for _, v := range in {
		if keep(v) {
			out = append(out, v)
		}
	}
	return out
}

func main() {
	nums := []int{1, 2, 3, 4, 5}

	strs := Map(nums, func(n int) string { // T=int, R=string 自动推断
		return fmt.Sprintf("#%d", n)
	})
	evens := Filter(nums, func(n int) bool { return n%2 == 0 })

	fmt.Println(strs)  // [#1 #2 #3 #4 #5]
	fmt.Println(evens) // [2 4]
}

步骤 5:泛型结构体 Stack[T]

package main

import "fmt"

type Stack[T any] struct {
	items []T
}

// 方法上不能再声明新的类型参数,直接复用接收者的 T
func (s *Stack[T]) Push(v T) {
	s.items = append(s.items, v)
}

func (s *Stack[T]) Pop() (T, bool) {
	var zero T // 泛型零值惯例
	if len(s.items) == 0 {
		return zero, false
	}
	v := s.items[len(s.items)-1]
	s.items = s.items[:len(s.items)-1]
	return v, true
}

func (s *Stack[T]) Len() int { return len(s.items) }

func main() {
	s := &Stack[string]{}
	s.Push("a")
	s.Push("b")
	fmt.Println(s.Len()) // 2

	v, ok := s.Pop()
	fmt.Println(v, ok) // b true

	// 类型错误在编译期暴露,不是运行时
	// s.Push(123) // 编译错误:cannot use 123 as string
}

步骤 6:标准库泛型包速览

package main

import (
	"fmt"
	"maps"
	"slices"
)

func main() {
	nums := []int{3, 1, 4, 1, 5}

	slices.Sort(nums)              // 原地排序
	fmt.Println(slices.Max(nums))  // 5
	fmt.Println(slices.Index(nums, 4)) // 1
	fmt.Println(slices.Contains(nums, 9)) // false

	ages := map[string]int{"a": 1, "b": 2}
	keys := slices.Sorted(maps.Keys(ages)) // 取 key 并排序(Go 1.23 迭代器风格)
	fmt.Println(keys) // [a b]
}

golang.org/x/exp/constraints 是标准库约束落地前的过渡包,新代码直接用 cmp、slices、maps。

讲清楚要点

  1. 类型参数声明在方括号里:func F[T C](a T)、type S[T C] struct{...};调用时编译器按实参推断 T,推断不出才显式写 F[int](...)。
  2. 约束 = 类型的类型:既是「白名单类型集」(~int | ~float64),也可以是方法要求(和普通接口一样可以混写方法与类型集)。
  3. ~ 表示底层类型:~int 放行 type MyInt int;不写 ~ 则只放行字面 int。
  4. 方法不能新增类型参数:func (s *Stack[T]) Push(v T) 合法;func (s *Stack[T]) Map[U any](...) 编译错误——这是 Go 泛型与 C++/Java 的重要差异。
  5. 泛型是编译期检查、无装箱开销(Go 实现按「GC 形状」共享代码,指针类类型共享一份实例化,值类型各自生成);any + 断言则是运行时检查 + 装箱。
  6. 克制原则:只有当「逻辑相同、类型不同、逻辑里真的要对 T 做运算/比较」时才上泛型;多态需求优先接口。

你可能会懵的地方

  • 泛型和接口不都是「多态」吗?怎么选? 接口是「运行时多态」:传进来什么实现都行,按方法集派发,适合「同一行为多种实现」;泛型是「编译期多态」:同一逻辑适配多种数据类型,需要运算符语义(比较、加减)时接口表达不了,只能泛型。经验法则:行为多变用接口,类型多变用泛型。

  • 为什么约束用接口语法?它和普通接口一样吗? Go 团队复用 interface 关键字降低学习成本,但带类型集(|、~)的接口只能当约束用,不能作为变量类型:var x Number 编译错误。

  • comparable 为什么单独存在? map 的 key、slice 的相等查找都依赖 ==。comparable 约束精确表达「这个类型可以当 key」,编译期就挡住拿切片当 key 的代码。

  • 泛型会让编译变慢、二进制变大吗? Go 采用 GC shape stenciling(按内存形状共享实例化),膨胀远小于 C++ 模板;普通项目无感知,不必为此回避泛型。

  • 什么时候不该用泛型? 只有一种类型却强行泛型化;用泛型模拟继承/方法重载;为了「以后可能支持别的类型」提前抽象。Go 的审美是「等第二个真实用例出现再泛型化」。

错法对照

错法问题正解
var x Number(类型集约束)类型集接口不能当变量类型只在 [T Number] 位置使用
func (s *Stack[T]) Convert[U any](...)方法不支持新类型参数提成包级函数 func Convert[T,U any](...)
约束写 int 忘了 ~intMyInt 等自定义类型被拒~int 表达「底层类型」
用泛型硬造继承体系Go 没有继承,抽象错位组合 + 接口
约束里写了方法又拿 T 当 map key方法不影响比较性需要 key 用 comparable,方法约束分开表达
到处 [T any] 却只调用一种类型过度设计,增加阅读成本具体类型直达,出现第二用例再泛型化

卡点急救

  1. constraint not satisfied:实参类型不在约束的类型集里。检查 ~ 是否遗漏、类型集是否要扩充。
  2. cannot use ... as ... value in argument to f:推断出的 T 与另一参数类型不一致,显式写 F[int](...) 统一。
  3. invalid operation: a + b (T is not ...):T 的约束没给运算能力。加运算所需的类型集(数值)或改用函数参数注入行为。
  4. 方法报新类型参数错误:按「讲清楚要点」第 4 条改成包级泛型函数。

面试常问

  • Go 泛型与 Java/C++ 泛型的区别? Java 是类型擦除,运行时无类型信息,基本类型需装箱;C++ 模板对每个类型完整生成代码,编译慢、体积大;Go 按 GC 形状共享实例化(字典 + 部分生成),兼顾编译速度与体积,且约束用接口语法显式声明。

  • comparable 的意义? 表达「支持 ==/!=」,是 map key 的最低要求;没有它,泛型代码里无法对 T 做相等比较,编译器会拒绝。

  • ~int 是什么意思? 允许「底层类型为 int」的所有命名类型(如 type Age int)。类型集里写不带 ~ 的 int 只匹配字面 int。

  • Go 的方法为什么不支持类型参数? 设计权衡:接口的方法集派发与泛型方法组合会导致实现复杂化,官方明确裁剪。解决方式是把该操作提成包级泛型函数。

自测三问

  1. 写一个泛型函数 Min[T cmp.Ordered],并说明调用 Min(2.5, 1.5) 时 T 被推断成什么。
  2. type Celsius float64 能满足约束 ~float64 吗?能满足 float64 吗?为什么?
  3. 泛型 Stack 的 Pop 为什么返回 (T, bool) 而不是只返回 T?

小结一句话

泛型 = 方括号里的类型参数 + 接口语法的约束(类型集/~ 波浪号/comparable),用来消除同逻辑多类型的重复代码;行为多态交给接口,泛型保持克制。

延伸

  • 官方教程:Getting started with generics
  • Go Blog: An Introduction To Generics
  • 下一课:I/O 与 CLI——用 flag、bufio、encoding/json 写出第一个实用命令行工具。
加载本节练习…
← 上一课错误处理下一课 →I/O 与 CLI