Go 泛型入门:从零开始理解类型参数
Go 泛型入门:从零开始理解类型参数
引言
Go 1.18 引入了泛型,这是 Go 语言诞生以来最大的语法变化之一。对于初学者来说,泛型听起来可能有点抽象,但它的核心思想很简单:写一份代码,能安全地处理多种类型。
在没有泛型之前,我们通常用 interface{} 来模拟“任意类型”,但这样会丢失类型信息,运行时才能发现错误。泛型让编译器在编译期就能检查类型,代码也更清晰。
本文将从最简单的例子出发,逐步介绍类型参数、类型约束、泛型函数和泛型类型,最后给出一个实用的泛型切片操作示例。你只需要有 Go 的基本语法知识(变量、函数、结构体、切片)就能跟上。
为什么需要泛型?
假设我们要写一个函数,返回两个整数中的较小值:
func MinInt(a, b int) int {
if a < b {
return a
}
return b
}
如果需要支持 float64 和 string,就得再写两个几乎一样的函数。代码重复且难以维护。
用 interface{} 可以避免重复,但会失去类型安全:
func MinAny(a, b interface{}) interface{} {
// 无法直接比较 a < b,需要类型断言,且可能 panic
return nil
}
泛型就是为了解决这个问题:写一个函数,用类型参数代替具体类型,编译器保证类型安全。
泛型函数:从 Min 开始
基本语法
Go 泛型函数在函数名后加上方括号 [类型参数 约束]。例如:
package main
import "fmt"
func Min[T int | float64 | string](a, b T) T {
if a < b {
return a
}
return b
}
func main() {
fmt.Println(Min(3, 5)) // 输出 3
fmt.Println(Min(3.14, 2.71)) // 输出 2.71
fmt.Println(Min("apple", "banana")) // 输出 apple
}
这里 T 是类型参数,int | float64 | string 是类型约束,表示 T 可以是这三种类型之一。调用时,Go 通常能自动推断 T,所以不需要显式写 Min[int](3, 5)。
类型推断
如果参数类型不同,推断会失败。例如 Min(3, 5.0) 会报错,因为 3 是 int,5.0 是 float64,编译器无法确定 T。这时可以显式指定:
fmt.Println(Min[float64](3, 5.0)) // 输出 3
注意:3 会被转换为 float64。
类型约束:comparable 和自定义接口
上面的约束 int | float64 | string 叫做类型集。但有些类型不能直接用 < 比较,比如结构体。Go 内置了一个约束 comparable,表示可比较的类型(支持 == 和 !=)。
comparable 约束
func Contains[T comparable](s []T, v T) bool {
for _, item := range s {
if item == v {
return true
}
}
return false
}
func main() {
fmt.Println(Contains([]int{1, 2, 3}, 2)) // true
fmt.Println(Contains([]string{"a", "b"}, "c")) // false
}
comparable 在 Go 1.20 之后也可以用于结构体比较,但注意:包含切片、map、函数的类型不可比较,因此不能传给 comparable。
自定义约束接口
我们可以定义自己的约束接口,里面可以包含类型集和方法。例如,一个支持 ~int 或 ~float64 的数值约束:
type Number interface {
~int | ~float64
}
func Sum[T Number](nums []T) T {
var total 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
}
~ 表示底层类型是 int 或 float64 的所有类型,比如 type MyInt int 也满足 ~int。不加 ~ 则只匹配精确类型。
泛型类型:通用切片
泛型也可以用在类型定义上。下面定义一个 Stack 栈:
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(item T) {
s.items = append(s.items, item)
}
func (s *Stack[T]) Pop() (T, bool) {
var zero T
if len(s.items) == 0 {
return zero, false
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item, true
}
func main() {
var s Stack[int]
s.Push(1)
s.Push(2)
if v, ok := s.Pop(); ok {
fmt.Println(v) // 2
}
}
any 是 interface{} 的别名,表示任意类型。当需要零值时,用 var zero T 获取。
实战:泛型 Filter 和 Map
最后,我们实现两个常用的切片操作,它们可以处理任何类型:
func Filter[T any](s []T, f func(T) bool) []T {
var result []T
for _, v := range s {
if f(v) {
result = append(result, v)
}
}
return result
}
func Map[T, U any](s []T, f func(T) U) []U {
result := make([]U, len(s))
for i, v := range s {
result[i] = f(v)
}
return result
}
func main() {
nums := []int{1, 2, 3, 4, 5}
evens := Filter(nums, func(n int) bool { return n%2 == 0 })
fmt.Println(evens) // [2 4]
strs := Map(nums, func(n int) string { return fmt.Sprint(n * 2) })
fmt.Println(strs) // [2 4 6 8 10]
}
注意 Map 有两个类型参数 T 和 U,分别表示输入和输出类型。
总结
泛型让 Go 代码在保持类型安全的同时,减少了重复。关键点:
- 函数名后的
[T constraint]声明类型参数。 - 类型约束可以是具体类型、联合类型(
|)或接口。 comparable用于可比较类型,any表示任意类型。- 泛型类型可以有自己的方法。
- 类型推断让调用更简洁,必要时可显式指定。
初学者可以先从 Min、Contains 这类小函数练手,再逐步用到自己的项目中。泛型不是万能的,简单场景用具体类型可能更直观,但在容器、算法和工具函数中,它能显著提升代码复用性。
本文来自博客园,作者:辄夜花泽,转载请注明原文链接:https://www.cnblogs.com/johnbin/p/22926187

浙公网安备 33010602011771号