查看原文
其他

Go泛型系列:Go1.18 类型约束那些事

站长polaris polarisxu 2022-05-13

阅读本文大概需要 6 分钟。

大家好,我是 polarisxu。

上篇《Go泛型系列:提前掌握Go泛型的基本使用》简单讲解了泛型中的约束,但约束相关内容远不止那些,本文介绍更多约束相关内容。

请安装最新的 tip 版本,方便验证本文的内容。当然,也可以通过 https://gotipplay.golang.org/ 在线验证。

01 语法变更

上次提到,定义约束的语法类似这样:

type Addable interface {
 type intint8int16int32int64uintuint8uint16uint32uint64uintptrfloat32float64complex64complex128string
}

不过目前已经确认,语法改成如下形式:

type Addable interface {
  int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | uintptr | float32 | float64 | complex64 | complex128 | string
}

对于这样的修改,大家褒贬不一。就语义来说,新的方式 | 有或之意,更贴切,而且少了一个 type,更简洁。

02 额外用法

除了以上提到的优点,新的方式还还有额外的用途,也就是不用每次都定义一个接口约束。

看一个具体示例:

package main

import (
 "fmt"
)

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

func main() {
 fmt.Println(add(12))
 fmt.Println(add(1.22.3))
  // fmt.Println(add("a", "b"))
}
// Output:
// 3
// 3.5

最后注释的一行代码,会编译报错:string does not satisfy int|float64

注意 add 函数的泛型约束:T int|float64。如果约束是逗号分隔,无法采用这种语法:

func add[T int,float64](a, b T) T

以上代码显然很不友好,编译器也不好解析。

采用了 | 后,不需要每次都定义接口约束,可以让代码更少。不过,这种方式建议只在少数类型约束时才适合,否则可读性太差。

除了以上用法,约束还有一种用法。

在 Go 语言中,基于某类型定义新类型,有时可能希望泛型约束是某类型的所有衍生类型。看一个具体例子:

package main

import (
 "fmt"
)

func add[T ~string](x, y T) T {
 return x + y
}

type MyString string

func main() {
 var x string = "ab"
 var y MyString = "cd"
 fmt.Println(add(x, x))
 fmt.Println(add(y, y))
}

// Output:
// abab
// cdcd

注意 add 函数的签名:

func add[T ~string](x, y T) T

约束 ~string  表示支持 string 类型以及底层是 string 类型的类型,因此 MyString 类型值也可以传递给 add。

03 注意事项

约束形式的多样性,导致 Go 泛型语法一下子复杂起来:

// 没有任何约束
func add[T any](x, y T) T
// 约束 Addble (需要单独定义)
func add[T Addble](x, y T) T
// 约束允许 int 或 float64 类型
func add[T int|float64](x, y T) T
// 约束允许底层类型是 string 的类型(包括 string 类型)
func add[T ~string](x, y T) T

在泛型中,有些场景可能想当然可以成立,但结果可能不成立,在使用时需要注意(当然,不排除将来支持)。比如:

func MakeChan[T chan bool | chan int](c T) {
  _ = make(T) // 错误
   _ = new(T) // 正确
  _ = len(c)  // 正确
}

// 以下代码无法编译:
// cannot range over c (variable of type T constrained by []string|map[int]string) (T has no structural type)
func ForEach[T []string | map[int]string](c T, f func(int, string)) {
 for i, v := range c {
  f(i, v)
 }
}

ForEach 函数的签名,你能看懂吗?泛型确实让 Go 复杂起来了,虽然语法允许,但建议大家以后写泛型代码时一定要尽量保证可读性。




往期推荐


我是 polarisxu,北大硕士毕业,曾在 360 等知名互联网公司工作,10多年技术研发与架构经验!2012 年接触 Go 语言并创建了 Go 语言中文网!著有《Go语言编程之旅》、开源图书《Go语言标准库》等。


坚持输出技术(包括 Go、Rust 等技术)、职场心得和创业感悟!欢迎关注「polarisxu」一起成长!也欢迎加我微信好友交流:gopherstudio

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存