首页 / Go 语言入门教程 / 泛型基础

Go 语言入门教程

泛型基础

本教程共 80 篇 · 第 47 篇 · 更新于 2026-07-27 · 约 7 分钟阅读

GoGo 入门教程泛型Generics类型参数Go 1.18

47. 泛型基础

本节目标:搞懂泛型解决什么问题,学会写泛型函数,理解类型参数和类型约束的基本概念。

为什么需要泛型

假设你要写一个「求和」函数。整数求和写一个,浮点数求和再写一个:

func SumInts(s []int) int {
    var total int
    for _, v := range s {
        total += v
    }
    return total
}

func SumFloats(s []float64) float64 {
    var total float64
    for _, v := range s {
        total += v
    }
    return total
}

逻辑完全一样,只是类型不同。如果有十种数字类型,你要写十个函数?

泛型就是解决这个问题的。它让你写一份代码,适用于多种类型。Go 在 1.18 版本(2022 年 3 月)引入了泛型。

泛型函数

用类型参数把上面的两个函数合成一个:

package main

import "fmt"

// [T int | float64] 是类型参数声明
func Sum[T int | float64](s []T) T {
    var total T
    for _, v := range s {
        total += v
    }
    return total
}

func main() {
    ints := []int{1, 2, 3}
    floats := []float64{1.1, 2.2, 3.3}

    fmt.Println(Sum(ints))   // 6
    fmt.Println(Sum(floats)) // 6.6
}

[T int | float64] 里的 T 是类型参数,int | float64 是类型约束,表示 T 可以是 int 或 float64。

三个核心概念

  • 类型参数(Type Parameter)T,一个占位符,代表「某种类型」
  • 类型约束(Type Constraint)int | float64,限定 T 能是哪些类型
  • 类型实参(Type Argument):调用时传入的具体类型

调用方式

显式指定类型

Sum[int]([]int{1, 2, 3})       // 手动指定 T = int
Sum[float64]([]float64{1.1})   // 手动指定 T = float64

类型推断(推荐)

Go 编译器能根据参数自动推断类型:

Sum([]int{1, 2, 3})       // 推断出 T = int
Sum([]float64{1.1, 2.2})  // 推断出 T = float64
Tip

大部分情况下不需要手动写类型实参,让编译器推断就行。只有推断不出来时才显式指定。

any 约束

如果类型参数可以是任何类型,用 any 约束:

func Print[T any](v T) {
    fmt.Println(v)
}

Print(42)
Print("hello")
Print(true)

any 是 Go 1.18 引入的 interface{} 的别名。用 any 做约束表示「不限制类型」。

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 表示「可比较的类型」,包括数字、字符串、布尔、指针、channel 等。切片、map、函数不可比较,不能用 comparable

Note

comparableany 是 Go 预声明的两个最常用约束。any 不限制类型,comparable 要求类型支持 ==!=

一个实用的例子:泛型 Map 函数

用泛型写一个「对切片每个元素做转换」的函数:

package main

import "fmt"

// Map 把 []T 转成 []U
func Map[T any, 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}

    // 把每个整数转成字符串
    strs := Map(nums, func(n int) string {
        return fmt.Sprintf("第%d个", n)
    })
    fmt.Println(strs) // [第1个 第2个 第3个]

    // 把每个整数翻倍
    doubled := Map(nums, func(n int) int {
        return n * 2
    })
    fmt.Println(doubled) // [2 4 6]
}

这个函数有两个类型参数:T 是输入类型,U 是输出类型。没有泛型的话,你要为每种类型组合写一个函数。

泛型 vs 接口

你可能会问:用 any 接口不也能做到吗?

// 用接口的写法
func SumAny(s []any) any {
    var total int
    for _, v := range s {
        total += v.(int) // 运行时类型断言,可能 panic
    }
    return total
}

问题在于:

  1. 类型安全:接口版需要在运行时做类型断言,传错类型会 panic。泛型版在编译期就检查类型,传错类型编译不过
  2. 性能:接口版有装箱拆箱开销,泛型版直接操作具体类型
  3. 可读性:泛型版的返回值是具体类型,不用再断言
// 泛型版:返回 int,直接用
result := Sum([]int{1, 2, 3}) // result 是 int

// 接口版:返回 any,还得断言
result := SumAny([]any{1, 2, 3}) // result 是 any
n := result.(int)                // 还得断言
Tip

能用泛型就用泛型,比接口更安全更高效。但泛型不能替代所有场景:如果逻辑需要根据不同类型做不同操作,还是得用接口。

小结

泛型基础记住这几点:

  • Go 1.18 引入泛型,语法是 func Name[T Constraint](args) returns
  • any 约束不限制类型,comparable 要求可比较
  • 调用时通常靠类型推断,不用手写类型实参
  • 泛型比接口更安全(编译期检查)、更高效(无装箱)

下一章讲类型约束的深入用法:自定义约束、~ 底层类型、union 联合。