Go语言入门23:泛型

Keywords: #技术 #Golang #Go 入门笔记
Table of Contents

Go 语言入门基础学习笔记之 Go 语言的泛型

golang

泛型

泛型通过引入类型形参和类型实参,让一个函数获得了处理多种不同类型数据的能力。

实际上,Go 语言中可以通过接口+反射来基本实现泛型的功能。但是反射有一些问题,比如编译时不会提示错误,用起来不方便等等。因此 Go 语言从 1.18 版本开始正式支持泛型编程

func getBigger(a, b int64) int64 {
    if a > b {
        return a
    } else {
        return b
    }
}
func main() {
    getBigger(3, 6)
}

比如我们需要让一个比较大小的函数不仅仅支持 int 32 格式,还需要支持 int 64 格式。如果不使用泛型则需要写两个高度重复的函数代码,但是使用泛型就可以只写一个函数。

func getBigger[T int32 | int64](a, b T) T {
    if a > b {
        return a
    } else {
        return b
    }
}
func main() {
    getBigger[int32](3, 6)
}

也可以换一种形式,使用接口类型来定义类型。

type Comparable interface {
    int32 | int64
}
func getBigger[T Comparable](a, b T) T {
    if a > b {
        return a
    } else {
        return b
    }
}
func main() {
    getBigger[int32](3, 6)
}

int 32 也可以写成 rune 类型。Go 语言中的字符 rune 类型是完全等同于 int 32 的。type rune = int32

但是如果我们直接定义一个类型名为 int 32,在使用时用这个类型名代替 int 32 是不正确的,会报错。

type myInt int32
type Comparable interface {
    int32 | int64
}
func getBigger[T Comparable](a, b T) T {
    if a > b {
        return a
    } else {
        return b
    }
}
func main() {
    getBigger[myInt](3, 6) // 错误写法
}

除非把 myInt 加入 Comparable 接口中才可以使用。

type Comparable interface {
    int32 | int64 | myInt
}

或者可以在 int 32 之前加上 ~,表示只要是 int 32 类型的都可以使用。

type Comparable interface {
    ~int32 | int64
}

我们可以也把 Comparable 类型换成 Go 语言标准库中的 Ordered 类型。

func getBigger[T cmp.Ordered](a, b T) T {
    if a > b {
        return a
    } else {
        return b
    }
}

Ordered 类型包含大部分整型、浮点型和字符串类型。

type Ordered interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
        ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
        ~float32 | ~float64 |
        ~string
}

需要注意的是,虽然函数支持泛型,但是结构体方法却不支持泛型。而结构体可以支持泛型。

type Apple[T cmp.Ordered] struct{}
func (Apple[T]) getBigger(a, b T) T {
    if a > b {
        return a
    } else {
        return b
    }
}
func main() {
    a := Apple[int32]{}
    a.getBigger(3,6)
}

前面提到,Go 语言的泛型功能可以通过接口+反射的方法来变相实现。比如下面的 http 请求的函数例子。

type GetUserRequest struct{}
type GetBookRequest struct{}
func httpRPC(request any) {
    url := "http://127.0.0.1/"
    tp := reflect.TypeOf(request)
    switch tp.Name() {
    case "GetUserRequest":
        url += "user"
    case "GetBookRequest":
        url += "book"
    default:
        panic("不支持的request类型")
    }
    fmt.Println(url)
    bs, _ := json.Marshal(request)
    http.Post(url, "text/plain", bytes.NewReader(bs))
}

在这个例子中,httpRPC 函数通过设置一个空接口类型来接收任意的数据类型,在函数中再通过反射机制来获取相应数据的具体类型。

reflect.TypeOf(request) 替换为 request.(type) 也可以拿到变量的类型名称。

当然,这样的情景也可以使用泛型。这里使用泛型会有一定的好处:

如果使用泛型在编写代码阶段,往函数中传入了意外的参数类型,编译阶段就会有错误提示。而如果使用反射则会进入 switch 的 default 抛出一个异常,只有在运行阶段才会发现错误。

因此,更推荐使用泛型编程。

type GetUserRequest struct{}
type GetBookRequest struct{}
func httpRPC[T GetUserRequest | GetBookRequest](request T) {
    url := "http://127.0.0.1/"
    tp := reflect.TypeOf(request)
    switch tp.Name() {
    case "GetUserRequest":
        url += "user"
    case "GetBookRequest":
        url += "book"
    fmt.Println(url)
    bs, _ := json.Marshal(request)
    http.Post(url, "text/plain", bytes.NewReader(bs))
}
func main() {
    httpRPC(GetUserRequest{})
}

可以直接省略 default 情况,因为这两个 case 已经可以命中所有情况了。


参考课程:

  1. golang 泛型玩到炉火纯青