泛型Go语言近年来最重要的特性之一,但是,很多开发者在使用泛型时,可能会对其中的某些语法感到困惑。特别是~int这样的写法,常常让人摸不着头脑。究竟这个波浪线~代表什么含义?它为什么存在?它又能为我们带来什么便利?

其实,~T表示“所有底层类型为T的类型”,而不仅仅是T本身,也就是近似类型。这种设计使得泛型函数能够接受具有相同底层类型的多种类型,从而增强了泛型的灵活性和实用性,同时保持了类型安全。

为什么需要~int?

在 Go 1.18 之前,如果你定义了一个类型别名type MyInt int,尽管MyInt的底层类型是int,但在类型系统中,MyIntint是不同的类型。这导致了一个实际问题:当你编写一个泛型函数来处理所有整数类型时,自定义的整数类型会被排除在外。

func PrintInt[T int](v T) {
    fmt.Println(v)
}

func main() {
  type MyInt int
  var x MyInt = 10
  // 传统泛型约束无法处理MyInt
  PrintInt(x) // 编译错误
}

这就是~int要解决的问题。~T语法表示“所有底层类型为T的类型”,而不仅仅是T本身。

func PrintInt[T ~int](v T) {
    fmt.Println(v)
}
func main() {
    type MyInt int
    var x MyInt = 10
    // 使用~int约束可以处理MyInt
    PrintInt(x) // 正常工作,输出10
}

波浪线本质是类型集合

Go泛型中,~int定义了一个类型集合,包含:

  • int本身
  • 所有底层类型为int的类型(如type MyInt int
  • 所有底层类型为上述类型的类型(递归定义)

在其他编程语言中也有泛型,然而使用~标识近似类型,或许又是Go语言一贯的哲学,在严格类型安全和实际工程需求之间寻找平衡。

实际应用场景

1. 自定义类型处理

当你的库需要支持用户自定义的数值类型时,~int变得至关重要。用户可以使用type UserID int这样的定义,而你的泛型函数仍然能够处理它。

2. 类型约束组合

~int可以与其他约束组合使用:

type Numeric interface {
    ~int | ~float64
}

这样的接口约束可以接受任何底层类型为intfloat64的类型。

3. 保持类型安全

与使用anyinterface{}相比,~int提供了编译时类型检查。编译器能确保只有正确的类型被传入,避免了运行时的类型断言。

值得注意的复杂性

~符号的引入反映了Go对兼容性和实用性的考量。如果没有这个特性,泛型的实用性将大打折扣,用户要么被迫使用标准类型,要么需要复杂的类型转换。

但这一设计也带来了一些复杂性。开发者需要理解:

  • 底层类型(underlying type)的概念
  • 类型集合(type set)的思维模型
  • 编译时类型推导的规则

特别注意

  • ~符号只能用于基本类型或复合类型。

基本类型(如~int~string)或复合类型(如~[]byte~map[string]any~func()~struct{})不能用于接口类型或未定义底层类型的类型。

例如,~interface{}是无效的。

  • 近似类型匹配的是确切的底层类型。

type MyInt int满足~int约束,但type MyIntSlice []MyInt并不满足 ~[]int约束。

因为MyIntSlice的底层类型是[]MyInt(而非[]int),因此无法匹配~[]int(要求底层类型严格等于[]int

写在最后

Go中的近似类型是泛型系统的核心组成部分,它通过~T语法提供了匹配底层类型的强大能力。这种设计既保持了Go语言的简洁哲学,又提供了足够的表达能力来处理现实世界的类型复杂性。

~int虽然丰富了Go语言泛型中类型约束的灵活性,有点像Java中的 B extends A 向上转型。

但是在Java中,如果B extends A,那List<B>可以通配List<? extends A>

Go中,type MyIntSlice []MyInt并不满足~[]int约束。

(也许Go以后会支持[]~int来满足,你觉得呢? )。