Go 语言自 1.18 版本起支持 泛型(Generics),这让 Go 代码可以更加通用,减少重复代码,提高开发效率。Go 1.24 进一步增强了泛型支持,引入了 泛型类型别名,让代码更简洁。

本文将带你从零开始,理解 Go 泛型的概念、语法、使用方式,并配有示例代码,帮助你快速上手。

什么是泛型?

简单来说,泛型 就是让我们编写可以适用于多种数据类型的代码。换句话说,泛型让你可以定义一个函数或数据结构,它可以处理任何类型的数据,而不需要事先指定类型。你只需要在使用时决定具体的类型。

1. 泛型函数

泛型函数是泛型最基本的应用。它允许你编写一个函数,可以接收任何类型的参数并返回相应的结果。

示例:一个求长度的泛型函数

假设你要写一个求切片长度的函数。没有泛型时,你可能需要为不同类型的切片写不同的函数:

func LengthInt(s []int) int {
    return len(s)
}

func LengthString(s []string) int {
    return len(s)
}

但有了泛型,我们就可以写一个通用的函数来处理不同类型的切片:

func Length[T any](s []T) int {
    return len(s)
}

这里的 T 是泛型类型,它表示函数 Length 可以接受任何类型的切片。any 表示“任何类型都行”。这样,你就可以用它来求任意类型切片的长度了:

sliceInt := []int{1, 2, 3, 4, 5}
sliceString := []string{"a", "b", "c"}

fmt.Println(Length(sliceInt))    // 输出: 5
fmt.Println(Length(sliceString)) // 输出: 3

2. 泛型结构体

除了函数,结构体 也可以使用泛型。这意味着你可以写一个通用的数据结构,适应不同类型的数据。

示例:一个通用的盒子

假设你想写一个盒子,它能装任何类型的东西。没有泛型时,你可能要写多个结构体:

type IntBox struct {
    value int
}

type StringBox struct {
    value string
}

但有了泛型,你可以写一个通用的结构体:

type Box[T any] struct {
    value T
}

现在,T 就表示盒子可以装任何类型的数据。你可以使用它来装 intstring 或其他任何类型的数据:

intBox := Box[int]{value: 10}
fmt.Println(intBox.value) // 输出: 10

stringBox := Box[string]{value: "hello"}
fmt.Println(stringBox.value) // 输出: hello

3. 泛型集合(Set)

泛型还可以用于集合类型,像 Set(集合)就是一个常见的应用。通过泛型,我们可以创建一个可以存放任何类型的集合。

示例:一个通用的集合

没有泛型时,你需要为每个数据类型写不同的集合实现:

type IntSet struct {
    data map[int]struct{}
}

type StringSet struct {
    data map[string]struct{}
}

但通过泛型,我们可以定义一个通用的集合结构:

type Set[T comparable] struct {
    data map[T]struct{}
}

这里的 T 表示集合中可以存放的元素类型,而 comparable 是 Go 中的一个限制,它表示这个类型的元素可以进行比较(比如 intstring 都可以比较)。然后,你可以使用这个集合结构来存储任何类型的数据:

intSet := Set[int]{data: make(map[int]struct{})}
intSet.data[1] = struct{}{}
fmt.Println(intSet.data) // 输出: map[1:{}]

stringSet := Set[string]{data: make(map[string]struct{})}
stringSet.data["apple"] = struct{}{}
fmt.Println(stringSet.data) // 输出: map[apple:{}]

4. 泛型通道(Channel)

Go 的通道(chan)也可以与泛型结合使用,允许你发送和接收任何类型的数据。

示例:一个通用的通道

我们可以定义一个通道,它可以传递任何类型的数据:

type Channel[T any] struct {
    ch chan T
}

func NewChannel[T any](size int) *Channel[T] {
    return &Channel[T]{ch: make(chan T, size)}
}

func (c *Channel[T]) Send(value T) {
    c.ch <- value
}

func (c *Channel[T]) Receive() T {
    return <-c.ch
}

使用时,你可以根据需要指定通道传递的数据类型:

stringChan := NewChanneln.Send("hello")
fmt.Println(stringChan.Receive()) // 输出: hello

5. 泛型方法

除了结构体,方法 也可以是泛型的。我们可以在结构体上定义泛型方法,做一些通用操作。

示例:一个通用的栈

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) {
    if len(s.items) == 0 {
        var zero T
        return zero, false
    }
    item := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return item, true
}

使用时,你可以定义一个 int 类型的栈,或者 string 类型的栈:

intStack := Stack[int]{}
intStack.Push(10)
intStack.Push(20)

value, ok := intStack.Pop()
fmt.Println(value, ok) // 输出: 20 true

6. 泛型迭代器

泛型也可以用于创建通用的迭代器,用来遍历各种集合。

示例:一个通用的迭代器

type Iterator[T any] struct {
    items []T
    index int
}

func NewIterator[T any](items []T) *Iterator[T] {
    return &Iterator[T]{items: items, index: 0}
}

func (it *Iterator[T]) HasNext() bool {
    return it.index < len(it.items)
}

func (it *Iterator[T]) Next() T {
    item := it.items[it.index]
    it.index++
    return item
}

使用时,你可以用它来遍历 int 类型的切片,或者 string 类型的切片:

it := NewIterator([]int{1, 2, 3, 4})
for it.HasNext() {
    fmt.Println(it.Next()) // 输出: 1 2 3 4
}

7. 泛型工厂函数

我们也可以用泛型来创建通用的对象工厂,动态创建不同类型的对象。

示例:一个通用的对象工厂

func NewObject[T any](value T) *T {
    return &value
}

使用时,你可以创建不同类型的对象:

strObj := NewObject("Hello, Go!")
fmt.Println(*strObj) // 输出: Hello, Go!

intObj := NewObject(42)
fmt.Println(*intObj) // 输出: 42

8. Go 1.24 新特性:泛型类型别名

在 Go 1.24 版本中,Go 还引入了 泛型类型别名,让类型别名也可以使用泛型。

示例:定义泛型类型别名

type Vector[T any] []T
type VectorAlias[T any] = Vector[T] // 泛型类型别名

这意味着 VectorAlias[int]Vector[int] 是完全一样的。

相关阅读:Go 1.24 新特性一览

结语

通过泛型,Go 语言能够编写更加灵活、通用的代码。你可以避免为每种类型重复编写相同的代码,同时保持代码的简洁和可维护性。希望这篇文章让你对 Go 语言的泛型有了更清晰的理解,记得通过实际编写一些代码来加深你的理解哦!


也可以看看