教程:泛型入门

本文翻译自《Tutorial: Getting started with generics》。

目录

前提条件

为代码新建一个文件夹

添加两个非泛型函数

添加一个泛型函数来处理多种类型

调用泛型函数时删除类型参数

声明一个类型约束

结论

完整的代码

本教程介绍Go中泛型的基本知识。使用泛型,你可以声明和使用函数或类型,这些函数或类型可以与调用它们的代码提供的任意一组类型一起工作。 在本教程中,你将声明两个简单的非泛型函数,然后在单个泛型函数中实现相同的逻辑。

前提条件

  • 安装Go 1.18或更高版本。有关安装说明,请参阅安装Go
  • 用于编辑代码的工具。你拥有的任何文本编辑器都可以正常工作。
  • 一种命令行终端。Go在Linux和Mac上的任何命令行终端,以及Windows中的PowerShell或cmd上都能很好地工作。

为代码新建一个文件夹

首先,为你要编写的代码创建一个文件夹。

1 打开命令提示符并更改到家目录。 在Linux或Mac上:

$ cd

在Windows上:

C:\> cd %HOMEPATH%

本教程的其余部分将把$作为提示符。你使用的命令也适用于Windows。

2 在命令提示符下,为代码创建一个名为generics的目录。

$ mkdir generics
$ cd generics

3 创建一个模块来保存你的代码。

运行go mod init命令创建一个名为example/generics的模块。

$ go mod init example/generics
go: creating new go.mod: module example/generics

注意:对于生产代码,你应该指定一个更适合自己需求的模块名称。有关详细信息,请参阅管理依赖关系

接下来,你将添加一些简单的代码来处理映射(map)。

添加两个非泛型函数

在这一步中,你将添加两个函数,每个函数将一个map里的值相加并返回总数。

你要声明两个函数而不是只声明一个,因为你使用的是两个不同类型的map:一个存储int64值,另一个存储float64值。

编写代码

1 使用文本编辑器,在generics目录中创建一个名为main.go的文件,在该文件中编写Go代码。

2 在main.go文件的顶部,粘贴以下包声明。

package main

与函数库相对的独立可执行的Go程序,始终位于main包中。

3 在声明main包代码的下面,粘贴以下声明两个函数的代码。

// SumInts函数把映射m的所有int64值加起来。
func SumInts(m map[string]int64) int64 {
    var s int64
    for _, v := range m {
        s += v
    }
    return s
}

// SumFloats函数把映射m的所有float64值加起来。
func SumFloats(m map[string]float64) float64 {
    var s float64
    for _, v := range m {
        s += v
    }
    return s
}

在此代码中,你:

声明两个函数,将映射m的值相加并返回其总和。

  • SumFloats函数使用字符串键到float64值的映射m
  • SumInts函数采用字符串键到int64值的映射m

4 在main.go的顶部,包声明的下面,粘贴以下main函数代码来初始化这两个映射,并作为实参分别调用上一步中声明的函数。

func main() {
    // 初始化字符串键整型数值的一个map
    ints := map[string]int64{
        "first":  34,
        "second": 12,
    }

    // 初始化字符串键浮点数值的一个map
    floats := map[string]float64{
        "first":  35.98,
        "second": 26.99,
    }

    fmt.Printf("Non-Generic Sums: %v and %v\n",
        SumInts(ints),
        SumFloats(floats))
}

在此代码中,你:

  • 初始化一个int64值的映射和一个float64值的映射,每个映射里面都有两个条目。
  • 调用前面声明的两个函数,以分别计算每个映射的值的总和。
  • 打印输出结果。

5 在main.go的顶部附近,就在包声明的下方,导入你需要支持刚刚编写的代码的包。 前几行代码应该如下所示:

package main
import "fmt"

6保存代码文件。

运行代码

在包含main.go的目录中的命令行终端中,运行代码:

$ go run .
Non-Generic Sums: 46 and 62.97

使用泛型,你只需编写一个函数,而不是两个。接下来,你将为包含整数值或浮点值的映射添加一个泛型函数。

添加一个泛型函数来处理多种类型

在本节中,你将添加一个泛型函数,该函数可以接收包含整数值或浮点值的映射,从而用这一个函数有效地替换你刚刚编写的两个函数。

要支持多种类型的值,该函数需要一种方式来声明它支持哪些类型。另一方面,调用该函数的代码需要用一种方式来指定是使用整数值还是浮点值的映射。

为了支持这一点,你编写的这个函数,除了声明普通函数参数之外,还要声明类型参数(type parameter)。这些类型参数使函数具有通用性(成为泛型函数),使其能够处理不同类型的实参。你将使用类型参数和普通函数实参来调用该函数。

每个类型参数都有一个类型约束(type constraint),它充当类型参数的一种元类型(meta type)。每个类型约束都指定了允许的具体类型,调用泛型函数的代码可以将这些具体类型传入相应的类型参数。

虽然一个类型参数可以通过类型约束指定一组类型,但在编译时,一个类型参数代表单个类型——由调用泛型函数的代码提供一个具体的类型。如果一个类型参数的类型约束不允许某个具体类型,代码就会编译报错。

请记住,类型参数所代表的具体类型必须支持泛型代码对其执行的所有操作。例如,如果泛型函数的代码试图对代表数值类型的类型参数执行字符串操作(例如下标操作),代码就会编译报错。

在你即将编写的代码中,你将使用一个允许int64float64类型的类型约束。

编写代码

1 在前面添加的两个函数下面,粘贴以下泛型函数。

// SumIntsOrFloats函数计算映射m的值的总和,同时支持int64或float64类型的值。
func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
    var s V
    for _, v := range m {
        s += v
    }
    return s
}

在上述代码中,

在上述代码中,

  • 声明一个SumIntsOrFloats函数,该函数具有两个类型参数(位于方括号内)KV,以及一个使用类型参数的普通函数参数,即类型是map[K]Vm。该函数返回V类型的值。
  • 为类型参数K指定类型约束comparable。专门针对此类情况,在Go中预先声明了comparable类型约束。它允许其值可用作比较运算符==!=的操作数的任何类型。Go要求映射的键是可比较的。因此必须将K声明为可比较的,这样你就可以将K用作映射中的键。它还能确保别人调用上述函数时传入允许的键类型。
  • V类型参数指定一个约束,它是两种类型的联合:int64float64。使用|运算符指定两种类型的联合,这意味着此约束允许其中任何一种类型。编译器将允许其中任何一种类型作为别人调用上述函数时传入的类型参数。
  • 指定函数参数m的类型为map[K]V,其中KV是已经为类型参数指定的类型。请注意,我们知道map[K]V是有效的映射类型,因为K是可比较的类型。如果我们没有声明K是可比较的类型,编译器将拒绝对map[K]V的使用。

在main.go中,在你已有的代码下方,粘贴以下代码。

fmt.Printf("Generic Sums: %v and %v\n",
    SumIntsOrFloats[string, int64](ints),
    SumIntsOrFloats[string, float64](floats))

在此代码中,你:

  • 调用你刚刚声明的泛型函数,传递你创建的每个map。
  • 指定类型参数——方括号中的类型名称——以清楚地指出在你调用的函数中替换类型参数的具体类型。

正如你将在下一节中看到的,通常可以在函数调用中省略类型参数。Go通常可以从你的代码中推断出它们。

  • 打印输出函数返回的总和。

运行代码

在包含main.go的目录的命令行运行代码:

$ go run .
Non-Generic Sums: 46 and 62.97
Generic Sums: 46 and 62.97

为了运行你的代码,在每次调用中,编译器将类型参数替换为指定的具体类型。

在调用你编写的泛型函数时,你给出了类型参数,告诉编译器具体该使用什么类型来代替函数的类型参数。正如你将在下一节中看到的,在许多情况下,你可以省略这些类型参数,因为编译器可以推断出它们。

调用泛型函数时删除类型参数

在本节中,你将增加调用泛型函数的代码的一个修改版本,简化了调用代码。你将删除在本例中不需要的类型参数。

当Go编译器可以推断出要使用的具体类型时,可以在调用代码中省略类型参数。编译器可以从函数实参的类型中推断出类型参数。

请注意,这并不总是可行的。例如,如果需要调用没有普通参数的泛型函数,则需要在函数调用的代码中给出类型实参。

编写代码

在main.go中,在你已经拥有的代码下面,粘贴以下代码。

fmt.Printf("Generic Sums, type parameters inferred: %v and %v\n",
    SumIntsOrFloats(ints),
    SumIntsOrFloats(floats))

运行代码

在包含main.go的目录的命令行运行代码:

$ go run .
Non-Generic Sums: 46 and 62.97
Generic Sums: 46 and 62.97
Generic Sums, type parameters inferred: 46 and 62.97

接下来,你将int64float64类型的并集定义为可以重用的类型约束(例如被其他代码使用),来进一步简化该泛型函数。

声明一个类型约束

在本节中,你将把前面定义的类型约束移动到它自己的接口中,这样你就可以在多个地方重用它。以这种方式声明的类型约束有助于简化代码,例如当类型约束很复杂时。

你可以将类型约束声明为接口。类型约束允许使用实现该接口的任何类型。例如,如果用三个方法声明类型约束的接口,然后将其与泛型函数中的类型参数一起使用,那么用于调用该函数的类型实参必须具有所有这些方法。

类型约束的接口也可以引用特定的类型,正如你将在本节中看到的那样。

编写代码

1 就在main函数之上,在import语句之后立即粘贴以下代码来声明一个类型约束。

type Number interface {
    int64 | float64
}

在此代码中,你:

  • 声明要用作类型约束的Number接口类型。
  • 在接口内声明int64float64的并集。

从本质上讲,你正在将int64float64的并集从函数声明移动到一个新的类型约束中。这样,当你想将类型参数约束为int64float64时,可以使用此Number类型约束,而不是写int64|float64

2 在已有的函数下面,粘贴以下泛型函数SumNumbers的代码。

// SumNumbers计算映射m的值的总和。m的值的类型可以是整数或浮点数。
func SumNumbers[K comparable, V Number](m map[K]V) V {
    var s V
    for _, v := range m {
        s += v
    }
    return s
}

在此代码中,你:

使用新的接口类型而不是类型并集作为类型约束来声明一个泛型函数。和以前一样,你将类型参数用于普通函数参数和返回值的类型。

3 在main.go中,在你已经拥有的代码下面,粘贴以下代码。

fmt.Printf("Generic Sums with Constraint: %v and %v\n",
    SumNumbers(ints),
    SumNumbers(floats))

在此代码中,你:

对每个映射调用SumNumbers函数,打印输出每个映射的值的总和。

如前一节所述,在对该泛型函数的调用中省略了类型参数(方括号中的类型名称)。Go编译器可以从函数实参中推断出类型参数。

运行代码

在包含main.go的目录的命令行运行代码:

$ go run .
Non-Generic Sums: 46 and 62.97
Generic Sums: 46 and 62.97
Generic Sums, type parameters inferred: 46 and 62.97
Generic Sums with Constraint: 46 and 62.97

结论

你刚刚学习了Go中的泛型。

建议学习的下一个主题:

  • Go Tour手把手教你入门Go语言基础知识。
  • 你可以在Effective Go中找到有用的Go语言最佳实践。

完整的代码

package main

import "fmt"

type Number interface {
    int64 | float64
}

func main() {
    // 初始化字符串键整型数值的一个map
    ints := map[string]int64{
        "first": 34,
        "second": 12,
    }

    // 初始化字符串键浮点数值的一个map
    floats := map[string]float64{
        "first": 35.98,
        "second": 26.99,
    }

    fmt.Printf("Non-Generic Sums: %v and %v\n",
        SumInts(ints),
        SumFloats(floats))

    fmt.Printf("Generic Sums: %v and %v\n",
        SumIntsOrFloats[string, int64](ints),
        SumIntsOrFloats[string, float64](floats))

    fmt.Printf("Generic Sums, type parameters inferred: %v and %v\n",
        SumIntsOrFloats(ints),
        SumIntsOrFloats(floats))

    fmt.Printf("Generic Sums with Constraint: %v and %v\n",
        SumNumbers(ints),
        SumNumbers(floats))
}

// SumInts 函数把映射m的所有int64值加起来。
func SumInts(m map[string]int64) int64 {
    var s int64
    for _, v := range m {
        s += v
    }
    return s
}

// SumFloats 函数把映射m的所有float64值加起来。
func SumFloats(m map[string]float64) float64 {
    var s float64
    for _, v := range m {
        s += v
    }
    return s
}

// SumIntsOrFloats 泛型函数SumIntsOrFloats计算映射m的值的总和,同时支持int64或float64类型的值。
func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
    var s V
    for _, v := range m {
        s += v
    }
    return s
}

// SumNumbers 泛型函数SumNumbers计算映射m的值的总和。m的值的类型可以是整数或浮点数。使用新的接口类型而不是类型并集作为类型约束
func SumNumbers[K comparable, V Number](m map[K]V) V {
    var s V
    for _, v := range m {
        s += v
    }
    return s
}