Go 泛型入门:从零开始理解类型参数

Go 泛型入门:从零开始理解类型参数

引言

Go 1.18 引入了泛型,这是 Go 语言诞生以来最大的语法变化之一。对于初学者来说,泛型听起来可能有点抽象,但它的核心思想很简单:写一份代码,能安全地处理多种类型

在没有泛型之前,我们通常用 interface{} 来模拟“任意类型”,但这样会丢失类型信息,运行时才能发现错误。泛型让编译器在编译期就能检查类型,代码也更清晰。

本文将从最简单的例子出发,逐步介绍类型参数、类型约束、泛型函数和泛型类型,最后给出一个实用的泛型切片操作示例。你只需要有 Go 的基本语法知识(变量、函数、结构体、切片)就能跟上。

为什么需要泛型?

假设我们要写一个函数,返回两个整数中的较小值:

func MinInt(a, b int) int {
    if a < b {
        return a
    }
    return b
}

如果需要支持 float64string,就得再写两个几乎一样的函数。代码重复且难以维护。

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) 会报错,因为 3int5.0float64,编译器无法确定 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
}

~ 表示底层类型是 intfloat64 的所有类型,比如 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
    }
}

anyinterface{} 的别名,表示任意类型。当需要零值时,用 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 有两个类型参数 TU,分别表示输入和输出类型。

总结

泛型让 Go 代码在保持类型安全的同时,减少了重复。关键点:

  • 函数名后的 [T constraint] 声明类型参数。
  • 类型约束可以是具体类型、联合类型(|)或接口。
  • comparable 用于可比较类型,any 表示任意类型。
  • 泛型类型可以有自己的方法。
  • 类型推断让调用更简洁,必要时可显式指定。

初学者可以先从 MinContains 这类小函数练手,再逐步用到自己的项目中。泛型不是万能的,简单场景用具体类型可能更直观,但在容器、算法和工具函数中,它能显著提升代码复用性。

posted @ 2026-09-10 20:05  辄夜花泽  阅读(7)  评论(0)    收藏  举报
辄夜花泽