• 简体中文
  • 泛型入门

    本教程介绍了 Go 中泛型的基础知识。使用泛型,你可以声明和使用为调用代码提供的一组类型中的任何类型工作的函数或类型。

    在本教程中,你将声明两个简单的非泛型函数,然后在单个泛型函数中捕获相同的逻辑。

    你将经历以下部分:

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

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

    先决条件

    • 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 命令,给它你新代码的模块路径。

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

      注意: 对于生产代码,你将指定一个更具体符合你自己需求的模块路径。有关更多信息,请参阅 管理依赖项

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

    添加非泛型函数

    在此步骤中,你将添加两个函数,每个函数将映射的值加在一起并返回总和。

    你声明两个函数而不是一个,因为你正在处理两种不同类型的映射:一个存储 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
      }

      在这段代码中,你:

      • 声明两个函数来将映射的值加在一起并返回总和。
        • SumFloats 接受一个 stringfloat64 值的映射。
        • SumInts 接受一个 stringint64 值的映射。
    4. main.go 的顶部,包声明下方,粘贴以下 main 函数以初始化两个映射,并在调用你在上一步中声明的函数时使用它们作为参数。

      func main() {
      	// 初始化整数映射
      	ints := map[string]int64{
      		"first":  34,
      		"second": 12,
      	}
      
      	// 初始化浮点数映射
      	floats := map[string]float64{
      		"first":  35.98,
      		"second": 26.99,
      	}
      
      	fmt.Printf("非泛型求和: %v 和 %v\n",
      		SumInts(ints),
      		SumFloats(floats))
      }

      在这段代码中,你:

      • 初始化一个 float64 值的映射和一个 int64 值的映射,每个映射有两个条目。
      • 调用你之前声明的两个函数来找到每个映射值的总和。
      • 打印结果。
    5. main.go 的顶部,包声明下方,导入你刚编写代码所需的包。

      代码的前几行应如下所示:

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

    运行代码

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

    $ go run .
    非泛型求和: 46 和 62.97

    使用泛型,你可以在这里写一个函数而不是两个。接下来,你将添加一个适用于包含整数或浮点值的映射的单个泛型函数。

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

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

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

    为此,你将编写一个函数,该函数声明类型参数以及普通函数参数。这些类型参数使函数泛型,使其能够处理不同类型的参数。你将使用类型参数和普通函数参数调用该函数。

    每个类型参数都有一个类型约束,充当类型参数的元类型。每个类型约束指定调用代码可以为相应类型参数使用的允许类型参数。

    虽然类型参数的约束通常表示一组类型,但在编译时,类型参数代表调用代码提供的类型参数所代表的单一类型。如果调用代码提供的类型参数的类型不被类型参数的约束允许,代码将无法编译。

    请记住,类型参数必须支持泛型代码在其上执行的所有操作。例如,如果你的函数代码尝试对包含数值类型的类型参数执行 string 操作(如索引),代码将无法编译。

    在你即将编写的代码中,你将使用一个允许整数或浮点类型的约束。

    编写代码

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

      // SumIntsOrFloats 对映射 m 的值求和。它支持 map 值的类型 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
      }

      在这段代码中,你:

      • 声明一个带有两个类型参数(在方括号内)KVSumIntsOrFloats 函数,以及一个使用类型参数的参数 m,类型为 map[K]V。该函数返回一个类型为 V 的值。
      • K 类型参数指定 comparable 类型约束。comparable 约束是 Go 中预声明的,专门用于此类情况。它允许任何类型的值用作比较运算符 ==!= 的操作数。Go 要求映射键是可比较的。因此,将 K 声明为 comparable 是必要的,以便你可以在映射变量中使用 K 作为键。它还确保调用代码使用允许的映射键类型。
      • V 类型参数指定一个约束,即两种类型的联合:int64float64。使用 | 指定两种类型的联合,这意味着此约束允许任一类型。编译器将允许任一类型作为调用代码中的参数。
      • 指定 m 参数的类型为 map[K]V,其中 KV 是为类型参数指定的类型。注意,我们知道 map[K]V 是一个有效的映射类型,因为 K 是一个可比较的类型。如果我们没有声明 Kcomparable,编译器将拒绝引用 map[K]V
    2. main.go 中,在你已有的代码下方,粘贴以下代码。

      fmt.Printf("泛型求和: %v 和 %v\n",
      	SumIntsOrFloats[string, int64](ints),
      	SumIntsOrFloats[string, float64](floats))

      在这段代码中,你:

      • 调用你刚刚声明的泛型函数,传递你创建的每个映射。

      • 指定类型参数 —— 方括号中的类型名称 —— 以明确你调用的函数中应替换类型参数的类型。

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

      • 打印函数返回的总和。

    运行代码

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

    $ go run .
    非泛型求和: 46 和 62.97
    泛型求和: 46 和 62.97

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

    在你编写的泛型函数调用中,你指定了类型参数,告诉编译器在函数的泛型参数位置使用什么类型。正如你将在下一节中看到的,在许多情况下,你可以省略这些类型参数,因为编译器可以推断它们。

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

    在本节中,你将添加一个泛型函数调用的修改版本,通过省略类型参数来简化调用代码。在这种情况下,类型参数不是必需的。

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

    请注意,这并不总是可能的。例如,如果你需要调用一个没有参数的泛型函数,你需要在函数调用中包含类型参数。

    编写代码

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

      fmt.Printf("泛型求和,类型参数推断: %v 和 %v\n",
      	SumIntsOrFloats(ints),
      	SumIntsOrFloats(floats))

      在这段代码中,你:

      • 调用泛型函数,省略类型参数。

    运行代码

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

    $ go run .
    非泛型求和: 46 和 62.97
    泛型求和: 46 和 62.97
    泛型求和,类型参数推断: 46 和 62.97

    接下来,通过将整数和浮点数的联合捕获到一个可以在其他代码中重用的类型约束中,进一步简化函数。

    声明类型约束

    在最后一节中,你将把之前定义的约束移动到自己的接口中,以便在多个地方重用。以这种方式声明约束有助于简化代码,特别是当约束更复杂时。

    你将类型约束声明为接口。该约束允许任何实现该接口的类型。例如,如果你声明一个具有三个方法的类型约束接口,然后在泛型函数中使用它作为类型参数,调用该函数时使用的类型参数必须具有所有这些方法。

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

    编写代码

    1. main 上方,紧接在导入语句之后,粘贴以下代码以声明类型约束。

      type Number interface {
          int64 | float64
      }

      在这段代码中,你:

      • 声明 Number 接口类型用作类型约束。

      • 在接口内声明 int64float64 的联合。

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

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

      // SumNumbers 对映射 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("使用约束的泛型求和: %v 和 %v\n",
      	SumNumbers(ints),
      	SumNumbers(floats))

      在这段代码中,你:

      • 使用每个映射调用 SumNumbers,打印每个值的总和。

        与前一节一样,你在对泛型函数的调用中省略了类型参数(方括号中的类型名称)。Go 编译器可以从其他参数中推断类型参数。

    运行代码

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

    $ go run .
    非泛型求和: 46 和 62.97
    泛型求和: 46 和 62.97
    泛型求和,类型参数推断: 46 和 62.97
    使用约束的泛型求和: 46 和 62.97

    结论

    干得好!你刚刚介绍了 Go 中的泛型。

    建议的后续主题:

    完整代码

    你可以在 Go playground 中运行此程序。在 playground 上,只需点击 运行 按钮。

    package main
    
    import "fmt"
    
    type Number interface {
    	int64 | float64
    }
    
    func main() {
    	// 初始化整数映射
    	ints := map[string]int64{
    		"first": 34,
    		"second": 12,
    	}
    
    	// 初始化浮点数映射
    	floats := map[string]float64{
    		"first": 35.98,
    		"second": 26.99,
    	}
    
    	fmt.Printf("非泛型求和: %v 和 %v\n",
    		SumInts(ints),
    		SumFloats(floats))
    
    	fmt.Printf("泛型求和: %v 和 %v\n",
    		SumIntsOrFloats[string, int64](ints),
    		SumIntsOrFloats[string, float64](floats))
    
    	fmt.Printf("泛型求和,类型参数推断: %v 和 %v\n",
    		SumIntsOrFloats(ints),
    		SumIntsOrFloats(floats))
    
    	fmt.Printf("使用约束的泛型求和: %v 和 %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 对映射 m 的值求和。它支持浮点数和整数作为映射值。
    func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
    	var s V
    	for _, v := range m {
    		s += v
    	}
    	return s
    }
    
    // SumNumbers 对映射 m 的值求和。它支持整数和浮点数作为映射值。
    func SumNumbers[K comparable, V Number](m map[K]V) V {
    	var s V
    	for _, v := range m {
    		s += v
    	}
    	return s
    }