7

【1-7 Golang】Go语言快速入门—泛型

 1 year ago
source link: https://studygolang.com/articles/35878
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

【1-7 Golang】Go语言快速入门—泛型

tomato01 · 大约23小时之前 · 298 次点击 · 预计阅读时间 6 分钟 · 大约8小时之前 开始浏览    

  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版本支持了泛型,但是还是不建议在标准库使用,毕竟这次代码变动较大,而且后续泛型可能还会有较大变动,所以线上使用泛型需谨慎。


有疑问加站长微信联系(非本文作者))

280

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK