14

Go中的泛型——如何使用以及它们是怎么工作的

 4 years ago
source link: http://blog.studygolang.com/2020/04/go-generics/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Go 中的泛型已经接近成为现实。本文讲述的是泛型的最新设计,以及如何自己尝试泛型。

generics-1.png

Go 由于不支持泛型而臭名昭著,但最近,泛型已接近成为现实。Go 团队实施了一个看起来比较稳定的设计草案,并且正以源到源翻译器原型的形式获得关注。本文讲述的是泛型的最新设计,以及如何自己尝试泛型。

例子

FIFO Stack

假设你要创建一个先进先出堆栈。没有泛型,你可能会这样实现:

type Stack []interface{}

func (s Stack) Peek() interface{} {
    return s[len(s)-1]
}

func (s *Stack) Pop() {
    *s = (*s)[:len(*s)-1]
}

func (s *Stack) Push(value interface{}) {
    *s = append(*s, value)
}

但是,这里存在一个问题:每当你 Peek 项时,都必须使用类型断言将其从 interface{} 转换为你需要的类型。如果你的堆栈是 *MyObject 的堆栈,则意味着很多 s.Peek().(*MyObject) 这样的代码。这不仅让人眼花缭乱,而且还可能引发错误。比如忘记 * 怎么办?或者如果您输入错误的类型怎么办?s.Push(MyObject{})` 可以顺利编译,而且你可能不会发现到自己的错误,直到它影响到你的整个服务为止。

通常,使用 interface{} 是相对危险的。使用更多受限制的类型总是更安全,因为可以在编译时而不是运行时发现问题。

泛型通过允许类型具有类型参数来解决此问题:

type Stack(type T) []T

func (s Stack(T)) Peek() T {
    return s[len(s)-1]
}

func (s *Stack(T)) Pop() {
    *s = (*s)[:len(*s)-1]
}

func (s *Stack(T)) Push(value T) {
    *s = append(*s, value)
}

这会向 Stack 添加一个类型参数,从而完全不需要 interface{} 。现在,当你使用 Peek() 时,返回的值已经是原始类型,并且没有机会返回错误的值类型。这种方式更安全,更容易使用。(译注:就是看起来更丑陋,^-^)

此外,泛型代码通常更易于编译器优化,从而获得更好的性能(以二进制大小为代价)。如果我们对上面的非泛型代码和泛型代码进行基准测试,我们可以看到区别:

type MyObject struct {
    X int
}

var sink MyObject

func BenchmarkGo1(b *testing.B) {
    for i := 0; i < b.N; i++ {
        var s Stack
        s.Push(MyObject{})
        s.Push(MyObject{})
        s.Pop()
        sink = s.Peek().(MyObject)
    }
}

func BenchmarkGo2(b *testing.B) {
    for i := 0; i < b.N; i++ {
        var s Stack(MyObject)
        s.Push(MyObject{})
        s.Push(MyObject{})
        s.Pop()
        sink = s.Peek()
    }
}

结果:

BenchmarkGo1
BenchmarkGo1-16     12837528            87.0 ns/op        48 B/op          2 allocs/op
BenchmarkGo2
BenchmarkGo2-16     28406479            41.9 ns/op        24 B/op          2 allocs/op

在这种情况下,我们分配更少的内存,同时泛型的速度是非泛型的两倍。

合约(Contracts)

上面的堆栈示例适用于任何类型。但是,在许多情况下,你需要编写仅适用于具有某些特征的类型的代码。例如,你可能希望堆栈要求类型实现 String() 函数。这就是 Contracts :

contract stringer(T) {
    T String() string
}

type Stack(type T stringer) []T

// Now we can use the String method of T:
func (s Stack(T)) String() string {
    ret := ""
    for _, v := range s {
        if ret != "" {
            ret += ", "
        }
        ret += v.String()
    }
    return ret
}

更多示例

以上示例仅涵盖了泛型的基础知识。你还可以在函数中添加类型参数,并在合约(Contracts)中添加特定类型。

有关更多示例,你可以从两个地方获得:

设计草案

设计草案包含更详细的描述以及更多示例:

https://go.googlesource.com/proposal/+/4a54a00950b56dd0096482d0edae46969d7432a6/design/go2draft-contracts.md ,如果访问不了,可以看我备份的: https://github.com/polaris1119/go_dynamic_docs/blob/master/go2draft-contracts.md

实现原型的 CL

原型 CL 也有几个示例。查找以“ .go2”结尾的文件:

https://go-review.googlesource.com/c/go/+/187317

如何尝试泛型?

使用 WebAssembly Playground

到目前为止,尝试泛型的最快,最简单的方法是通过 WebAssembly Playground 。它使用 WASM 构建的源代码到源代码翻译器原型在你的浏览器中直接运行 Go 代码。但这存在一些限制(请参见 https://github.com/ccbrown/wasm-go-playground )。

编译 CL

上面引用的 CL 包含一个源到源转换器的实现,该转换器可用于将泛型代码编译为可以由 Go 的当前版本编译的代码。它将泛型代码(“多态”代码)称为Go 2代码,将非多态代码称为 Go 1 代码,但是根据实现的细节,泛型可能会成为 Go 1 版本而不是 Go 2 版本的一部分。

它还添加了一个 “go2go” 命令,可用于从 CLI 转换代码。

你可以按照 Go 的从源代码安装 Go 指令来编译 CL。当你到达可选的 “Switch to the master branch” 步骤时,请 用 checkout CL 代替:

git fetch "https://go.googlesource.com/go" refs/changes/17/187317/14 && git checkout FETCH_HEAD

请注意,这将检出补丁集 14,这是撰写本文时的最新补丁集。转到 CL 并找到“下载”按钮以获取最新补丁集的签出命令。

编译 CL 之后,可以使用 go/* 包编写用于使用泛型的自定义工具,或者可以仅使用 go2go 命令行工具:

go tool go2go translate mygenericcode.go2

原文链接:https://blog.tempus-ex.com/generics-in-go-how-they-work-and-how-to-play-with-them/

作者: Chris Brown

日期:2020-04-08

翻译:polaris

欢迎关注我的公众号:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK