[官文翻译]golang指南:开始使用泛型

356 阅读9分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第3天,点击查看活动详情


本文翻译自:
Tutorial: Getting started with generics - The Go Programming Language


指南:开始使用泛型

内容

该指南介绍了 go 中泛型的基础。使用泛型, 可以声明和使用函数或类型,它们编写用于使用任意类型的集合来调用代码。

在该指南中,你会声明两个非泛型函数,然后提取共同的逻辑到单个泛型函数中。

你会通过以下几部分来完成:

  1. 为代码创建文件夹。
  2. 添加非泛型函数。
  3. 添加泛型函数处理多种类型。
  4. 调用泛型函数时移除类型参数。
  5. 声明类型约束。

注意:  其它指南查看 教程

注意:  如果你喜欢,可以使用 “Go dev branch” 模式的 Go playground 代替来编辑和运行程序。

前提

  • 安装 Go 1.18 或更高版本。  安装说明查看 Installing Go
  • 编辑代码的工具。  平时使用的任何文本编辑器都可以。
  • 一个命令终端  Go 在 Liunx 或 Mac 的任何终端上在 Windows 上的 PowerShell 或 cmd 上都能正常使用。

为代码创建文件夹

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

  1. 打开终端命令行,进入到 home 目录。

    在 Linux 或 Mac 上:

    $ cd
    

    在 Windows 上:

    C:> cd %HOMEPATH%
    

    下面的内容会显示 $ 作为提示符。你使用的命令在 Windows 上也能正常工作。

  2. 在命令行,为调用泛型的代码创建目录。

    $ mkdir generics
    $ cd generics
    
  3. 创建模块管理代码。

    运行 go mod init 命令,设置为新代码的模块路径。

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

    注意:  对于生产环境的代码,你会指定一个更符合自己需求的模块路径。更多信息查看 Managing dependencies

接下来,你会添加一些使用 Map 的简单代码。

添加非泛型函数

该步骤中,你会添加两个函数,它们都会把 Map 中的值相加,然后返回求和的结果。

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

编写代码

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

  2. 进入 main.go ,在文件的最上方,粘贴下面的包声明。

    package main
    

    一个独立的程序(相对的是库)总是在 main 包中。

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

    // SumInts 将 m 中的值加在一起。
    func SumInts(m map[string]int64) int64 {
        var s int64
        for _, v := range m {
            s += v
        }
        return s
    }
    
    // SumFloats 将 m 中的值加在一起。
    func SumFloats(m map[string]float64) float64 {
        var s float64
        for _, v := range m {
            s += v
        }
        return s
    }
    

    在该代码中:

    • 声明了两个函数将 map 中的值加在一起并返回和。

      • SumFloats 接收 string - float64 的键值 map 。
      • SumInts 接收 string - int64 的键值 map 。
  4. 在 main.go 的最上面,包声明以下,粘贴以下 main 函数来初始化两个 map ,然后作为调用处理阶段声明的函数的参数。

    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))
    }
    

    在该代码中:

    • 初始化一个 float64 值的 map 和 一个 int64 值的 map ,各有两个实体。
    • 调用前面声明的两个函数来计算每个 map 中值的和。
    • 打印结果。
  5. 在 main.go 的顶部附近,紧靠着包声明下方,导入支持刚才编写的代码所需的库。

    代码的前几行看上去应该如下:

    package main
    
    import "fmt"
    
  6. 保存 main.go 。

运行代码

在包含 main.go 的目录的命令行,执行代码。

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

使用泛型,在这里可以写一个函数代替两个。接着,你会添加一个泛型函数,它可用于包含整数值或浮点数值的 map 。

添加泛型函数处理多种类型

在该部分,你会添加单个泛型函数,它能接收包含整数值或浮点数值的 map ,可以高效地用一个函数替换上面刚刚编写的两个函数。

要支持其中某个类型的值,单个函数需要一个方式来声明它支持的类型。调用代码,另一方面,会需要明确是调用带整数值的 map 还是调用带浮点数值的 map 。

要支持这一点,你会编写一个函数,它还声明了 类型参数 作为原始的函数参数的补充。这些 类型参数 使函数泛型化,使其可以处理不同类型的参数。你会使用 类型参数 和原来的函数参数来调用函数。

每种类型参数都有一个 <类型约束> ,它作为类型参数的一种元类型。每个类型约束指定了允许的类型参数,这样调用的代码可以用于对应的类型参数。

一个类型参数的约束通常代表类型的集合,但在编译时类型参数代表了单个类型 - 作为调用代码时提供的类型参数。如果类型参数的类型不符合类型参数的约束,代码不会被编译。

请记住,一个类型参数必须支持对其执行的泛型代码的所有操作。例如,如果函数的代码尝试对约束包含数值类型的参数进行 string 操作(例如取索引),代码不会被编译。

在你要编写的代码中,你可以使用允许整数和浮点数类型的约束。

编写代码

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

    // SumIntsOrFloats 对 Map m 时的值求和。它同时支持 int64 和 float64 作为用于 map 值的类型。
    func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
        var s V
        for _, v := range m {
            s += v
        }
        return s
    }
    

    在该代码中:

    • 声明 SumIntsOrFloats 函数,它有两个类型参数(在中括号中), K 和 V , 一个参数使用类型参数,类型 map[K]Vm 。该函数返回类型 V 的值。
    • K 类型参数指定类型约束 comparable 。专门用于以下几种情况,在 go 中 comparable 已经预定义。它允许使用值作为比较操作符 ==!= 的运算对象的任意类型。Go 需要 Map 的键值是可比较的。所以声明 K 作为 comparable 是必要的,这样你可以使用 K 作为 map 变量的键。它也确保调用的代码能为 map 键使用允许的类型。
    • V 类型参数指定两种类型合集的约束:int64 和 float64 。 使用 | 指定两种类型的合集,这意味着该约束允许其中的任一种类型。在调用的代码中其中的任意一种类型都被编译器允许作为一个参数。
    • 指定 map[K]V 类型的参数 m ,这里 K 和 V 是已经为类型参数指定的类型。注意我们知道 map[K]V 是有效的映射类型,因为是可比较的类型。如果 K 还没被定义为可比较的,编译器会拒绝对 map[K]V 的引用。
  2. 在 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

接下来,你会进一步简化函数,通过将整数和浮点数整合到可重用(比如在其它代码中使用)的类型约束中。

声明类型约束

在最后的部分,你会将前面定义的约束移到它自己的接口中,这样你可以在多个地方重用。 用这种试声明约束可使代码简化、效率更高,比如当约束更复杂时。

声明一个 类型约束 作为接口。该约束允许任意类型实现该接口。 例如,如果定义了一个有三个方法的类型约束接口,之后可以在泛型函数的类型参数中使用它,类型参数调用的函数必须具有所有这些方法。

约束接口也可引用指定的类型,正在该部分看到的这样。

编写代码

  1. 在 main 函数的上面,紧接着 import 语句,粘贴以下代码定义一个类型约束。

    type Number interface {
        int64 | float64
    }
    

    在该代码中:

    • 定义了一个 Number 接口类型作为类型约束。

    • 在接口内部定义了 int64 和 float64 的合集。

      本质上,你将合集从函数的声明移到了一个新的类型约束中。那样,当你想约束一个类型参数为 int64 或 float64 时,就可以使用这个 Number 类型约束以代替将 int64 | float64 写出来。

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

    // SumNumbers 对 map m 中的值求和。它同时支持整数和浮点数作为 map 的值。
    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))
    

    在该代码中:

    • 用每个 map 调用 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 playground 中运行代码。 在 playground 中只需简单地点击 运行 按钮。

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 中的值求和。
func SumInts(m map[string]int64) int64 {
    var s int64
    for _, v := range m {
        s += v
    }
    return s
}

// SumFloats 将 m 中的值求和。
func SumFloats(m map[string]float64) float64 {
    var s float64
    for _, v := range m {
        s += v
    }
    return s
}

// SumIntsOrFloats 将 map 同中的值求和。它同时支持整数和浮点数作为 map 的值。
func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
    var s V
    for _, v := range m {
        s += v
    }
    return s
}

// SumNumbers 将 map 同中的值求和。它同时支持整数和浮点数作为 map 的值。
func SumNumbers[K comparable, V Number](m map[K]V) V {
    var s V
    for _, v := range m {
        s += v
    }
    return s
}