Go精进|Go语言学习笔记——Golang 1.18新特性泛型


文章目录

  • Golang 1.18新特性泛型
    • 一 什么是泛型
    • 二 Golang中的泛型
    • 三 泛型语法详解
      • 3.1 泛型的语法
      • 3.2 Constraint(约束)是什么
      • 3.3 自定义constraint(约束)
    • 四 泛型综合使用案例
      • 4.1 泛型切片
      • 4.2 泛型map
      • 4.3 泛型结构体
      • 4.4 泛型通道
      • 4.5 泛型函数
      • 4.6 泛型变量嵌套
      • 4.7 泛型方法
        • 4.7.1 接收器泛型
        • 4.7.2 方法参数泛型
      • 4.8 泛型接口
        • 4.8.1 基本接口泛型
        • 4.8.2 一般接口泛型

Golang 1.18新特性泛型 一 什么是泛型 泛型的英文是Generics,就是函数的参数,或者容器元素的类型,支持更广泛的类型,不再是特定的类型。
在Golang、Java、C++等这类静态语言中,是需要严格定义传入变量的类型的,斌不是随心所欲,例如在golang中:
func Sum(a, b int) int { return a + b }

在函数Sum中,不仅要严格定义传入参数a和b的变量类型,而且返回值的类型也需要严格定义,所有你只能传入int类型进行调用:
Sum(1, 2) // 3

如果传入其它类型的变量就会报错:
fmt.Println(Sum(1.23, 2.54)); ./main.go:33:18: cannot use 1.23 (untyped float constant) as int value in argument to Sum (truncated) ./main.go:33:24: cannot use 2.54 (untyped float constant) as int value in argument to Sum (truncated)

因此,如果当golang开发者想开发类似实现两个float类型变量相加的功能,只能另写一个函数:
func SumFloat(a, b float) float { return a + b }

或者写一个通用的Sum函数使用interface反射来判断:
func Sum(a, b interface{}) interface{} { switch a.(type) { case int: a1 := a.(int) b1 := b.(int) return a1 + b1 case float64: a1 := a.(float64) b1 := b.(float64) return a1 + b1 default: return nil } }

这样的话,不仅重复很多代码,而且类型频繁转换导致不仅性能低效,安全性上也不高。
所以泛型诞生了。
然而泛型是一把双刃剑,在给开发者带来便利的同时,同样会带来编译和效率的问题,因为泛型需要系统去推倒和计算变量的类型的,这在无形中会增加编译的时间和降低运行效率。
二 Golang中的泛型 首先来看一下,在Golang 1.18版本中是如何利用泛型来实现Sum函数的
func Sum[T int|float64](a,b T) T { return a + b }

然后再调用一下:
fmt.Println(Sum[int](1, 2))//3 fmt.Println(Sum[float64](1.23, 2.54))//3.77

先不去理解函数中各组件的含义,仅仅看代码就简洁了不少,乙肝函数就实现了多个类型的功能。
因为泛型针对的是类型变量,在golang中,类型是贯穿整个语法生态的,比如:变量、函数、接口、通道等。
三 泛型语法详解 3.1 泛型的语法
MyType[T1 constraint1 | constraint2, T2 constraint3...] ...

泛型的语法非常简单, 就类似于上面这样, 其中:
  • MyType可以是函数名, 结构体名, 类型名…
  • T1, T2…是泛型名, 可以随便取
  • constraint的意思是约束, 也是泛型中最重要的概念, 接下来会详解constraint
  • 使用 | 可以分隔多个constraint, T满足其中之一即可(如T1可以是constraint1constraint2中的任何一个)
3.2 Constraint(约束)是什么
约束的意思是限定范围, constraint的作用就是限定范围, 将T限定在某种范围内
而常用的范围, 我们自然会想到的有:
  • any(interface{}, 任何类型都能接收, 多方便啊!)
  • Interger(所有int, 多方便啊, int64 int32…一网打尽)
  • Float(同上)
  • comparable(所有可以比较的类型, 我们可以给所有可以比较的类型定制一些方法)
这些约束, 不是被官方定义为内置类型, 就是被涵盖在了constraints包内!!!
下面是builtin.go的部分官方源码:
// any is an alias for interface{} and is equivalent to interface{} in all ways. type any = interface{}// comparable is an interface that is implemented by all comparable types // (booleans, numbers, strings, pointers, channels, interfaces, // arrays of comparable types, structs whose fields are all comparable types). // The comparable interface may only be used as a type parameter constraint, // not as the type of a variable. type comparable comparable

下面是constraints.go的部分官方源码:
// Integer is a constraint that permits any integer type. // If future releases of Go add new predeclared integer types, // this constraint will be modified to include them. type Integer interface { Signed | Unsigned }// Float is a constraint that permits any floating-point type. // If future releases of Go add new predeclared floating-point types, // this constraint will be modified to include them. type Float interface { ~float32 | ~float64 } //......

3.3 自定义constraint(约束)
下面是constraints包中的官方源码:
type Signed interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 }

Signed约束就是这样被写出来的, 其中需要我们掌握的点有如下几个:
  • 使用interface{}就可以自定义约束
  • 使用 | 就可以在该约束中包含不同的类型, 例如int, int8, int64均满足Signed约束
  • 你可能会有疑问, ~是什么??? int我认识, ~int我可不认识呀??? 没关系, 实际上~非常简单, 它的意思就是模糊匹配, 例如:
    • type MyInt int64
    • 此时 MyInt并不等同于int64类型(Go语言特性)
    • 若我们使用int64来约束MyInt, 则Myint不满足该约束
    • 若我们使用~int64来约束MyInt, 则Myint满足该约束(也就是说, ~int64只要求该类型的底层是int64, 也就是模糊匹配了)
    • 官方为了鲁棒性, 自然把所有的类型前面都加上了~
例如:
type My_constraint_Num interface { ~int64 | ~float64 }

四 泛型综合使用案例 4.1 泛型切片
package mainimport "fmt"type Vector[T any] []T type NumSlice[T int | float64] []Tfunc main() { v := Vector[string]{"a", "b", "c"} fmt.Printf("v: %v\n", v) ns := NumSlice[int]{1, 2, 3, 4, 5} fmt.Printf("ns: %v\n", ns) ns2 := NumSlice[float64]{1.1, 2.2, 3.3, 4.4, 5.5} fmt.Printf("ns2: %v\n", ns2) }

运行结果:
[Running] go run "e:\golang开发学习\go_pro\main.go" v: [a b c] ns: [1 2 3 4 5] ns2: [1.1 2.2 3.3 4.4 5.5][Done] exited with code=0 in 0.983 seconds

4.2 泛型map
package mainimport ( "fmt" "golang.org/x/exp/constraints" )// map的key必须要可以比较, 也就是可以被 == 和 != 比较(用于处理哈希冲突) type MyMap[K comparable, V constraints.Integer | constraints.Float] map[K]Vfunc main() { m := make(MyMap[string, int]) m["表哥"] = 100 m["小张"] = 0 for k, v := range m { fmt.Printf("key: %v, val: %v\n", k, v) } }

运行结果:
[Running] go run "e:\golang开发学习\go_pro\main.go" key: 表哥, val: 100 key: 小张, val: 0[Done] exited with code=0 in 1.432 seconds

4.3 泛型结构体
这里以一个手写链表(只能存储整数)做示范
package mainimport ( "fmt" "golang.org/x/exp/constraints" )type MyIntergerNode[T constraints.Integer] struct { Next *MyIntergerNode[T] //注意这里一定要加类型声明 Data T }func main() { head := &MyIntergerNode[int64]{Next: nil, Data: 1} head.Next = &MyIntergerNode[int64]{Next: nil, Data: 2} for p := head; p != nil; p = p.Next { fmt.Printf("%d ", p.Data) } }

运行结果:
[Running] go run "e:\golang开发学习\go_pro\main.go" 1 2 [Done] exited with code=0 in 1.15 seconds

4.4 泛型通道
package mainimport "fmt"type Ch[T any] chan Tfunc main() { ch := make(Ch[int], 1) ch <- 10 res := <-ch fmt.Printf("res: %v\n", res) fmt.Printf("ch: %v\n", ch) }

运行结果:
[Running] go run "e:\golang开发学习\go_pro\main.go" res: 10 ch: 0xc0000180e0[Done] exited with code=0 in 1.237 seconds

4.5 泛型函数
这里自定义一个比较64比特大小的类型的函数
package mainimport ( "fmt" )// 自定义约束 type My_64_Bits_Long_Num interface { ~int64 | ~float64 }func MyCompare[T My_64_Bits_Long_Num](a, b T) bool { return a < b }func main() { var a int64 = 1 var b int64 = 8 // 函数可以省略不写参数类型 ans := MyCompare(a, b) if ans { fmt.Printf("%v小于%v", a, b) } else { fmt.Printf("%v大于%v", a, b) } }

运行结果:
[Running] go run "e:\golang开发学习\go_pro\main.go" 1小于8 [Done] exited with code=0 in 1.18 seconds

注意:泛型函数的调用可以不用声明T的类型,这里是利用率类型推导
go里的类型推导用法:
a := 3 //编译器自动推导 a 是int型变量 b := "hello" // 编译器自动推导 b 是string型变量

这里的调用也就说得通了:
ans := MyCompare(a, b) // 自动推导出 T 是 int

4.6 泛型变量嵌套
【Go精进|Go语言学习笔记——Golang 1.18新特性泛型】就像声明变量类型支持嵌套一样,泛型变量也支持嵌套。
例如下面定义了两个泛型变量,一个是S,另一个是嵌套了S的map泛型变量P
type MyStruct[S int | string, P map[S]string] struct { Name string Content S Job P }

接下来是如何去实例化:
//实例化int的实参 var MyStruct1 = MyStruct[int, map[int]string]{ Name:"small", Content: 1, Job:map[int]string{1: "ss"}, } fmt.Printf("%+v", MyStruct1) // {Name:small Content:1 Job:map[1:ss]} //实例化string的实参 var MyStruct2 = MyStruct[string, map[string]string]{ Name:"small", Content: "yang", Job:map[string]string{"aa": "ss"}, } fmt.Printf("%+v", MyStruct2)//{Name:small Content:yang Job:map[aa:ss]}

注意:T和S要保持实参的一致
再来看一个稍微复杂的例子,两个泛型变量之间的嵌套使用,Struct1这个结构体切片,它第二个泛型参数的类型是Slice1:
//切片泛型 type Slice1[T int | string] []T //结构体泛型,它的第二个泛型参数的类型是第一个切片泛型。 type Struct1[P int | string, V Slice1[P]] struct { NameP Title V }

这种情况下,该如何去实例化?请始终记住:在泛型里面,如果要实例化一个泛型变量,需要用实际传入的变量类型去替换T
//实例化切片 mySlice1 := Slice1[int]{1, 2, 3} //用int去替换P, 用Slice1去替换Slice1[p] myStruct1 := Struct1[int, Slice1[int]]{ Name:123, Title: []int{1, 2, 3}, } //用string去替换P, 用Slice1去替换Slice1[p] myStruct2 := Struct1[string, Slice1[string]]{ Name:"hello", Title: []string{"hello", "small", "yang"}, } fmt.Println(mySlice1, myStruct1, myStruct2) //[1 2 3]{123 [1 2 3]}{hello [hello small yang]}

最后再看另一中嵌套方式:
type Slice1[T int|float64|string] []T type Slice2[T int|string] Slice1[T]

如何去实例化呢?
mySlice1 := Slice1[int]{1, 2, 3, 4} mySlice2 := Slice2[string]{"hello", "small"} fmt.Println(mySlice1, mySlice2) //[1 2 3 4] [hello small]

发现:Slice2其实就是继承和实现了Slice1,也就是说Slice2的类型参数约束的取值范围必须是在Slice1的取值范围里,下面尝试改一下:
type Slice1[T int|float64|string] []T type Slice2[T bool|int|string] Slice1[T] mySlice1 := Slice1[int]{1, 2, 3, 4} mySlice2 := Slice2[bool]{true, false}

运行则会报错:
T does not implement int|float64|string

因此可以尝试更加复杂的嵌套写法:
type Slice1[T bool | float64 | string | int] []T type Slice2[T bool | float64 | string] Slice1[T] type Slice3[T bool | int] Slice2[T]

注意一眼就能看出,Slice3的曲子范围并不是在Slice2的范围中,因为多了一个int类型。
4.7 泛型方法
先看一个例子:
type DemoInt int func (t DemoInt) methodName(param string) string { }

这种类型,不管是前面的(t DemoInt),还是方法名后面的参数(param string)里面都会涉及到具体的类型变量,因此都可以改造成泛型
4.7.1 接收器泛型 先定义一个泛型变量,然后在这个变量上加上一个方法:
//申请一个自定义的泛型约束类型 type NumberAll interface { ~int|~int64|~int32|~int16|~int8|~float64|~float32 } //申请一个泛型切片类型,泛型参数是T,约束的类型是 NumberAll type SliceNumber[T NumberAll] []T //给泛型切片加上1个接收器方法 func (s SliceNumber[T]) SumIntsOrFloats() T { var sum T for _, v := range s { sum += v } return sum }

注意:(s SliceNumber[T])这个写法,T后面是不用带上它的约束类型NumberAll的,然后返回值也是T
如何去调用呢?其实和普通接收器方法一样,只不过需要先去实例化泛型切片
//实例化成int var ss1 SliceNumber[int] = []int{1, 2, 3, 4} //简化 //ss1 := SliceNumber[int]{1, 2, 34} ss1.SumIntsOrFloats() // 10 //实例化成float64 var ss2 SliceNumber[float64] = []float64{1.11, 2.22, 3.33} //简化 //ss2 := SliceNumber[float64]{1.11, 2.22, 3.33} ss2.SumIntsOrFloats()//6.66

4.7.2 方法参数泛型 当接收器是一个普通的类型,在方法的参数里面使用泛型变量,如下:
type DemoSlice []int func (d DemoSlice) FindOne[T int](a T) bool { }

这是会发现报错:Method cannot have type parameters,即:方法是不支持泛型的
既然函数支持泛型,接收器也支持泛型,所以我们把两者结合起来稍加修改:
type DemoSlice[T int | float64] []T func (d DemoSlice[T]) FindOne(a T) bool { for _, t := range d { if t == a { return true } } return false } s1 := DemoSlice[int]{1, 2, 3, 4} fmt.Println(s1.FindOne(1)) s2 := DemoSlice[float64]{1.2, 2.3, 3.4, 4.5} fmt.Println(s2.FindOne(1.2))

4.8 泛型接口
  • 如果一个接口里面只有方法,这个接口叫基本接口
  • 如果一个接口里面有约束类型,有或者没有方法的,这个接口叫一般接口
4.8.1 基本接口泛型 如何定义一个泛型接口呢?
type MyInterface[T int | string] interface { WriteOne(data T) T ReadOne() T }

和定义泛型函数一样,在接口名后面接[],里面填充的接口里面方法中需要用到泛型参数
但值得注意的是,别写反了:
//会提示错误:interface method must have no type parameters type MyInterface interface { WriteOne[T int | string] (data T) T ReadOne[T int | string] () T }

那么如何实现一个基本泛型接口呢?
先定义一个普通的结构体类型,然后通过接收器方式绑定两个方法:
type Note struct { } func (n Note) WriteOne(one string) string { return "hello" } func (n Note) ReadOne() string { return "small" }

然后,我们看如何实例化泛型接口并实现接口:
var one MyInterface[string] = Note{} fmt.Println(one.WriteOne("hello")) fmt.Println(one.ReadOne())

值得注意的是泛型参数的值的类型,要和被实现方法的参数保持一致,不然会报错:
//接口实例化用的是int,但是实现的方法里面都是string类型,并不匹配,无法被实现。 var one MyInterface[int] = Note{} fmt.Println(one.WriteOne("hello")) fmt.Println(one.ReadOne())

报错如下:
cannot use Note{} (value of type Note) as type MyInterface[int] in variable declaration: Note does not implement MyInterface[int] (wrong type for ReadOne method) have ReadOne() string want ReadOne() int

4.8.2 一般接口泛型 先定义一个一般泛型接口,就是接口里有约束类型:
type MyInterface2[T int | string] interface { int|string WriteOne(data T) T ReadOne() T }

当我们实例化:
type Note2 int func (n Note2) WriteOne(one string) string { return "hello" } func (n Note2) ReadOne() string { return "small" } var one MyInterface2[string] = Note{}

会出现报错:接口包含约束元素int和string,只能作为类型参数来使用
简而言之,一般泛型接口只能被当做类型参数来使用,无法被实例化

    推荐阅读