乐趣区

关于go:17-GolangGo语言快速入门泛型

  Golang 在 1.18 版本反对了泛型,写过 java/c++ 等语言的可能对泛型有肯定的理解。那么泛型到底是什么呢?他有什么作用呢?

为什么须要泛型

  为什么须要泛型呢?Golang 是强类型语言,任何变量或者函数参数,都须要定义明确的参数类型。假如咱们须要实现这么一个函数,输出两个参数,函数返回其相加的值,输出参数能够是两个整型 int,浮点数 float,还有可能是字符串等等,这时候通常怎么办?定义多个函数实现吗?如上面程序所示:

// 定义多个函数实现
func twoIntValueSum(a, b int) int {return a + b}

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

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

// 定义一个函数,类型是 interface{}

  这样就可能导致存在大量反复代码,而且调用方还须要依据参数类型决定调用哪一个办法。还能怎么办呢?只定义一个函数,只是参数是 interface{},函数外部通过反射等形式,执行对应的操作,如上面程序:

func twoValueSum(a, b interface{}) (interface{}, error)  {if reflect.TypeOf(a).Kind() != reflect.TypeOf(b).Kind() {return nil, errors.New("two value type different")
    }

    switch reflect.TypeOf(a).Kind() {
    case reflect.Int:
        return reflect.ValueOf(a).Int() + reflect.ValueOf(b).Int(), nil
    case reflect.Float64:
        return reflect.ValueOf(a).Float() + reflect.ValueOf(b).Float(), nil
    case reflect.String:
        return reflect.ValueOf(a).String() + " " + reflect.ValueOf(b).String(), nil
    default:
        return nil, errors.New("unknow value type")
    }
}

  应用反射实现的话,依赖反射性能较低,二来能够看到输出参数和返回值都是 interface{},应用方还须要多执行一步返回值类型转换,而且反射相对而言还是比较复杂的。

泛型初体验

  那么还有其余什么方法吗?这就要说到 Go 1.18 版本实现的泛型了,泛型相当于定义了一个函数模板,真正调用函数的时候,再确定参数以及返回值等具体类型,基于泛型实现上述性能如下:

package main

import "fmt"

func main() {ret := twoValueSum[int](100, 200)
    fmt.Println(ret)

    ret1 := twoValueSum[string]("hello", "world")
    fmt.Println(ret1)
}

func twoValueSum[T int | float64 | string](a T, b T) T {return a + b}

  泛型类型或者泛型函数定义的语法格局能够形容为 [Identifier TypeConstraint],上述程序中的 T 就是标识符(Identifier),int 等就是 TypeConstraint(类型限度,也就是说 twoValueSum 函数的输出参数类型只能是这几种,不能是其余的),留神在调用具体函数时,须要申明真正的类型。

  上面举几个泛型程序事例,介绍下泛型类型常见定义形式(泛型函数参考 twoValueSum 函数定义):

// 定义切片类型,元素类型能够是 int,float64 或者 string
type Slice[T int|float64|string] []T
// 实例化变量 arr1,留神申明了切片元素类型为 int
var arr1 Slice[int] = []int{1, 2, 3} 
// 实例化变量 arr2,留神申明了切片元素类型为 string
var arr2 Slice[string] = []string{"Hello", "World"} 

// 自定义 map 类型,key 只能是 string,value 能够是 int、float32 或者 float64
type DefineMap[KEY string, VALUE int | float32 | float64] map[KEY]VALUE  
var m DefineMap[string, int] = map[string]int{
    "zhangsan": 89,
    "lisi":  80,
}

  再举一个例子,假如想实现一个切片比拟的函数(相似于字符串的字典序比拟),该怎么定义呢?参数能够是 []int,[]float,[]string 等等,只有元素可比拟即可。参考官网给的事例:

// 定义浮点数类型汇合,type Float interface {~float32 | ~float64}
// 定义有符号整数汇合
type Signed interface {~int | ~int8 | ~int16 | ~int32 | ~int64}
// 定义无符号整数汇合
type Unsigned interface {~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr}
// 定义整数汇合
type Integer interface {Signed | Unsigned}

// 定义可比拟类型汇合:整数,浮点数,字符串
type Ordered interface {Integer | Float | ~string}

//Compare 函数须要输出两个切片,切片元素类型必须是可排序的,这里限度为类型 Ordered
func Compare[E Ordered](s1, s2 []E) int
// 二分法实现函数,输出切片以及查找元素,切片元素类型必须是可排序的,这里限度为类型 Ordered
func BinarySearch[E Ordered](x []E, target E) (int, bool)

  留神目前 Go 语言规范库还没有应用泛型(Go 作者不倡议),不过有几个试验库应用了泛型,有趣味的读者能够查阅:

golang.org/x/exp/constraints
Constraints that are useful for generic code, such as constraints.Ordered.

golang.org/x/exp/slices
A collection of generic functions that operate on slices of any element type.

golang.org/x/exp/maps
A collection of generic functions that operate on maps of any key or element type.

  另外,留神符号 ~,这是什么意思呢?假如咱们定义一个泛型切片类型限度蕴含 int,另外还有一个自定义类型(其实也是 int),自定义类型能用来结构该切片吗?如下:

type Slice[T int | float64 | string] []T
type Integer int

var arr Slice[Integer] = []Integer{1,2,3}

//Integer does not implement int|float64|string (possibly missing ~ for int in constraint int|float64|string)

  留神尽管自定义类型 Integer 其实也就是 int,然而这两种类型是不相等的,所以这里才有语法错误 ”Integer does not implement int”,针对这种状况,Go 语言给出的倡议是应用符号 ~ 定义,如:

type Slice[T ~int| ~ float64 | ~string] []T
type Integer int

// 编译通过
var arr Slice[Integer] = []Integer{1,2,3}

  更多的泛型语法,以及应用场景,有趣味的读者持续钻研,这里就不一一介绍了。

泛型函数底层是怎么实现的

  最初再思考一个问题,Go 语言是如何实现上述泛型事例呢?为什么只定义一个函数实现,就能传递多种类型参数呢?为什么只定义一种变量类型,却能实例化多种类型的变量呢?

  咱们以上面的程序为例,看一下编译后的汇编代码,就明确其实现原理了:

package main

import "fmt"

func main() {ret := twoValueSum[int](100, 200)
    fmt.Println(ret)

    ret1 := twoValueSum[string]("hello", "world")
    fmt.Println(ret1)
}


func twoValueSum[T int | float64 | string](a T, b T) T {return a + b}

//go tool compile -S -N -l test.go

"".main STEXT
    //main 函数中的函数调用替换了!0x0037 00055 (test.go:7)    CALL    "".twoValueSum[go.shape.int_0](SB)
    0x00e4 00228 (test.go:10)    CALL    "".twoValueSum[go.shape.string_0]

// 编译阶段生成的两个具体的函数
"".twoValueSum[go.shape.int_0] STEXT"".twoValueSum[go.shape.string_0] STEXT

  能够看到,咱们只定义了一个函数实现 twoValueSum,然而 Go 编译器为咱们生成了两个具体的函数(因为咱们调用了这两种函数实现),而针对 twoValueSum 的函数调用,也都在编译过程被替换了。

总结

  须要留神的是,Golang 尽管在 1.18 版本反对了泛型,然而还是不倡议在规范库应用,毕竟这次代码变动较大,而且后续泛型可能还会有较大变动,所以线上应用泛型需谨慎。

退出移动版