golang 泛型_泛型是Go的泛型

golang 泛型

About three years ago, I was working on a pull request approval manager for GitHub written in Go called Checks-Out. While building out the integration layer, I came across a situation where the straightforward approach in Go was overly repetitive. The ideal solution would have been to use generics, but they weren’t available.

大约三年前,我正在为Go编写一个名为Checks-Out的 GitHub的请求请求批准管理器。 在构建集成层时,我遇到了一种情况,即Go中简单的方法过于重复。 理想的解决方案是使用泛型,但它们不可用。

However, I figured out that in many cases, you can use closures to pass values. I described my experience with this problem, and my solution in a blog post and a talk, both called Closures are the Generics of Go.

但是,我发现在许多情况下,可以使用闭包来传递值。 我在博客文章演讲中描述了我在此问题上的经验,并提出了解决方案,它们都称为Closures是Go的泛型。

Things are about to change. The Go team has released a blog post and a draft spec that describe a potential implementation of generics for Go. The draft proposal uses parameterized types to add most of the generics functionality that people have requested, without changing the fundamental character of Go.

事情将要改变。 Go团队发布了博客文章规范草案 ,描述了Go泛型的潜在实现。 提案草案使用参数化类型来添加人们所要求的大多数泛型功能,而不会改变Go的基本特征。

在Go语言中引入泛型 (Introducing Generics in Go)

Go is an intentionally small language whose design favors good tooling and readability over features. The Go team also values backwards compatibility; old code should continue to work so that it can be maintained for years. The generics draft was written with these goals in mind.

Go是一种有意使用的小型语言,其设计偏向于使用良好的工具和易读性而非功能。 Go团队还重视向后兼容性。 旧代码应继续工作,以便可以保留多年。 编写通用专利草案时就牢记了这些目标。

We’ll start our look at Go generics by trying out the simplest case: a user-defined container type. We’ll use a linked list as our sample container type. Here’s what it looks like to write one in Go today:

我们将通过尝试最简单的情况开始研究Go泛型: 用户定义的容器类型 。 我们将使用链接列表作为示例容器类型。 这是今天在Go中编写一个代码的样子:

type LinkedList struct {
value interface{}
next *LinkedList
}

And here’s what that looks like when using generics:

这是使用泛型时的样子:

type LinkedList[type T] struct {
value T
next *LinkedList[T]
}

It takes three small changes to change this type into a generic type:

将此类型更改为通用类型需要进行三处小的更改:

  1. Place [type T] after the type name and before the struct literal. T is the name we'll use as the placeholder for whatever type is supplied when our linked list is used. Many other languages use T as the placeholder type name for a generic type, and it's likely that Go will adopt the same custom. If you need to refer to additional types within the same type or function, use other capital letters; we'll see that in a bit.

    [type T]放在类型名称之后和struct文字之前。 T是我们使用链表时将提供的任何类型的占位符。 许多其他语言使用T作为泛型类型的占位符类型名称,Go可能会采用相同的自定义。 如果需要在同一类型或函数中引用其他类型,请使用其他大写字母;否则,请使用大写字母。 我们稍后会看到。

  2. Use T for the value field's type instead of interface{}.

    使用T作为value字段的类型,而不是interface{}

  3. Change the next pointer’s type from *LinkedList to *LinkedList[T]. When using a generic type, you must provide the type parameters. Leaving them out is a compile-time error.

    next指针的类型从*LinkedList更改为*LinkedList[T] 。 使用泛型类型时,必须提供类型参数。 忽略它们是编译时错误。

Let’s write some methods to work with our generic type. We’ll start with a method to find the length of our linked list:

让我们写一些方法来处理我们的泛型类型。 我们将从找到链表长度的方法开始:

func (ll *LinkedList[T]) Len() int {
count := 0
for node := ll; node != nil; node = node.next {
count++
}
return count
}

The method receiver uses *LinkedList[T] instead of *LinkedList, but the rest of the code is identical to the code that you'd write if you weren't using generics. Let's write a method that does refer to the parameterized type:

方法接收器使用*LinkedList[T]而不是*LinkedList ,但是其余代码与如果不使用泛型时编写的代码相同。 让我们编写一个确实引用参数化类型的方法:

func (ll *LinkedList[T]) InsertAt(pos int, value T) *LinkedList[T] {
if ll == nil || pos <= 0 {
return &LinkedList[T]{
value: value,
next: ll,
}
}
ll.next = ll.next.InsertAt(pos-1, value)
return ll
}

This method takes in a parameter of type [T]

此方法采用[T]类型的参数

(This method isn’t the most efficient way to insert into a linked list, but it is short enough to make a good example. Also note that it is safe; if you pass 0 or a negative number for the insertion index, it will prepend to the linked list and if you pass a number greater than the length, it will simply append.)

(此方法不是插入链表中最有效的方法,但是它足够简短,可以成为一个很好的例子。另外请注意,这是安全的;如果为插入索引传递0或负数,它将放在链接列表的前面,如果您传递的数字大于长度,那么它将简单地追加。)

Here are a few additional methods that are useful for our linked list:

以下是一些对我们的链表有用的其他方法:

func (ll *LinkedList[T]) Append(value T) *LinkedList[T] {
return ll.InsertAt(ll.Len(), value)
}func (ll *LinkedList[T]) String() string {
if ll == nil {
return "nil"
}
return fmt.Sprintf("%v->%v", ll.value, ll.next.String())
}

And now that we have some useful methods on our generic type, let’s try it out:

现在,我们在通用类型上有了一些有用的方法,让我们尝试一下:

var head *LinkedList[string]
head = head.Append("Hello")
fmt.Println(head.String())
fmt.Println(head.Len())
head = head.Append("Hola")
head = head.Append("हैलो")
head = head.Append("こんにちは")
head = head.Append("你好")
fmt.Println(head.String())
fmt.Println(head.Len())

(We don’t need to call String explicitly when passing a value to fmt.Println, but I wanted to make it explicit. See https://tour.golang.org/methods/17 for more information.)

(将值传递给fmt.Println ,我们不需要显式调用String ,但是我想使其显式。有关更多信息,请参见https://tour.golang.org/methods/17 。)

This looks exactly like existing Go code, with only one change: when declaring a variable of type *LinkedList, we supply the type that we want to use with this particular instance. This code prints out:

这看起来很像现有的Go代码,只做了一个更改:在声明*LinkedList类型的变量时,我们提供了要用于此特定实例的类型。 此代码输出:

Hello->nil
1
Hello->Hola->हैलो->こんにちは->你好->nil
5

If we want to use our linked list with a different type, we simply supply the different type when we instantiate a different variable. If we have a type Person:

如果要使用其他类型的链表,则在实例化其他变量时只需提供其他类型。 如果我们type Person

type Person struct {
Name string
Age int
}

We can write the following code:

我们可以编写以下代码:

var peopleList *LinkedList[Person]
peopleList = peopleList.Append(Person{"Fred", 23})
peopleList = peopleList.Append(Person{"Joan", 30})
fmt.Println(peopleList)

Which prints out:

打印出:

{Fred 23}->{Joan 30}->nil

Just as The Go Playground allows you to try out current Go code, the Go team has made a new playground for testing generic code. You can try out our linked list at https://go2goplay.golang.org/p/OUX5OKRMqQw.

正如Go Playground允许您试用当前的Go代码一样,Go团队已经建立了一个新的操场来测试通用代码。 您可以在https://go2goplay.golang.org/p/OUX5OKRMqQw上尝试我们的链接列表。

Let’s try something new. We’re going to add another method to our linked list to tell us whether or not a specific value is in it:

让我们尝试一些新的东西。 我们将向链接列表添加另一种方法,以告诉我们其中是否包含特定值:

func (ll *LinkedList[T]) Contains(value T) bool {
for node := ll; node != nil; node = node.next {
if node.value == value {
return true
}
}
return false
}

Unfortunately, this will not work. If we try to compile it, we’ll get the error:

不幸的是,这是行不通的。 如果我们尝试对其进行编译,则会收到错误消息:

cannot compare node.value == value (operator == not defined for T)

The problem is that our placeholder type T doesn't specify what it can do. So far, all we can do is store it and retrieve it. If we want to do more, we have to specify some constraints on T.

问题在于我们的占位符类型T没有指定它可以做什么。 到目前为止,我们所能做的就是存储并检索它。 如果我们想做更多,我们必须在T上指定一些约束。

Since many (but not all!) Go types can be compared using == and !=, the Go generics proposal includes a new built-in interface called comparable. If we go back to the definition of our linked list type we can make a small change to support ==:

由于可以使用==!=来比较许多(但不是全部!)Go类型,因此Go泛型提议包含了一个新的内置接口,称为comparable 。 如果我们回到链表类型的定义,我们可以做一点改动来支持==

type LinkedList[type T comparable] struct {
value T
next *LinkedList[T]
}

We added the interface comparable to our type parameter definition clause and now we can use == to compare variables of type T within LinkedList's methods.

我们添加了comparable类型参数定义子句comparable的接口,现在我们可以使用==LinkedList的方法中比较类型T变量。

Using our previous data, if we run the following lines:

如果运行以下行,则使用我们之前的数据:

fmt.Println(head.Contains("Hello"))
fmt.Println(head.Contains("Goodbye"))
fmt.Println(peopleList.Contains(Person{"Joan", 30}))

You get the following results:

您得到以下结果:

true
false
true

You can see this code run at https://go2goplay.golang.org/p/dO7npX6IeaQ .

您可以在https://go2goplay.golang.org/p/dO7npX6IeaQ上看到此代码。

However, we can no longer assign non-comparable types to LinkedList. If we tried to make a linked list of functions:

但是,我们不能再将不可比较的类型分配给LinkedList 。 如果我们尝试建立函数的链接列表:

var functionList *LinkedList[func()]
functionList = functionList.Append(func() { fmt.Println("What about me?") })
fmt.Println(functionList)

It would fail at compilation time with the error message:

它将在编译时失败,并显示错误消息:

func() does not satisfy comparable

In addition to generic types, you can also write generic functions. One of the most common complaints about Go is that you cannot write a single function that processes a slice of any type. Let’s write three:

除了通用类型,您还可以编写通用函数。 关于Go的最常见的抱怨之一是,您不能编写处理任何类型切片的单个函数。 让我们写三个:

func Map[type T, E](in []T, f func(T) E) []E {
out := make([]E, len(in))
for i, v := range in {
out[i] = f(v)
}
return out
}func Reduce[type T, E](in []T, start E, f func(E, T) E) E {
out := start
for _, v := range in {
out = f(out, v)
}
return out
}func Filter[type T](in []T, f func(T) bool) []T {
out := make([]T, 0, len(in))
for _, v := range in {
if f(v) {
out = append(out, v)
}
}
return out
}

Just like a generic type, a generic function has a type parameter section. For functions, it appears between the function name and the function parameters. For Map and Reduce, we are using two type parameters in our function, both declared in the type parameter section and separated by commas. The function bodies are identical to what you'd use if the types were specific; the only difference is that we pass []E to make in Map and []T to make in Filter.

就像泛型类型一样,泛型函数具有类型参数部分。 对于功能,它出现在功能名称和功能参数之间。 对于MapReduce ,我们在函数中使用两个类型参数,它们都在类型参数部分中声明,并用逗号分隔。 如果类型是特定的,则函数主体与您将使用的主体相同; 唯一的区别是,我们通过[]EmakeMap[]TmakeFilter

When we run the code:

当我们运行代码时:

strings := []string{"1", "2", "Fred", "3"}
numStrings := Filter(strings, func(s string) bool {
_, err := strconv.Atoi(s)
return err == nil
})
fmt.Println(numStrings)nums := Map(numStrings, func(s string) int {
val, _ := strconv.Atoi(s)
return val
})
fmt.Println(nums)total := Reduce(nums, 0, func(start int, val int) int {
return start + val
})
fmt.Println(total)

We get the output:

我们得到输出:

1 2 3]
[1 2 3]
6

Try it for yourself at https://go2goplay.golang.org/p/ek3QTecbSL3 .

https://go2goplay.golang.org/p/ek3QTecbSL3上尝试一下。

One thing to notice: we didn’t explicitly specify the types when invoking the functions. Go generics use type inference to figure which types to use for function calls. There are situations where this doesn’t work (such as a type parameter that’s used for a return type, but not an input parameter). In those cases, you are required to specify all of the type arguments.

需要注意的一件事:调用函数时,我们没有明确指定类型。 Go泛型使用类型推断来确定要用于函数调用的类型。 在某些情况下,这是行不通的(例如用于返回类型的类型参数,而不是输入参数)。 在这些情况下,需要指定所有类型参数。

Let’s try to write another generic function. Go has a math.Max function that compares two float64 values and returns the larger one. It's written this way because nearly any other numeric type in Go can be converted to float64 for comparison (trivia time: a uint64 or int64 that requires more than 53 bits to express its value will lose precision when converted to a float64). Converting back and forth is ugly, so let's try to write a generic function to do this instead:

让我们尝试编写另一个通用函数。 Go具有math.Max函数,该函数比较两个float64值并返回较大的一个。 这样写是因为Go中几乎所有其他数字类型都可以转换为float64进行比较(琐事时间:需要53位以上的位来表示其值的uint64int64在转换为float64时会失去精度)。 来回转换很丑陋,所以让我们尝试编写一个通用函数来代替它:

func Max[type T](v1, v2 T) T {
if v1 > v2 {
return v1
}
return v2
}

Unfortunately, if we try to compile this function, we’ll get an error:

不幸的是,如果我们尝试编译此函数,则会收到错误消息:

cannot compare v1 > v2 (operator > not defined for T)

This is a lot like the error we got when we tried to compare values in our linked list, only this time it’s the > operator instead of the ==. Go isn't going to provide a built-in interface to support other operators. In this case, we have to write our own interface using a type list:

这很像我们尝试比较链表中的值时遇到的错误,只是这次是>运算符而不是== 。 Go不会提供内置接口来支持其他操作员。 在这种情况下,我们必须使用类型列表编写自己的接口:

type Ordered interface {
type string, int, int8, int16, int32, int64, float32, float64, uint, uint8, uint16, uint32, uintptr
}func Max[type T Ordered](v1, v2 T) T {
if v1 > v2 {
return v1
}
return v2
}

In order to work with operators, we declare an interface and list the types that support the operator that we want to use. Note that the valid operators are the ones that work for all of the listed types. For example, a generic function or type that uses Ordered as a type constraint cannot use - or *, because those are not defined for string.

为了与运算符一起使用,我们声明一个接口并列出支持我们要使用的运算符的类型。 请注意,有效的运算符是适用于所有列出的类型的运算符。 例如,使用Ordered作为类型约束的泛型函数或类型不能使用-* ,因为未为string定义它们。

Now that we have our interface constraint, we can pass an instance of any of those specified types (or any user-defined types whose underlying type is one of these types) into Max:

现在我们有了接口约束,我们可以将这些指定类型(或任何其类型为基础类型的用户定义类型)的实例传递给Max

fmt.Println(Max(100, 200))
fmt.Println(Max(3.5, 1.2))
fmt.Println(Max("sheep", "goat"))

This produces the output:

产生输出:

200
3.5
sheep

You can try this for yourself at https://go2goplay.golang.org/p/g2N1Zjc4oBs .

您可以在https://go2goplay.golang.org/p/g2N1Zjc4oBs上亲自尝试。

The types specified in a type list are underlying types. (See the Go language specification for a definition). That means the following code also works:

类型列表中指定的类型是基础类型 。 (有关定义,请参见Go语言规范 )。 这意味着以下代码也适用:

type MyInt intvar a MyInt = 10
var b MyInt = 20
fmt.Println(Max(a,b))

To be honest, I don’t prefer type lists. However, they provide a very concise way to specify what operators are available. They also allow you to specify what literals can be assigned to a variable of a generic type. Just as the available operators are the intersection of the operators on all types in the type list, the literals that can be assigned are the ones that can be assigned to all of the listed types. In the case of Ordered, you can't assign a literal, because there is no literal that can be assigned to both a string and any of the numeric types.

老实说,我不喜欢类型列表。 但是,它们提供了一种非常简洁的方法来指定可用的运算符。 它们还允许您指定可以将什么文字分配给通用类型的变量。 就像可用的运算符是类型列表中所有类型的运算符的交集一样,可以分配的文字就是可以分配给所有列出的类型的文字。 对于Ordered ,您不能分配文字,因为没有文字可以同时分配给string和任何数字类型。

You can use any interface as a type constraint, not just comparable or one with a type list. And an interface used as a type constraint can contain both methods and a type list. However, you cannot use an interface with a type list as a regular interface type.

您可以将任何接口用作类型约束,而不仅仅是comparable或具有类型列表的接口。 用作类型约束的接口可以同时包含方法和类型列表。 但是,不能将具有类型列表的接口用作常规接口类型。

There is a lot more in generics than I can cover here. Read through the Go Generics Draft (formally called the Type Parameters — Draft Design) to see more details on the draft design, things that it doesn’t cover, and additional sample code.

泛型中有很多我无法在这里介绍的内容。 通读Go Generics Draft (正式名称为Type Parameters-Draft Design ),以了解有关草稿设计,其未涵盖的内容以及其他示例代码的更多详细信息。

泛型与接口 (Generics vs. Interfaces)

While it’s very nice that Go reused the concept of interfaces to implement generics, it does lead to a little bit of confusion. The question is: When do you use generics and when do you use interfaces?

Go很好地重用了接口的概念来实现泛型,但这确实引起了一些混乱。 问题是: 什么时候使用泛型?什么时候使用接口?

It’s still very early days, so patterns are still being developed. There are some basic principles that are likely to be followed. The first principle is to do nothing. If your current code works with interfaces, leave it alone. Save generics for situations that can’t be addressed with interfaces alone:

现在还处于初期,因此仍在开发模式。 可能会遵循一些基本原则。 第一个原则是什么都不做。 如果您当前的代码可用于接口,请不要理会它。 将泛型保存为无法单独使用接口解决的情况:

  1. If you have a container type, consider switching to generics when they are available. Save interface{} for situations where reflection is needed.

    如果您有容器类型,请考虑在可用时切换到泛型。 保存界面{},以用于需要反射的情况。
  2. If you had been writing multiple implementations of functions to handle different numeric types or slice types, switch to generics.

    如果您已经编写了多个函数实现来处理不同的数字类型或切片类型,请切换到泛型。
  3. If you want to write a function or method that creates a new instance, you need to use generics.

    如果要编写创建新实例的函数或方法,则需要使用泛型。

The next question that people ask is around performance. The answer is: don’t worry about it for now. The current prototype tools are using a technique (re-writing generic Go code to standard Go code) that isn’t going to be used in any production release. There are multiple ways to compile and implement generics. Once there are final tools, we’ll be able to see what the tradeoffs are. Chances are, there won’t be a significant difference for most programs.

人们要问的下一个问题是性能。 答案是: 暂时不用担心。 当前的原型工具正在使用一种技术(将通用Go代码重写为标准Go代码),该技术不会在任何生产版本中使用。 有多种编译和实现泛型的方法。 一旦有了最终的工具,我们将能够看到权衡取舍。 有可能,大多数程序之间不会有显着差异。

少了什么东西? (What’s Missing?)

If you are a language geek, you’re probably aware of other features that fall under the umbrella of generics in other languages. Many of them will probably be left out of Go’s generics. These include:

如果您是语言怪胎,则可能知道其他功能属于其他语言的泛型。 其中许多可能会被Go的泛型排除在外。 这些包括:

  • Specialization (Providing special-case implementations of a generic function for specific types).

    专业化(为特定类型提供通用功能的特殊情况实现)。
  • Metaprogramming (Code that generates code at compile time).

    元编程(在编译时生成代码的代码)。
  • Operator methods (Making a generic type that supports operators like >, *, or []).

    运算符方法(使通用类型支持>,*或[]等运算符)。
  • Currying (Creating a new type or function based on a generic type by specifying some of the parameterized types).

    Currying(通过指定一些参数化类型,基于通用类型创建新类型或函数)。

下一步是什么? (What’s Next?)

The generics design is still in the draft stage; it’s likely that there will be further tweaks. If the design becomes a proposal and that proposal is accepted, the earliest possible release that would include generics is Go 1.17.

仿制药设计仍处于起草阶段。 可能会有进一步的调整。 如果设计成为建议书并且该建议书被接受,则包含通用名称的最早发布版本可能是Go 1.17。

It’s still early days, but I’m excited about the prospects for this design. It adds the most requested features to Go without making the language a great deal more complicated. Some people will be disappointed that other advanced features are left out, but that’s not the Go way. Go is intended to be a simple language that’s easy to read, easy to learn, and easy to maintain. By adding just enough generics to solve the most common problems, Go continues to meet that ideal.

还处于初期,但我对这种设计的前景感到兴奋。 它在Go中增加了最需要的功能,而不会使语言复杂得多。 有些人会失望地遗漏了其他高级功能,但这不是Go方式。 Go旨在成为一种易于阅读,易学且易于维护的简单语言。 通过添加足够的泛型来解决最常见的问题,Go继续达到了这一理想。

DISCLOSURE STATEMENT: © 2020 Capital One. Opinions are those of the individual author. Unless noted otherwise in this post, Capital One is not affiliated with, nor endorsed by, any of the companies mentioned. All trademarks and other intellectual property used or displayed are property of their respective owners.

披露声明:©2020 Capital One。 观点是个别作者的观点。 除非本文中另有说明,否则Capital One不与任何提及的公司有附属关系或认可。 使用或显示的所有商标和其他知识产权均为其各自所有者的财产。

翻译自: https://medium.com/capital-one-tech/generics-are-the-generics-of-go-3e0ef0cb9e04

golang 泛型

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值