首页
Preview

Go 语言中泛型的综合指南

Go 是一种静态类型语言。这意味着变量和参数的类型在编译时进行检查。内置的 Go 类型,如映射、切片、通道,以及内置的 Go 函数,如 lenmake,能够接受和返回 不同类型的值,但在 1.18 版本之前,用户定义的类型和函数不能。

这意味着在 Go 中,例如我为 int 创建了一个二叉树:

type IntTree struct {
 left, right *IntTree
 value int
}
func (t * IntTree) Lookup(x int) *IntTree { … }

…然后想要为 字符串或浮点数 创建一个二叉树,并且需要类型安全,我需要为每种类型编写一个 自定义树。这很冗长、容易出错,并且严重违反了 不要重复自己 原则。

如果,另一方面,我只是将我的二叉树函数更改为只接受类型为 interface{} 的参数(与接受 任何类型 相同),我将绕过 编译时类型检查 的安全网,这是 Go 的主要优势之一。使用类型为 interface{} 的参数还有其他陷阱。在 Go 中,不能创建由接口指定的新变量实例。Go 也没有提供一种处理任何类型的切片的方法,这意味着不能将 []string[]int 赋值给 interface{} 类型的变量。

结果是,在版本 1.18 和泛型之前,常见的算法,如对切片的映射、规约、过滤等,通常必须为每种类型重新实现。这常常令软件开发人员感到沮丧,并不可避免地成为 Go 语言的主要批评之一。

Go 开发人员需要的是能够编写函数或结构体,在使用时可以指定参数或变量的 具体类型,同时在编译时保持 类型安全

例如,考虑下面两个函数,它们分别对 map[string]int64map[string]float64 的值求和:

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

如果能够编写一个单一的类型安全函数,高效地对任何数字映射的值求和,而不是为每种映射类型重新编写一个函数,那将是很酷的事情。

…或者下面这两个函数,它们将 intint16 的值翻倍:

func DoubleInt(value int) int {
   return value * 2
}

func DoubleInt16(value int16) int16 {
   return value * 2
}

同样,如果能够编写一个单一的类型安全函数,将任何数字类型翻倍,那将是很酷的事情。

为什么等了这么久?

如果泛型的情况如此强大,为什么 Go 开发团队要花费 10 多年的时间将该特性添加到语言中呢?毫不奇怪,这是因为这个问题很难解决。Go 强调快速的编译器、清晰易读的代码和良好的执行时间,而各种提议的实现往往在某些程度上牺牲了其中一项或多项。

然而,正如开源软件的美丽之处一样,集体问题解决最终凝聚出了一种被 Go 团队和社区认为可接受的实现。结果是一种本质上是 Go 的解决方案,快速高效,同时足够灵活以满足规范要求。

Go 中的泛型 —— 简介

  • 自从版本 1.18 以来,Go 语言已经扩展,允许在 函数声明和类型声明 中添加显式定义的结构约束,称为 类型参数
func F[T any](p T) { … }

type M[T any] []
  • 类型参数列表 [T any] 使用 方括号,但看起来像一个常规的参数列表。这些类型参数可以在函数的 常规参数函数体中使用。
  • 泛型代码期望 类型参数满足某些要求,被称为 约束。每个类型参数必须有一个 约束 定义:
func F[T Constraint](p T) { … }
  • 这些约束只是 接口类型
  • 类型参数约束可以通过以下三种方式之一限制类型参数集:
  1. 任意 类型 T 限制为该类型
func F[T MyConstraint](p T) { … }
  1. 近似元素 ~T 限制为所有 _基础_类型为 T 的类型
func F[T ~int] (p T) { … }
  1. 联合元素 T1 | T2 | … 限制为 列出的任何元素之一
func F[T ~int | ~string | ~float64] (p T) { … }
  • 当泛型函数和类型使用 运算符 与定义的 类型参数 进行操作时,这些运算符必须满足 参数约束定义的接口我们将在后面看到示例。
  • 这个设计与 Go 1 完全向后兼容。

解释类型参数

泛型函数和类型中的类型参数类似于普通非类型参数,但并不完全相同。它们通过出现在常规参数之前的方括号而不是圆括号的附加参数列表进行定义。一旦定义,它们可以与常规参数一起列出,并在泛型函数或类型的体中使用。

与常规参数具有类型一样,类型参数具有元类型,也称为 约束

// Print prints the elements of any slice.
// Print has a type parameter T and has a single (non-type)
// parameter s, which is a slice of that type parameter.
func Print[T any](s []T) {
   for _, v := range s {
     fmt.Println(v)
   }
}

这意味着在函数 Print 中,标识符 T 是一个类型参数,它是一个 当前未知但在调用函数时将知道的类型。如上所示,any 约束允许任何类型作为类型参数,并仅允许函数使用 any 类型允许的操作。any 的接口类型是空接口:interface{}。事实上,在泛型中,any 关键字只是 语法糖,表示 interface{}。因此,我们可以将 Print 示例写成

func Print[T interface{}](s []T) {
 // same as above
}

类型参数 (T) 也可以在定义函数的参数类型时使用 —— 因此,参数 s 被定义为 T 类型的切片。它也可以在函数体内部用作类型。

惯例(即良好的实践)是将类型参数命名为单个大写字母,例如 TS

由于 Print 定义了一个 类型参数,所有 Print 的调用现在都必须提供一个 类型参数 。类型参数像类型参数声明一样作为一个前置参数列表传递,使用方括号。

// Call Print with a []int.
// Print has a type parameter T, and we want to pass a []int,
// so we pass a type argument of int by writing Print[int].
// The function Print[int] expects a []int as an argument.
Print[int]([]int{1, 2, 3})

// This will print:
// 1
// 2
// 3

在上面的示例中,类型参数 [int] 被传递给 Print 函数,以明确表示我们正在使用一个 ints 切片调用通用函数。

类型参数约束解释

下面是一个将任何类型的切片转换为[]string的函数,它通过调用每个元素的String方法来实现。

// This function is INVALID.
func Stringify[T any](s []T) (ret []string) {
 for _, v := range s {
 ret = append(ret, v.String()) // INVALID
 }
 return ret
}

虽然这看起来没问题,但在这个例子中,v的类型为T,而T可以是任何类型。

这意味着T现在必须有一个String()方法,以使调用v.String()有效。

正如我们之前所看到的,类型参数满足一定的要求,称为约束。这些约束限制了调用者传递的类型参数和通用函数中的代码的类型参数。

约束只是接口类型。在Go中,任何定义的类型只要实现了与接口相同的方法,就可以将其分配给接口类型的变量。这意味着满足约束意味着实现接口类型。

因此,调用者只能传递满足约束的类型参数,并且类型参数必须实现约束定义的任何方法。

回到我们上面的例子,我们看到约束是任何。泛型函数可以与该类型参数的值使用的唯一操作是那些允许任何类型的值使用的操作。

那么任何类型是否具有String()操作呢?事实证明,它没有。为了使我们的Stringify示例编译,我们不能使用任何作为类型约束。

为了使Stringify示例工作,我们需要一个带有String()方法的接口类型,该方法不带参数并返回字符串类型的值。

// Stringer is a type constraint that requires the type argument to have
// a String method and permits the generic function to call String().
// The String method should return a string representation of the value.
type Stringer interface {
 String() string
}

// Stringify calls the String method on each element of s, and returns the results. The single type parameter T is followed by the constraint that applies to T, in this case Stringer.
func Stringify[T Stringer](s []T) (ret []string) {
 for _, v := range s {
 ret = append(ret, v.String())
 }
 return ret
}

以及一个int类型的调用示例:

import (
"fmt"
"strconv"
)

type myint int

func (i myint) String() string {
 return strconv.Itoa(int(i))
}

func main() {
  x := []myint{myint(1), myint(2), myint(3)}
  Stringify(x)
  fmt.Println(x)
  // Prints "[1 2 3]"
}

请注意,就像每个普通参数可能具有自己的类型一样,每个类型参数也可能具有自己的约束:

// Stringer is a type constraint that requires a String method.
// The String method should return a string representation of the value.
type Stringer interface {
 String() string
}
// Plusser is a type constraint that requires a Plus method.
// The Plus method is expected to add the argument to an internal
// string and return the result.
type Plusser interface {
 Plus(string) string
}
// ConcatTo takes a slice of elements with a String method and a slice
// of elements with a Plus method. The slices should have the same
// number of elements. This will convert each element of s to a string,
// pass it to the Plus method of the corresponding element of p,
// and return a slice of the resulting strings.
func ConcatTo[S Stringer, P Plusser](s []S, p []P) []string {
 r := make([]string, len(s))
 for i, v := range s {
 r[i] = p[i].Plus(v.String())
 }
 return r
}

Go的预定义约束和类型推断

考虑下面的泛型求和函数,该函数解决了我们之前识别的非泛型求和问题:

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

请注意,K类型参数的约束:comparable。由于许多泛型函数涉及比较和循环遍历映射和切片,Go提供了常用约束类型的辅助库。

标准库的comparable允许任何可以用作比较运算符==!=的操作数的类型。由于Go要求映射键可比较,因此将K声明为comparable是必要的,以便你可以在映射变量中使用K作为键。

其次,请注意V类型参数的约束类型的管道分隔列表:int64 | float64。使用|指定两种类型的并集,这意味着该约束允许int64float64。编译器将允许任何类型作为调用代码中的参数。

请注意函数的参数-map[K]V。我们知道map[K]V是一个有效的映射类型,因为K是一个可比较的类型。如果我们没有声明K可比较,编译器将拒绝对map[K]V的引用。

调用上述函数的方法如下:

// 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("Generic Sums: %v and %v\n",
    SumIntsOrFloats[string, int64](ints),
    SumIntsOrFloats[string, float64](floats))

请注意,我们指定了类型参数-方括号中的类型名称-以清楚地说明应该替换调用中的类型参数的类型。

通常,Go编译器可以从函数的参数中推断出你要使用的类型。这称为类型推断,使你可以以更简化的方式调用泛型函数(省略类型参数)。

fmt.Printf("Generic Sums, type parameters inferred: %v and %v\n",
    SumIntsOrFloats(ints),
    SumIntsOrFloats(floats))

总结和个人见解

Go泛型是Go语言中简单、优雅和强大的补充,使开发人员可以以类型安全的方式使用简单的抽象模式。在未来几年中,随着泛型的广泛应用,它们将导致更高效和有效的Go编码。尽管如此,Go程序员在选择使用泛型时应该谨慎。引用Ian Lance Taylor的话:

“如果你发现自己多次编写完全相同的代码,而唯一的区别是该代码使用不同的类型,请考虑使用类型参数。另一种说法是,在你注意到自己即将多次编写完全相同的代码之前,应避免使用类型参数。”

泛型代码示例

最后,这里有一些Go中有用的泛型示例:

package main

import (
	"fmt"

	"golang.org/x/exp/constraints"
)

type Number interface {
	constraints.Float | constraints.Integer | constraints.Complex
}

func Double[T Number](value T) T {
	return value * 2
}

func DotProduct[T Number](s1, s2 []T) T {
	if len(s1) != len(s2) {
		panic("DotProduct: slices of unequal length")
	}
	var r T
	for i := range s1 {
		r += s1[i] * s2[i]
	}
	return r
}

func Sum[K comparable, V Number](m map[K]V) V {
	var s V
	for _, v := range m {
		s += v
	}
	return s
}

func main() {
	// Invoke Double
	fmt.Println(Double(23))
	fmt.Println(Double(23.23))
	fmt.Println(Double(-2323.3434))

	// Invoke DotProduct
	i := []int{1, 2, 3}
	j := []int{4, 5, 6}
	fmt.Println(DotProduct(i, j))

	// Invoke Sum
	ints := map[string]int64{
		"first":   23,
		"second":  565,
		"third":   755,
		"fourth":  766,
		"fifth":   8977,
		"sixth":   70433,
		"seventh": 4339222,
	}
	fmt.Println(Sum(ints))

}

通用数字函数

package main

import (
	"fmt"

	"golang.org/x/exp/constraints"
)

type Number interface {
	constraints.Float | constraints.Integer | constraints.Complex
}

func Double[T Number](value T) T {
	return value * 2
}

func DotProduct[T Number](s1, s2 []T) T {
	if len(s1) != len(s2) {
		panic("DotProduct: slices of unequal length")
	}
	var r T
	for i := range s1 {
		r += s1[i] * s2[i]
	}
	return r
}

func Sum[K comparable, V Number](m map[K]V) V {
	var s V
	for _, v := range m {
		s += v
	}
	return s
}

func main() {
	// Invoke Double
	fmt.Println(Double(23))
	fmt.Println(Double(23.23))
	fmt.Println(Double(-2323.3434))

	// Invoke DotProduct
	i := []int{1, 2, 3}
	j := []int{4, 5, 6}
	fmt.Println(DotProduct(i, j))

	// Invoke Sum
	ints := map[string]int64{
		"first":   23,
		"second":  565,
		"third":   755,
		"fourth":  766,
		"fifth":   8977,
		"sixth":   70433,
		"seventh": 4339222,
	}
	fmt.Println(Sum(ints))

}

通用切片函数

package main

import (
	"fmt"
	"sort"

	"golang.org/x/exp/constraints"
)

// Map turns a []T1 to a []T2 using a mapping function.
// This function has two type parameters, T1 and T2.
// This works with slices of any type.
func Map[T1, T2 any](s []T1, f func(T1) T2) []T2 {
	r := make([]T2, len(s))
	for i, v := range s {
		r[i] = f(v)
	}
	return r
}

// Reduce reduces a []T1 to a single value using a reduction function.
func Reduce[T1, T2 any](s []T1, initializer T2, f func(T2, T1) T2) T2 {
	r := initializer
	for _, v := range s {
		r = f(r, v)
	}
	return r
}

// Filter filters values from a slice using a filter function.
// It returns a new slice with only the elements of s
// for which f returned true.
func Filter[T any](s []T, f func(T) bool) []T {
	var r []T
	for _, v := range s {
		if f(v) {
			r = append(r, v)
		}
	}
	return r
}

// Merge - receives slices of type T and merges them into a single slice of type T.
func Merge[T any](slices ...[]T) (mergedSlice []T) {
	for _, slice := range slices {
		mergedSlice = append(mergedSlice, slice...)
	}
	return mergedSlice
}

// Includes - given a slice of type T and a value of type T,
// determines whether the value is contained by the slice.
func Includes[T comparable](slice []T, value T) bool {
	for _, el := range slice {
		if el == value {
			return true
		}
	}
	return false
}

// // Sort - sorts given a slice of any orderable type T
// The constraints.Ordered constraint in the Sort() function guarantees that 
// the function can sort values of any type supporting the operators <, <=, >=, >.
func Sort[T constraints.Ordered](s []T) {
	sort.Slice(s, func(i, j int) bool {
		return s[i] < s[j]
	})
}

func main() {

	s := []int{1, 2, 3, 7, 5, 22, 18}
	j := []int{4, 5, 6}

	floats := Map(s, func(i int) float64 { return float64(i) })
	fmt.Println(floats)

	sum := Reduce(s, 0, func(i, j int) int { return i + j })
	fmt.Println(sum)

	evens := Filter(s, func(i int) bool { return i%2 == 0 })
	fmt.Println(evens)

	merged := Merge(s, j)
	fmt.Println(merged)

	i := Includes(s, 22)
	fmt.Println(i)

	Sort(s)
	fmt.Println(s)

}

通用map函数

package main

import (
	"fmt"

	"golang.org/x/exp/constraints"
)

// Keys returns the keys of the map m in a slice.
// The keys will be returned in an unpredictable order.
// This function has two type parameters, K and V.
// Map keys must be comparable, so key has the predeclared
// constraint comparable. Map values can be any type.
func Keys[K comparable, V any](m map[K]V) []K {
	r := make([]K, 0, len(m))
	for k := range m {
		r = append(r, k)
	}
	return r
}

// Sum sums the values of map containing numeric or float values
func Sum[K comparable, V constraints.Float | constraints.Integer](m map[K]V) V {
	var s V
	for _, v := range m {
		s += v
	}
	return s
}

func main() {
	k := Keys(map[int]int{1: 2, 2: 4})
	fmt.Println(k)

	s := Sum(map[int]int{1: 2, 2: 4})
	fmt.Println(s)

}

集合的通用实现

package main

import "fmt"

// Set is a set of any values.
type Set[T comparable] map[T]struct{}

// Make returns a set of some element type.
func Make[T comparable]() Set[T] {
	return make(Set[T])
}

// Add adds v to the set s.
// If v is already in s this has no effect.
func (s Set[T]) Add(v T) {
	s[v] = struct{}{}
}

// Delete removes v from the set s.
// If v is not in s this has no effect.
func (s Set[T]) Delete(v T) {
	delete(s, v)
}

// Contains reports whether v is in s.
func (s Set[T]) Contains(v T) bool {
	_, ok := s[v]
	return ok
}

// Len reports the number of elements in s.
func (s Set[T]) Len() int {
	return len(s)
}

// Iterate invokes f on each element of s.
// It's OK for f to call the Delete method.
func (s Set[T]) Iterate(f func(T)) {
	for v := range s {
		f(v)
	}
}

func main() {
	// Create a set of ints.
	// We pass int as a type argument.
	// Then we write () because Make does not take any non-type arguments.
	// We have to pass an explicit type argument to Make.
	// Function argument type inference doesn't work because the
	// type argument to Make is only used for a result parameter type.
	set := Make[int]()

	// Add the value 1 to the set s.
	set.Add(1)
	set.Add(3)
	set.Add(5)
	set.Add(7)
	set.Add(1)
	fmt.Println(set.Contains(2))

}

参考资料

Tutorial: Getting started with generics - The Go Programming Language

An Introduction To Generics - The Go Programming Language

Type Parameters Proposal

A Proposal for Adding Generics to Go - The Go Programming Language

Generic Map, Filter and Reduce in Go

An Introduction to Generics in Go

译自:https://itnext.io/a-comprehensive-guide-to-generics-in-go-5a9dcda5669c

版权声明:本文内容由TeHub注册用户自发贡献,版权归原作者所有,TeHub社区不拥有其著作权,亦不承担相应法律责任。 如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

点赞(0)
收藏(0)
菜鸟一只
你就是个黄焖鸡,又黄又闷又垃圾。

评论(0)

添加评论