Go泛型快速入门 | 青训营笔记

182 阅读5分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第一篇笔记。

写作背景

争议巨大但同时万众期待的泛型终于在Go 1.18发布,它支持并行处理流中的数据。

参考资料: 参考go文档,网络文字和b站视频写下这篇介绍Go1.18泛型的笔记:

本文力求能让未接触泛型编程的人快速上手Go的泛型。

正文

泛型概念和泛型函数

泛型的含义:在定义函数(结构等)时候,可能会有多种类型传入,真正使用方法的时候才可以确定用的是什么类型,此时就可以用一个更加宽泛的类型(存在一定约束,只能在哪些类型的范围内使用)暂时占位,这个类型就叫泛型。

泛型的基本写法:[泛型标识 泛型约束] [T any]

假设我们有个计算两数之和的函数

func Add(a int, b int) int { 
    return a + b 
}

这个函数很简单,但是我想计算浮点类型的数或者字符串相加,哪有什么办法呢?解决办法之一是定义不同类型的函数,如下。

func AddFloat32(a float32, b float32) float32 {
    return a + b 
}

func AddString(a string, b string) string {
    return a + b 
}

这样的写法虽然能实现想要的效果,但是代码冗余和阅读性大大降低,有什么办法解决这个问题呢?

我们通过使用泛型这个概念能优雅地实现这些功能

func Add[T int | float32 | string](a T, b T) T { 
    return a + b 
} 
func main() { 
    c := Add[string]("1", "2") 
    d := Add[int](1,2) 
    fmt.Println(c) 
    fmt.Println(d) 
} 
//"12" //3

声明一个Add函数:

  • Add函数中T是类型形参,在定义Add方法时,T代表的具体类型并不确定,类似一个占位符。

  • int | float32 | string 这部分称为类型约束,中间的|的意思是告诉编译器,类型形参T值可以接受int,float32或string这三种类型。

  • 中括号里的 T int|float32|string 这一整串因为定义了所有的类型形参(在这个例子里只有一个类型形参T),所以我们称其为类型形参列表

必须传入类型实参 将其确定为具体的类型之后才可使用。而传入类型实参确定具体类型的操作被称为 实例化 :

func main() { 
    c := Add[string]("1", "2") //传入类型实参为string
    d := Add[int](1,2) //传入类型实参为int 
    fmt.Println(c) 
    fmt.Println(d) 
} 
//"12" //3

结构体泛型

type Person[T any] struct {
    name  string 
    sex   T 
    class T 
} 

func main() {
    t := Person[int]{name: "cbz", sex: 1, class: 1} 
    fmt.Println(t) 
} 
//{cbz 1 1}

结构体泛型多个变量

type Person[T any, S any] struct {
  name  T
  class S
}

func main() {
  p := Person[string, int]{name: "cbz", class: 123}
  fmt.Println(p)
}
//{cbz 123}

使用,分割,就可以实现多个泛型的实现。

map泛型

package main

import "fmt"

type TMap[K comparable, V string | int] map[K]V

func main() {
  m := make(TMap[int, string])
  m[123] = "123dsf"
  fmt.Println(m)
}
//map[123:123dsf]

Slice泛型

type TSlice[S any] []S

func main() {
  s := make(TSlice[int], 6)
  s[5] = 545
  fmt.Println(s)
}
// [0 0 0 0 0 545]

~:指定底层类型

type SliceElement interface {
  int | uint | string
}
type Slice[T SliceElement] []T

func main() {
  var s1 Slice[int] //正确

  type MyInt int
  var s2 Slice[MyInt] //错误。MyInt类型底层类型虽然是int,但是不是int类型,不符合Slice[T]的类型约束
}

这里发生错误的原因是,泛型类型Slice[T]允许int作为类型实参,但是不允许以int为底层类型的Myint类型。

为了从根本上解决和这个问题,Go新增了一个符号~,为类型实参增加了广泛性,这样代表着不光是int,以int为底层类型的类型也都可用于实例化。

使用~如下

type SliceElement interface {
  ~int | uint | string
}
type Slice[T SliceElement] []T

func main() {
  var s1 Slice[int] //正确

  type MyInt int
  var s2 Slice[MyInt] //正确。MyInt类型虽然是MyInt,但是底层类型是int,允许实例化
}

限制:使用~时有时限制:

  1. ~后面的类型不能是接口
  2. ~后面的类型必须为基本类型
type test1 interface {
    ~[]byte  // 正确
    ~MyInt   // 错误,~后的类型必须为基本类型
    ~error   // 错误,~后的类型不能为接口
}

泛型结构体并集

当定义一个泛型接口时,需要支持多个基本类型一般做法:

type AllInt interface {  // 类型集 Uint 是 ~uint 和 ~uint8 等类型的并集
     ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint32
}

这样的代码阅读性大大降低。若果一个接口有多行类型定义,可以取它们之间的交集

type AllInt interface {  // 类型集 Uint 是 ~uint 和 ~uint8 等类型的并集
     ~int | ~int8 | ~int16 | ~int32 | ~int64 
}

type AllUint interface{
     ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint32
}

//接口A代表的类型是AllInt和AllUint的交集,也就是说支持所有AllInt和ALLUint所支持的类型。
type A interface{  
     AllInt        
     AllUint
}

总结

泛型是Go 1.18中一个很大新语言特性,泛型的出现能使用我们的代码质量很高,更有效率。但也因为新特性,会带来学习成本,但我们很高兴能有泛型可用,我们希望它们能让 Go 程序员更有效率。