关于golang:官方教程Go泛型入门

60次阅读

共计 7486 个字符,预计需要花费 19 分钟才能阅读完成。

前言

本周 Go 官网重磅公布了 Go 1.18 beta 1 版本,正式反对泛型。作为 Go 语言诞生 12 年以来最大的性能改革,官网配套推出了一个十分粗疏的 Go 泛型入门基础教程,通俗易懂。

自己对 Go 官网教程在翻译的根底上做了一些表述上的优化,以飨读者。

教程内容

这个教程次要介绍 Go 泛型的基础知识。通过泛型,你能够申明和应用泛型函数,在调用函数的时候,容许应用不同类型的参数作为函数实参。

在这个教程里,咱们先申明 2 个简略的非泛型函数,而后在一个泛型函数里实现这 2 个函数的逻辑。

接下来通过以下几个局部来进行解说:

  1. 为你的代码创立一个目录
  2. 实现非泛型函数
  3. 实现一个泛型函数来解决不同类型
  4. 调用泛型函数的时候移除类型实参
  5. 申明类型限度(type constraint)

留神:对于 Go 的其它教程,大家能够参考 https://go.dev/doc/tutorial/。

留神:大家能够应用 Go playground 的 Go dev branch 模式来编写和运行你的泛型代码,地址 https://go.dev/play/?v=gotip。

筹备工作

  • 装置 Go 1.18 Beta 1 或者更新的版本。装置指引能够参考上面的介绍。
  • 有一个代码编辑工具。任何文本编辑器都能够。
  • 有一个命令行终端。Go 能够运行在 Linux,Mac 上的任何命令行终端,也能够运行在 Windows 的 PowerShell 或者 cmd 之上。

装置和应用 beta 版本

这个教程须要应用 Go 1.18 Beta 1 版本了的泛型性能。应用如下步骤,装置 beta 版本

  1. 应用上面的命令装置 beta 版本

    $ go install golang.org/dl/go1.18beta1@latest
  2. 运行如下命令来下载更新

    $ go1.18beta1 download
  3. 应用 beta 版本的 go 命令,不要去应用 release 版本的 go 命令

    你能够通过间接应用 go1.18beta1 命令或者给 go1.18beta1 起一个简略的别名

    • 间接应用 go1.18beta1 命令

      $ go1.18beta1 version
    • go1.18beta1 命令起一个别名

      $ alias go=go1.18beta1
      $ go version

    上面的教程都假如你曾经把 go1.18beta1 命令设置了别名go

为你的代码创立一个目录

首先创立一个目录用于寄存你写的代码。

  1. 关上一个命令行终端,切换到你的 home 目录

    • 在 Linux 或者 Mac 上执行如下命令 (Linux 或者 Mac 上只须要执行cd 就能够进入到 home 目录)

      cd
    • 在 Windows 上执行如下命令

      C:\> cd %HOMEPATH%
  2. 在命令行终端,创立一个名为 generics 的目录

    $ mkdir generics
    $ cd generics
  3. 创立一个 go module

    运行 go mod init 命令,来给你的我的项目设置 module 门路

    $ go mod init example/generics

    留神:对于生产代码,你能够依据我的项目理论状况来指定 module 门路,如果想理解更多,能够参考 https://go.dev/doc/modules/ma…。

接下来,咱们来应用 map 写一些简略的代码。

实现非泛型函数

在这个步骤,你要实现 2 个函数,每个函数都是把 map 里 <key, value> 对应的所有 value 相加,返回总和。

你须要申明 2 个函数,因为你要解决 2 种不同类型的 map,一个 map 存储的 value 是 int64 类型,一个 map 存储的 value 是 float64 类型。

代码实现

  1. 关上你的代码编辑器,在 generics 目录创立文件main.go,你的代码将实现在这个文件里。
  2. 进入main.go,在文件最结尾,写包申明

    package main

    一个独立的可执行程序总是申明在 package main 里,这点和库不一样。

  3. 在包申明的上面,写如下代码

    // SumInts adds together the values of m.
    func SumInts(m map[string]int64) int64 {
        var s int64
        for _, v := range m {s += v}
        return s
    }
    
    // SumFloats adds together the values of m.
    func SumFloats(m map[string]float64) float64 {
        var s float64
        for _, v := range m {s += v}
        return s
    }

    在下面的代码里,咱们定义了 2 个函数,用于计算 map 里 value 的总和

    • SumInts 计算 value 为 int64 类型的总和
    • SumFloats 计算 value 为 float64 类型的总和
  4. main.gopackage main申明上面,实现 main 函数,用于初始化 2 个 map,并把它们作为参数传递给咱们实现的 2 个函数。

    func main() {
    // Initialize a map for the integer values
    ints := map[string]int64{
        "first": 34,
        "second": 12,
    }
    
    // Initialize a map for the float values
    floats := map[string]float64{
        "first": 35.98,
        "second": 26.99,
    }
    
    fmt.Printf("Non-Generic Sums: %v and %v\n",
        SumInts(ints),
        SumFloats(floats))
    }

    在这段代码里,咱们做了如下几个事件

    • 初始化 2 个 map,每个 map 都有 2 个记录
    • 调用 SumIntsSumFloats来别离计算 2 个 map 的 value 的总和
    • 打印后果
  5. main.gopackage main上面,增加 import fmt,下面代码里调用的打印函数须要fmt 这个 package。
  6. 保留main.go

代码运行

main.go 所在目录下,运行如下命令

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

应用泛型,咱们只须要实现 1 个函数就能够计算 2 个不同类型 map 的 value 总和。接下来,咱们会展现如何实现这个泛型函数。

实现一个泛型函数来解决不同类型

这个章节,咱们会实现一个泛型函数,该泛型函数既能够接管 value 为 int 类型的 map 作为参数,也能够接管 value 为 float 类型的 map 作为参数,这样咱们就不必为不同类型的 map 别离实现各自的函数了。

函数要反对这种泛型行为,须要有 2 个 前提条件

  1. 对于函数而言,须要一种形式来申明这个函数到底反对哪些类型的参数
  2. 对于函数调用方而言,须要一种形式来指定传给函数的到底是 int 类型的 map 还是 float 类型的 map

为了满足以上前提条件:

  1. 在申明函数的时候,除了须要像一般函数一样增加函数参数之外,还要申明 类型参数(type parameters)。这些类型参数让函数可能实现泛型行为,让函数能够解决不同类型的参数。
  2. 在函数调用的时候,除了须要像一般函数调用一样传递实参之外,还须要指定泛型函数的类型参数对应的 类型实参(type arguments)。

每个类型参数都有一个 类型限度(type constraint),类型限度就好比类型参数的 meta 类型,每个类型限度会指明函数调用时该类型参数容许的类型实参。

只管一个类型参数的类型限度是一系列类型的汇合,然而在编译期,类型参数只会示意一种具体的类型,也就是函数调用方理论应用的类型实参。如果类型实参的类型不满足类型参数的类型限度,编译就会失败。

记住:一个类型参数肯定要反对代码里对该类型所做的所有操作。例如你的函数代码试图对某个类型参数执行 string 操作,比方依照下标索引取值,然而这个类型参数的类型限度包含了数字类型,那代码就会编译失败。

在接下来的代码里,咱们会应用类型限度来容许 value 为 int 类型和 float 类型的 map 作为函数的入参。

代码实现

  1. 在下面实现的 SumIntsSumFloats前面,增加如下函数

    // SumIntsOrFloats sums the values of map m. It supports both int64 and float64
    // as types for map values.
    func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
        var s V
        for _, v := range m {s += v}
        return s
    }

    在这段代码里,咱们做了如下事件:

    • 申明函数 SumIntsOrFloats,它有 2 个类型参数KV(在 [] 外面),一个函数参数m,类型是map[K]V,返回返回类型是V
    • 类型参数 K 的类型限度是 comparablecomparable 限度是 Go 里预申明的。它能够承受任何能做 ==!=操作的类型。Go 语言里 map 的 key 必须是 comparable 的,因而类型参数 K 的类型限度应用 comparable 是很有必要的,这也能够确保调用方应用了非法的类型作为 map 的 key。
    • 类型参数 V 的类型限度是 int64float64的并集,| 示意取并集,也就是 int64float64的任一个都能够满足该类型限度,能够作为函数调用方应用的类型实参。
    • 函数参数 m 的类型是 map[K]V。咱们晓得map[K]V 是一个非法的 map 类型,因为 K 是一个 comparable 的类型。如果咱们不申明 K 为 comparable,那编译器会回绝对 map[K]V 的援用。
  2. main.go 已有代码前面,增加如下代码

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

    在这段代码里:

    • 调用了下面定义的泛型函数,传递了 2 种类型的 map 作为函数的实参。
    • 函数调用时指明了类型实参 (方括号[] 外面的类型名称),用于替换调用的函数的类型实参。

      在接下来的内容里,你会常常看到调用函数时,会省略掉类型实参,因为 Go 通常 (不是肯定) 能够依据你的代码推断出类型实参。

    • 打印函数的返回值。

代码运行

main.go 所在目录下,运行如下命令:

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

编译器会主动把函数里的类型参数替换函数调用里指定的类型实参,在很多场景里,咱们能够疏忽掉这些类型实参,因为编译器能够进行主动推导。

调用泛型函数的时候移除类型实参

在这个章节,咱们会增加一个批改版本的泛型函数调用,通过移除函数调用时的类型实参,让函数调用更为简洁。

咱们在函数调用时能够移除类型实参是因为编译器能够主动推导进去,编译器是依据函数调用时传的函数实参类型做的推导判断。

留神 类型实参的主动推导并不是永远可行的 。比方,你调用的泛型函数没有形参,不须要传递实参,那编译器就不能依据实参主动推导,须要在函数调用时在方括号[] 里显示指定类型实参。

代码实现

  • 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

接下来,咱们会进一步简化泛型函数。咱们能够把 int 和 float 类型的并集做成一个能够复用的类型限度。

申明类型限度(type constraint)

在最初这个章节,咱们会把泛型函数里的类型限度以接口 (interface) 的模式做定义,这样类型限度就能够在很多中央被复用。申明类型限度能够帮忙精简代码,特地是在类型限度很简单的场景下。

咱们能够申明一个类型限度 (type constraint) 为接口 (interface) 类型。这样的类型限度能够容许任何实现了该接口的类型作为泛型函数的类型实参。例如,你申明了一个有 3 个办法的类型限度接口,而后把这个类型限度接口作用于泛型函数的类型限度,那函数调用时的类型实参必须要实现了接口里的所有办法。

类型限度接口也能够指代特定类型,在上面大家能够看到具体应用。

代码实现

  1. main 函数下面,import语句上面,增加如下代码用于申明一个类型限度

    type Number interface {int64 | float64}

    在这段代码里,咱们

    • 申明了一个名为 Number 的接口类型用于类型限度
    • 在接口定义里,申明了 int64 和 float64 的并集

    咱们把本来来函数申明里的 int64 和 float64 的并集革新成了一个新的类型限度接口 Number,当咱们须要限度类型参数为 int64 或 float64 时,就能够应用 Number 这个类型限度来代替 int64 | float64 的写法。

  2. 在已有的函数上面,增加一个新的 SumNumbers 泛型函数

    // SumNumbers sums the values of map m. Its supports both integers
    // and floats as map values.
    func SumNumbers[K comparable, V Number](m map[K]V) V {
        var s V
        for _, v := range m {s += v}
        return s
    }

    在这段代码里

    • 咱们定义了一个新的泛型函数,函数逻辑和之前定义过的泛型函数 SumIntsOrFloats 齐全一样,只不过对于类型参数 V,咱们应用了 Number 来作为类型限度。和之前一样,咱们把类型参数用于函数形参和函数返回类型。
  3. main.go 已有代码前面,增加如下代码

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

    在这段代码里

    • 咱们对 2 个 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 泛型的入门常识。

如果你想持续试验,能够扩大 Number 接口,来反对更多的数字类型。

倡议接下来理解的主题:

  • Go Tour:https://go.dev/tour/welcome/1,十分棒的 Go 根底入门指引,一步一步教会你入门 Go。
  • 能够在 Effective Go:https://go.dev/doc/effective_go 和 How to Write Go code:https://go.dev/doc/code 找到写 Go 代码的最佳实际。

残缺代码

// example6.go
package main

import "fmt"

type Number interface {int64 | float64}

func main() {
    // Initialize a map for the integer values
    ints := map[string]int64{
        "first": 34,
        "second": 12,
    }

    // Initialize a map for the float values
    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 adds together the values of m.
func SumInts(m map[string]int64) int64 {
    var s int64
    for _, v := range m {s += v}
    return s
}

// SumFloats adds together the values of m.
func SumFloats(m map[string]float64) float64 {
    var s float64
    for _, v := range m {s += v}
    return s
}

// SumIntsOrFloats sums the values of map m. It supports both floats and integers
// as map values.
func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
    var s V
    for _, v := range m {s += v}
    return s
}

// SumNumbers sums the values of map m. Its supports both integers
// and floats as map values.
func SumNumbers[K comparable, V Number](m map[K]V) V {
    var s V
    for _, v := range m {s += v}
    return s
}

开源地址

文档和代码开源地址:https://github.com/jincheng9/…

也欢送大家关注公众号:coding 进阶,学习更多 Go、微服务和云原生架构相干常识。

References

  • https://go.dev/doc/tutorial/g…

正文完
 0