Skip to content

泛型 ​

泛型简介 ​

在Go语言中,泛型是一种允许开发者在函数或类型中定义与特定类型无关的通用逻辑的机制。泛型的目标是提供更通用的代码复用,减少冗余代码,提高代码的可读性和可维护性。

泛型的主要用途是实现一种可以处理多种数据类型的函数或类型,而无需为每种类型编写重复的代码。通过使用泛型,开发者可以编写更加灵活和通用的代码,以处理不同类型的数据。泛型还可以用于创建可以适应不同数据类型的通用数据结构或算法。

在Go语言中,泛型是通过类型参数化的方式实现的。通过在函数或类型名后使用尖括号<T>来表示类型参数,开发者可以定义与特定类型无关的通用逻辑。在泛型函数或类型的定义中,可以使用类型参数来代表任意类型,从而使得代码更加通用。

泛型在Go语言中的引入是为了解决传统面向对象编程语言中的一些问题。传统面向对象编程语言中,为了实现通用的功能,开发者需要编写大量的模板代码或使用宏等机制来模拟泛型的功能。然而,这些方法都有各自的缺点,如代码冗余、可读性差、难以维护等。引入泛型后,这些问题得到了有效的解决,使得开发者可以更加轻松地编写通用代码。

总之,泛型是一种允许开发者在函数或类型中定义与特定类型无关的通用逻辑的机制。它的主要用途是提高代码的复用性、可读性和可维护性,使得开发者可以更加轻松地编写通用代码。在Go语言中,泛型通过类型参数化的方式实现,使得开发者可以更加灵活地编写通用代码。

泛型语法 ​

泛型语法是Go语言中一项功能强大的特性,它允许开发者在函数或类型中使用任意类型作为参数,而无需为每种类型编写重复的代码。泛型语法通过类型参数化的方式实现,使得代码更具可复用性和可读性。

泛型语法的结构 ​

在Go语言中,泛型语法由类型参数、泛型函数和泛型类型三部分组成。

  1. 类型参数:类型参数是用于表示任意类型的占位符。它由一个名称(类型参数名)和一个类型(实际类型)组成。类型参数在函数或类型的定义中使用,并且可以根据需要进行命名。
  2. 泛型函数:泛型函数是普通的函数,只是在其定义中使用了类型参数。泛型函数可以接受任意类型的参数,并在函数内部使用类型参数来代表这些参数的类型。在函数内部,可以使用类型参数来表示函数的参数、返回值和局部变量的类型。
  3. 泛型类型:泛型类型是通过结构体或接口的定义来引入泛型特性。泛型类型可以包含任意类型的字段或属性,并且可以根据需要进行命名。在创建泛型类型的实例时,可以使用具体的类型来替换类型参数。

泛型函数 ​

下面是一个使用泛型语法的示例:

go
package main

import (
 "fmt"
)

// 泛型函数
func GenericFunction[T any](arg1 T, arg2 int) (T, error) {
 // 函数实现,使用 T 表示任意类型
 fmt.Println("arg1:", arg1)
 fmt.Println("arg2:", arg2)
 return arg1, nil
}

func main() {
 // 使用 int 类型调用泛型函数
 result, err := GenericFunction[int](42, 10)
 if err != nil {
 fmt.Println("Error:", err)
 return
 }
 fmt.Println("Result:", result)

 // 使用 string 类型调用泛型函数
 result, err = GenericFunction[string]("Hello", 10)
 if err != nil {
 fmt.Println("Error:", err)
 return
 }
 fmt.Println("Result:", result)
}

在上面的示例中,GenericFunction是一个泛型函数,使用了类型参数T来表示任意类型。T被限制为any类型,这意味着可以接受任何类型的参数。在函数实现中,可以使用类型参数T来表示参数、返回值和局部变量的类型。在main函数中,分别使用int和string类型来调用泛型函数,并打印结果。

泛型类型 ​

类型参数除了用于泛型函数之外,还可以用于Go的类型定义,来实现泛型类型(generic types)。

看如下代码示例,实现了一个泛型二叉树结构

go
type Tree[T interface{}] struct {
	left, right *Tree[T]
	data T
}

func (t *Tree[T]) Lookup(x T) *Tree[T] 

var stringTree Tree[string]

二叉树节点存储的数据类型可能是多样的,有的二叉树存储int,有的存储string等等。

使用泛型,可以让Tree这个结构体类型支持二叉树节点存储不同的数据类型。

对于泛型类型的方法,需要在方法接收者声明对应的类型参数。比如上例里的Lookup方法,在指针接收者*Tree[T]里声明了类型参数T。

泛型约束 ​

go
type Number interface {
    int64 | float64
}

在以上代码中,声明用作类型约束的 Number 接口类型。在接口内声明 int64 和 float64 的并集。本质上,您正在将联合从函数声明移动到新的类型约束中。这样,当您想要将类型参数约束为 int64 或 float64 时,您可以使用此 Number 类型约束,而不是写出 int64 | float64

go
// SumNumbers sums the values of map m. It supports both integers
// and floats as map values.
func SumNumbers[K comparable, V Number](m map[K]V) V {
    var s V
    for _, v := range m {
        s += v
    }
    return s
}

在以上代码中,声明一个泛型函数,其逻辑与之前声明的泛型函数相同,但使用新的接口类型而不是联合作为类型约束。和以前一样,您使用类型参数作为参数和返回类型。

go
// 调用SumNumbers
ints := map[int]int64{1: 1, 2: 2}
floats := map[int]float64{1: 1.1, 2: 2.2}
SumNumbers(ints),
SumNumbers(floats))

对每个映射调用 SumNumbers ,打印每个映射值的总和。您可以在调用泛型函数时省略类型参数(方括号中的类型名称)。 Go 编译器可以从其他参数推断类型参数。

泛型限制 ​

类型参数列表不能用于方法,只能用于函数。

go
type Foo struct {}

func (Foo) bar[T any](t T) {}

上面的例子在结构体类型Foo的方法bar使用了类型参数列表,编译会报错:methods cannot have type parameters

可以使用类型参数T如下:

go
type Foo[T any] struct {}

func (Foo[T]) bar(t T) {}

注意:类型限制必须是interface类型。

类型限制有2个作用:

  1. 用于约定有效的类型实参,不满足类型限制的类型实参会被编译器报错。
  2. 如果类型限制里的所有类型都支持某个操作,那在代码里,对应的类型参数就可以使用这个操作。

| 和 ~ ​

|: 表示取并集。比如下例的Number这个interface可以作为类型限制,用于限定类型参数必须是int,int32和int64这3种类型。

go
type Number interface{
	int | int32 | int64
}

~T: ~ 是Go 1.18新增的符号,~T表示底层类型是T的所有类型。~的英文读作tilde。

  • 例1:比如下例的AnyString这个interface可以作为类型限制,用于限定类型参数的底层类型必须是string。string本身以及下面的MyString都满足AnyString这个类型限制。

    go
    type AnyString interface{
       ~string
    }
    type MyString string
  • 例2:再比如,我们定义一个新的类型限制叫customConstraint,用于限定底层类型为int并且实现了String() string方法的所有类型。下面的customInt就满足这个type constraint。

    go
    type customConstraint interface {
       ~int
       String() string
    }
    
    type customInt int
    
    func (i customInt) String() string {
       return strconv.Itoa(int(i))
    }

几个注意点:

  • 可以直接在方括号[]里,直接定义类型限制,即使用类型限制字面值,比如上例。

  • 在类型限制的位置,interface{E}也可以直接写为E,因此就可以理解interface{~[]E}可以写为~[]E。

  • any是Go 1.18新增的预声明标识符,是interface{}的别名。

    go
    [S interface{~[]E}, E interface{}]
    
    [S ~[]E, E interface{}]
    
    [S ~[]E, E any]

类型推导 ​

我们看下面的代码示例:

go
func min[T constraints.Ordered] (x, y T) T {
	if x < y {
		return x
	}
	return y
}

var a, b, m1, m2 float64
// 方式1:显式指定type argument
m1 = min[float64](a, b)
// 方式2:不指定type argument,让编译器自行推导
m2 = min(a, b)

方式2没有传递类型实参,编译器是根据函数实参a和b推导出类型实参。

类型推导可以让我们的代码更简洁,更具可读性。

Go泛型有2种类型推导:

  1. 通过函数的实参推导出来具体的类型。比如上面例子里的m2 = min(a, b),就是根据a和b这2个函数实参

    推导出来T是float64。

  2. 通过已经确定的类型实参,推导出未知的类型实参。下面的代码示例里,根据函数实参2不能确定E是什么类型,但是可以确定S是[]int32,再结合类型限制里S的底层类型是[]E,可以推导出E是int32,int32满足constraints.Integer限制,因此推导成功。

    go
    type Point []int32
    
    func ScaleAndPrint(p Point) {
      r := Scale(p, 2)
      fmt.Println(r)
    }
    
    func Scale[S ~[]E, E constraints.Integer](s S, c E) S {
      r := make(S, len(s))
      for i, v := range s {
        r[i] = v * c
      }
      return r
    }

类型推导并不是一定成功,比如类型参数用在函数的返回值或者函数体内,这种情况就必须指定类型实参了。

go
func test[T any] () T {
  var result T
  return result
}
func test[T any] () {
  var result T
  fmt.Println(result)
}

更深入了解type inference可以参考:https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#type-inference

泛型代码示例 ​

Stack 栈 ​

示例来源https://coolshell.cn/articles/21615.html

编程支持泛型最大的优势就是可以实现类型无关的数据结构了。下面,我们用Slices这个结构体来实现一个Stack的数结构。

首先,我们可以定义一个泛型的Stack

go
type stack [T any] []T

看上去很简单,还是 [T any] ,然后 []T 就是一个数组,接下来就是实现这个数据结构的各种方法了。下面的代码实现了 Push() ,Top(),Top(),Len(),Print()这几个方法,这几个方法和 C++的STL中的 Stack很类似。

go
func (s *Stack[T]) Push(elem T) {
	*s = append(*s, elem)
}
func (s *Stack[T]) Pop() {
	if len(*s) > 0 {
		*s = (*s)[:len(*s)-1]
	}
}
func (s *Stack[T]) Top() *T {
	if len(*s) > 0 {
		return &(*s)[len(*s)-1]
	}
	return nil
}
func (s *Stack[T]) Len() int {
	return len(*s)
}
func (s *Stack[T]) Print() {
	for _, elem := range *s {
		fmt.Print(elem)
		fmt.Print(" ")
	}
	fmt.Println("")
}

上面的这个例子还是比较简单的,不过在实现的过程中,对于一个如果栈为空,那么 Top()要么返回error要么返回空值,在这个地方卡了一下。因为,之前,我们返回的“空”值,要么是 int 的0,要么是 string 的 “”,然而在泛型的T下,这个值就不容易搞了。也就是说,除了类型泛型后,还需要有一些“值的泛型”(注:在C++中,如果你要用一个空栈进行 Top() 操作,你会得到一个 segmentation fault),所以,这里我们返回的是一个指针,这样可以判断一下指针是否为空。

下面是如何使用这个stack的代码。

go
func main() {
	ss := Stack[string]{}
	ss.Push("Hello")
	ss.Push("Hao")
	ss.Push("Chen")
	ss.Print()
	fmt.Printf("stack top is - %v\n", *(ss.Top()))
	ss.Pop()
	ss.Pop()
	ss.Print()

	ns := Stack[int]{}
	ns.Push(10)
	ns.Push(20)
	ns.Print()
	ns.Pop()
	ns.Print()
	*ns.Top() += 1
	ns.Print()
	ns.Pop()
	fmt.Printf("stack top is - %v\n", ns.Top())
}

示例来源https://coolshell.cn/articles/21615.html

下面我们再来看一个双向链表的实现。下面这个实现中实现了 这几个方法:

  • add() – 从头插入一个数据结点
  • push() – 从尾插入一个数据结点
  • del() – 删除一个结点(因为需要比较,所以使用了 compareable 的泛型)
  • print() – 从头遍历一个链表,并输出值。
go
type node[T comparable] struct {
  data T
  prev *node[T]
  next *node[T]
}

type list[T comparable] struct {
  head, tail *node[T]
  len int
}

func (l *list[T]) isEmpty() bool {
  return l.head == nil && l.tail == nil
}

func (l *list[T]) add(data T) {
  n := &node[T] {
    data : data,
    prev : nil,
    next : l.head,
  }
  if l.isEmpty() {
    l.head = n
    l.tail = n
  }
  l.head.prev = n
  l.head = n
}

func (l *list[T]) push(data T) { 
  n := &node[T] {
    data : data,
    prev : l.tail,
    next : nil,
  }
  if l.isEmpty() {
    l.head = n
    l.tail = n
  }
  l.tail.next = n
  l.tail = n
}

func (l *list[T]) del(data T) { 
  for p := l.head; p != nil; p = p.next {
    if data == p.data {
      
      if p == l.head {
        l.head = p.next
      }
      if p == l.tail {
        l.tail = p.prev
      }
      if p.prev != nil {
        p.prev.next = p.next
      }
      if p.next != nil {
        p.next.prev = p.prev
      }
      return 
    }
  } 
}

func (l *list[T]) print() {
  if l.isEmpty() {
    fmt.Println("the link list is empty.")
    return 
  }
  for p := l.head; p != nil; p = p.next {
    fmt.Printf("[%v] -> ", p.data)
  }
  fmt.Println("nil")
}

上面这个代码都是一些比较常规的链表操作,学过链表数据结构的同学应该都不陌生,使用的代码也不难,如下所示,都很简单,看代码就好了。

go
func main(){
  var l = list[int]{}
  l.add(1)
  l.add(2)
  l.push(3)
  l.push(4)
  l.add(5)
  l.print() //[5] -> [2] -> [1] -> [3] -> [4] -> nil
  l.del(5)
  l.del(1)
  l.del(4)
  l.print() //[2] -> [3] -> nil
  
}

函数式范型 ​

示例来源https://coolshell.cn/articles/21615.html

接下来,我们就要来看一下我们函数式编程的三大件 map() 、 reduce() 和 filter() 。

泛型Map ​

go
func gMap[T1 any, T2 any] (arr []T1, f func(T1) T2) []T2 {
  result := make([]T2, len(arr))
  for i, elem := range arr {
    result[i] = f(elem)
  }
  return result
}

在上面的这个 map函数中我使用了两个类型 – T1 和 T2 ,

  • T1 – 是需要处理数据的类型
  • T2 – 是处理后的数据类型

T1 和 T2 可以一样,也可以不一样。

我们还有一个函数参数 – func(T1) T2 意味着,进入的是 T1 类型的,出来的是 T2 类型的。

然后,整个函数返回的是一个 []T2

好的,我们来看一下怎么使用这个map函数:

go
nums := []int {0,1,2,3,4,5,6,7,8,9}
squares := gMap(nums, func (elem int) int {
  return elem * elem
})
print(squares)  //0 1 4 9 16 25 36 49 64 81 

strs := []string{"Hao", "Chen", "MegaEase"}
upstrs := gMap(strs, func(s string) string  {
  return strings.ToUpper(s)
})
print(upstrs) // HAO CHEN MEGAEASE 


dict := []string{"零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖"}
strs =  gMap(nums, func (elem int) string  {
  return  dict[elem]
})
print(strs) // 零 壹 贰 叁 肆 伍 陆 柒 捌 玖

泛型 Reduce ​

接下来,我们再来看一下我们的Reduce函数,reduce函数是把一堆数据合成一个。

go
func gReduce[T1 any, T2 any] (arr []T1, init T2, f func(T2, T1) T2) T2 {
  result := init
  for _, elem := range arr {
    result = f(result, elem)
  }
  return result
}

函数实现起来很简单,但是感觉不是很优雅。

  • 也是有两个类型 T1 和 T2,前者是输出数据的类型,后者是佃出数据的类型。
  • 因为要合成一个数据,所以需要有这个数据的初始值 init,是 T2 类型
  • 而自定义函数 func(T2, T1) T2,会把这个init值传给用户,然后用户处理完后再返回出来。

下面是一个使用上的示例——求一个数组的和

go
nums := []int {0,1,2,3,4,5,6,7,8,9}
sum := gReduce(nums, 0, func (result, elem int) int  {
    return result + elem
})
fmt.Printf("Sum = %d \n", sum)

泛型 filter ​

filter函数主要是用来做过滤的,把数据中一些符合条件(filter in)或是不符合条件(filter out)的数据过滤出来,下面是相关的代码示例

go
func gFilter[T any] (arr []T, in bool, f func(T) bool) []T {
  result := []T{}
  for _, elem := range arr {
    choose := f(elem)
    if (in && choose) || (!in && !choose) {
      result = append(result, elem)
    }
  }
  return result
}

func gFilterIn[T any] (arr []T, f func(T) bool) []T {
  return gFilter(arr, true, f)
}

func gFilterOut[T any] (arr []T, f func(T) bool) []T {
  return gFilter(arr, false, f)
}

其中,用户需要提从一个 bool 的函数,我们会把数据传给用户,然后用户只需要告诉我行还是不行,于是我们就会返回一个过滤好的数组给用户。

比如,我们想把数组中所有的奇数过滤出来

go
nums := []int {0,1,2,3,4,5,6,7,8,9}
odds := gFilterIn(nums, func (elem int) bool  {
    return elem % 2 == 1
})
print(odds)

业务示例 ​

示例来源https://coolshell.cn/articles/21615.html

首先,我们先声明一个员工对象和相关的数据

go
type Employee struct {
  Name     string
  Age      int
  Vacation int
  Salary   float32
}

var employees = []Employee{
  {"Hao", 44, 0, 8000.5},
  {"Bob", 34, 10, 5000.5},
  {"Alice", 23, 5, 9000.0},
  {"Jack", 26, 0, 4000.0},
  {"Tom", 48, 9, 7500.75},
  {"Marry", 29, 0, 6000.0},
  {"Mike", 32, 8, 4000.3},
}

然后,我们想统一下所有员工的薪水,我们就可以使用前面的reduce函数

go
total_pay := gReduce(employees, 0.0, func(result float32, e Employee) float32 {
  return result + e.Salary
})
fmt.Printf("Total Salary: %0.2f\n", total_pay) // Total Salary: 43502.05

我们函数这个 gReduce 函数有点啰嗦,还需要传一个初始值,在用户自己的函数中,还要关心 result 我们还是来定义一个更好的版本。

一般来说,我们用 reduce 函数大多时候基本上是统计求和或是数个数,所以,是不是我们可以定义的更为直接一些?比如下面的这个 CountIf(),就比上面的 Reduce 干净了很多。

go
func gCountIf[T any](arr []T, f func(T) bool) int {
  cnt := 0
  for _, elem := range arr {
    if f(elem) {
      cnt += 1
    }
  }
  return cnt;
}

我们做求和,我们也可以写一个Sum的泛型。

  • 处理 T 类型的数据,返回 U类型的结果
  • 然后,用户只需要给我一个需要统计的 T 的 U 类型的数据就可以了。

代码如下所示:

go
type Sumable interface {
  type int, int8, int16, int32, int64,
        uint, uint8, uint16, uint32, uint64,
        float32, float64
}
func gSum[T any, U Sumable](arr []T, f func(T) U) U {
  var sum U
  for _, elem := range arr {
    sum += f(elem)
  }
  return sum
}

上面的代码我们动用了一个叫 Sumable 的接口,其限定了 U 类型,只能是 Sumable里的那些类型,也就是整型或浮点型,这个支持可以让我们的泛型代码更健壮一些。

于是,我们就可以完成下面的事了。

1)统计年龄大于40岁的员工数

go
old := gCountIf(employees, func (e Employee) bool  {
    return e.Age > 40
})
fmt.Printf("old people(>40): %d\n", old) 
// ld people(>40): 2

2)统计薪水超过 6000元的员工数

go
high_pay := gCountIf(employees, func(e Employee) bool {
  return e.Salary >= 6000
})
fmt.Printf("High Salary people(>6k): %d\n", high_pay) 
//High Salary people(>6k): 4

3)统计年龄小于30岁的员工的薪水

go
younger_pay := gSum(employees, func(e Employee) float32 {
  if e.Age < 30 {
      return e.Salary
  } 
  return 0
})
fmt.Printf("Total Salary of Young People: %0.2f\n", younger_pay)
//Total Salary of Young People: 19000.00

4)统计全员的休假天数

go
total_vacation := gSum(employees, func(e Employee) int {
  return e.Vacation
})
fmt.Printf("Total Vacation: %d day(s)\n", total_vacation)
//Total Vacation: 32 day(s)

5)把没有休假的员工过滤出来

go
no_vacation := gFilterIn(employees, func(e Employee) bool {
  return e.Vacation == 0
})
print(no_vacation)
//{Hao 44 0 8000.5} {Jack 26 0 4000} {Marry 29 0 6000}

封装通用List2Tree ​

Golang 中如何使用泛型封装列表转树的函数?

一、封装 ​

go
// 定义一个接口类型Tree
type Tree[T any] interface {
    GetParentId(t *T) any  // 获取上级id 
    GetRootId() any  // 获取定好的根级id值
    SetChild(parent *T, children []*T) []*T  // 设置子级分类的方法
    GetId(t *T) any  // 获取id
}  
// 用于列表转树的方法
func List2Tree[T any](list []*T, f Tree[T]) (tree []*T) {  
    treeMap := make(map[any][]*T)  
    for i := range list {  
       v := list[i]  
       treeMap[f.GetParentId(v)] = append(treeMap[f.GetParentId(v)], v)  
    }  
  
    tree = treeMap[f.GetRootId()]  
    getChildren(treeMap, tree, f)  
  
    return  
}  
// 递归设置子孙级
func getChildren[T any](treeMap map[any][]*T, root []*T, f Tree[T]) {  
    for i := range root {  
       v := root[i]  
       getChildren(treeMap, f.SetChild(v, treeMap[f.GetId(v)]), f)  
    }  
}

二、使用 ​

go
// 定义一个存在上下级关系的部门结构体
type Department struct {
	ID int 
	Name     string                          
	Code     string   
	ParentId int    // 上级id
	Children []*Department
}

// 实现Tree[T any]接口 所有方法
func (d *Department) GetParentId(t *Department) any {
	return t.ParentId
}

func (d *Department) GetRootId() any {
	return 0
}

func (d *Department) SetChild(parent *FindProductClassTree, children []*FindProductClassTree) []*FindProductClassTree {
	parent.Children = children
	return parent.Children
}

func (d *Department) GetId(t *Department) any {
	return t.ID
}


func main() {
	var list []*Department
	// 从数据库获取部门列表----此处省略代码
	tree := List2Tree[Department](list, &Department{})
	fmt.Println(tree)
}

泛型的应用场景 ​

  • 需要使用slice, map, channel类型,但是slice, map, channel里的元素类型可能有多种。

  • 通用的数据结构,比如链表,

  • 如果开发中出现大量重复业务,只是类型不同,建议使用泛型。