golang 1.18 新增泛型 简介

目录

(一)如何对泛型进行输出

(二)如何用泛型约束使用的类型范围

(三)泛型中的接口本身对范型进行约束

(四)泛型中如何操作切片

(五)如何利用泛型实现最大值最小值函数

(六)如何使用Golang泛型自带的comparable约束

(七)如何在泛型中操作指针

(八)Golang泛型中如何操作map

(九)如何在Golang泛型中使用队列操作

(十)Golang泛型中新加入的一些约束包

 总结

适用性

复杂性

效率


(一)如何对泛型进行输出

下面的例子是一个对泛型输出的基本例子。函数可以有一个额外的类型参数列表,它使用方括号,但看起来像一个普通的参数列表:func F[T any](p T) { ... },代码中的[T any]即为类型参数,意思是该函数支持任何T类型,当我们调用printSlice[string]([]string{“Hello”,“World”})时,会被类型推导为string类型,不过在编译器完全可以实现类型推导时,也可以省略显式类型,如:printSlice([]string{“Hello”,“World”}) ,这样也将会是对的;

package main
 
import (
  "fmt"
)
 
func printSlice[T any](s []T) {
  for _, v := range s {
    fmt.Printf("%v ", v)
  }
  fmt.Print("\n")
}
 
func main() {
  printSlice[int]([]int{1, 2, 3, 4, 5})
  printSlice[float64]([]float64{1.01, 2.02, 3.03, 4.04, 5.05})
  printSlice([]string{"Hello", "World"})
  printSlice[int64]([]int64{5, 4, 3, 2, 1})
}

输出为

1 2 3 4 5 
1.01 2.02 3.03 4.04 5.05 
Hello World 
5 4 3 2 1

(二)如何用泛型约束使用的类型范围

这个例子包含了一个类型约束。每个类型参数都有一个类型约束,就像每个普通参数都有一个类型:func F[T Constraint](p T) { ... },类型约束是接口类型。该提案扩展了interface语法,新增了类型列表(type list)表达方式,专用于对类型参数进行约束。

package main
 
import (
  "fmt"
)
 
type Addable interface {
  type int, int8, int16, int32, int64,
    uint, uint8, uint16, uint32, uint64, uintptr,
    float32, float64, complex64, complex128,
    string
}
 
func add[T Addable] (a, b T) T {
  return a + b
}
 
func main() {
  fmt.Println(add(1,2))
  fmt.Println(add("hello","world"))
}

输出为:

3
helloworld

在官方的最新proposal里有提到,在Golang中,并不是所有的类型都满足+号运算。在1.17的版本中,泛型函数只能使用类型参数所能实例化出的任意类型都能支持的操作。

比如下面的add函数的类型参数T没有任何约束,它可以被实例化为任何类型;那么这些实例化后的类型是否都支持+操作符运算呢?显然不是;因此,报错了!对于没有任何约束的类型参数实例,允许对其进行的操作包括:

  • 声明这些类型的变量。

  • 使用相同类型的值为这些变量赋值。

  • 将这些类型的变量以实参形式传给函数或从作为函数返回值。

  • 取这些变量的地址。

  • 将这些类型的值转换或赋值给interface{}类型变量。

  • 通过类型断言将一个接口值赋值给这类类型的变量。

  • 在type switch块中作为一个case分支。

  • 定义和使用由该类型组成的复合类型,比如:元素类型为该类型的切片。

  • 将该类型传递给一些内置函数,比如new。

这就意味着,如果不用interface约束,直接使用的话,你将得到如下的结果:

package main
 
import (
  "fmt"
)

func add[T any] (a, b T) T {
  return a + b
}
 
func main() {
  fmt.Println(add(1,2))
  fmt.Println(add("hello","world"))
}

输出为:

type checking failed for main
prog.go2:8:9: invalid operation: operator + not defined for a (variable of type parameter type T)

在约束里,甚至可以放进去接口如下:

package main
 
import (
  "fmt"
)
 
type Addable interface {
  type int,interface{}
}
 
func add[T Addable] (a T) T {
  return a
}
 
func main() {
  fmt.Println(add(1))
}

接着假如我们去掉string,如下代码所示。以该示例为例,如果编译器通过类型推导得到的类型不在这个接口定义的类型约束列表中,那么编译器将允许这个类型参数实例化;否则就像类型参数实例化将报错!

package main
 
import (
  "fmt"
)
 
type Addable interface {
  type int, int8, int16, int32, int64,
    uint, uint8, uint16, uint32, uint64, uintptr,
    float32, float64, complex64, complex128
}
 
func add[T Addable] (a, b T) T {
  return a + b
}
 
func main() {
  fmt.Println(add(1,2))
  fmt.Println(add("hello","world"))
}

输出为:

type checking failed for main
prog.go2:19:14: string does not satisfy Addable (string or string not found in int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr, float32, float64, complex64, complex128)

注意:我们自己定义的带有类型列表的接口将无法用作接口变量类型,如下代码将会报错

package main
 
type MyType interface {
  type int
}
 
func main() {
  var n int = 6
  var i MyType
  i = n
  _ = i
}

输出为:

type checking failed for main
prog.go2:9:8: interface contains type constraints (int)

(三)泛型中的接口本身对范型进行约束

package main
 
import (
  "fmt"
  "strconv"
)
 
type MyStringer interface {
  String() string
}
 
type StringInt int
type myString string
 
func (i StringInt) String() string {
  return strconv.Itoa(int(i))
}
 
func (str myString) String() string {
  return string(str)
}
 
func stringify[T MyStringer](s []T) (ret []string) {
  for _, v := range s {
    ret = append(ret, v.String())
  }
  return ret
}
 
func stringify2[T MyStringer](s []T) (ret []string) {
  for _, v := range s {
    ret = append(ret, v.String())
  }
  return ret
}
 
func main() {
  fmt.Println(stringify([]StringInt{1, 2, 3, 4, 5}))
  fmt.Println(stringify2([]myString{"1", "2", "3", "4", "5"}))
}

输出为:

[1 2 3 4 5]
[1 2 3 4 5]

代码中我们声明了MyStringer接口,并且使用StringInt和myString类型实现了此接口;在范型方法中,我们声明了范型的类型为:任意实现了MyStringer接口的类型;只要实现了这个接口,那么你就可以直接使用,在现在某些需要传interface{}作为参数的函数里面,可以直接指定类型了。当你改为如下代码时

func main() {
  fmt.Println(Stringify([]int{1, 2, 3, 4, 5}))
}

会报错:

type checking failed for main
prog.go2:27:14: int does not satisfy MyStringer (missing method String)

只有实现了Stringer接口的类型才会被允许作为实参传递给Stringify泛型函数的类型参数并成功实例化!当然也可以将MyStringer接口写成如下的形式:

type MySignedStringer interface {
  type int, int8, int16, int32, int64
  String() string
}

表示只有int, int8, int16, int32, int64,这样类型参数的实参类型既要在MySignedStringer的类型列表中,也要实现了MySignedStringer的String方法,才能使用。像这种不在里面的type StringInt uint就会报错。

(四)泛型中如何操作切片

可以看到在下面的例子里面,我们声明了一个可以存放任何类型的切片,叫做slice,如type slice[T any] []T。和泛型函数一样,使用泛型类型时,首先要对其进行实例化,即显式为类型参数赋值类型。如果在类型定义时,将代码改成vs:=slice{5,4,2,1},那么你会得到如note1中的结果。因为编译器并没有办法进行类型推导,也就是表示它并不知道,你输出的是那种类型。哪怕你在interface里面定义了约束。哪怕你在接口中定义了类型约束type int, string,同样会报错,如note2所示。

package main
 
import (
  "fmt"
)
 
type slice[T any] []T
 
/*
type any interface {
  type int, string
}*/
 
func printSlice[T any](s []T) {
  for _, v := range s {
    fmt.Printf("%v ", v)
  }
  fmt.Print("\n")
}
 
func main() {
  // note1: cannot use generic type slice[T interface{}] without instantiation
  // note2: cannot use generic type slice[T any] without instantiation
  vs := slice[int]{5, 4, 2, 1}
  printSlice(vs)
 
  vs2 := slice[string]{"hello", "world"}
  printSlice(vs2)
}

输出为:

5 4 2 1 
hello world

(五)如何利用泛型实现最大值最小值函数

package main
 
import (
  "fmt"
)
 
type minmax interface {
  type int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr, float32, float64
}
 
func max[T minmax](a []T) T {
  m := a[0]
  for _, v := range a {
    if m < v {
      m = v
    }
  }
  return m
}
 
func min[T minmax](a []T) T {
  m := a[0]
  for _, v := range a {
    if m > v {
      m = v
    }
  }
  return m
}
 
func main() {
  vi := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
  result := max(vi)
  fmt.Println(result)
 
  vi = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
  result = min(vi)
  fmt.Println(result)
}

输出为:

10
1

(六)如何使用Golang泛型自带的comparable约束

当你写成如下代码时,便会报错:

package main
 
import (
  "fmt"
)
 
func findFunc[T any](a []T, v T) int {
  for i, e := range a {
    if e == v {
      return i
    }
  }
  return -1
}
 
func main() {
  fmt.Println(findFunc([]int{1, 2, 3, 4, 5, 6}, 5))
}

输出为:

type checking failed for main
prog.go2:9:6: cannot compare e == v (operator == not defined for T)

因为不是所有的类型都可以==比较,所以Golang内置提供了一个comparable约束,表示可比较的。参考下面代码:

package main
 
import (
  "fmt"
)
 
func findFunc[T comparable](a []T, v T) int {
  for i, e := range a {
    if e == v {
      return i
    }
  }
  return -1
}
 
func main() {
  fmt.Println(findFunc([]int{1, 2, 3, 4, 5, 6}, 5))
}

输出为:

4

(七)如何在泛型中操作指针

package main
 
import (
  "fmt"
)
 
func pointerOf[T any](v T) *T {
  return &v
}
 
func main() {
  sp := pointerOf("foo")
  fmt.Println(*sp)
 
  ip := pointerOf(123)
  fmt.Println(*ip)
  *ip = 234
  fmt.Println(*ip)
}

输出为:

foo
123
234

(八)Golang泛型中如何操作map

在现实开发过程中,我们往往需要对slice中数据的每个值进行单独的处理,比如说需要对其中数值转换为平方值,在泛型中,我们可以抽取部分重复逻辑作为map函数:

package main
 
import (
  "fmt"
)
 
func mapFunc[T any, M any](a []T, f func(T) M) []M {
  n := make([]M, len(a), cap(a))
  for i, e := range a {
    n[i] = f(e)
  }
  return n
}
 
func main() {
  vi := []int{1, 2, 3, 4, 5, 6}
  vs := mapFunc(vi, func(v int) string {
    return "<" + fmt.Sprint(v*v) + ">"
  })
  fmt.Println(vs)
}

输出为:

[<1> <4> <9> <16> <25> <36>]

(九)如何在Golang泛型中使用队列操作

在现实开发过程中,我们有可能会需要一个队列去处理一些数据,在泛型中,我们可以抽取部分重复逻辑来实现

package main
 
import (
  "fmt"
)
 
type queue[T any] []T
 
func (q *queue[T]) enqueue(v T) {
  *q = append(*q, v)
}
 
func (q *queue[T]) dequeue() (T, bool) {
  if len(*q) == 0 {
    var zero T
    return zero, false
  }
  r := (*q)[0]
  *q = (*q)[1:]
  return r, true
}
 
func main() {
  q := new(queue[int])
  q.enqueue(5)
  q.enqueue(6)
  fmt.Println(q)
  fmt.Println(q.dequeue())
  fmt.Println(q.dequeue())
  fmt.Println(q.dequeue())
}

输出为:

&amp;[5 6]
5 true
6 true
0 false

(十)Golang泛型中新加入的一些约束包

官方也引入了一些官方包来方面泛型的使用,具体如下:

// constraints 定义了一组与类型参数一起使用的约束
package constraints
 
// Signed是允许任何有符号整数类型的约束。
type Signed interface { ... }
 
// Unsigned是允许任何无符号整数类型的约束。
type Unsigned interface { ... }
 
// Integer是允许任何整数类型的约束。
type Integer interface { ... }
 
// Float是一个允许任何浮点类型的约束。
type Float interface { ... }
 
// Complex是允许任何复杂数值类型的约束。
type Complex interface { ... }
 
// Ordered是一个约束,允许任何有序类型:任何支持操作符< <= >= >的类型。
type Ordered interface { ... }

使用方式示例如下:

package main
 
import (
  "constraints"
  "fmt"
)
 
type v[T constraints.Ordered] T
 
type Vector[T constraints.Ordered] struct {
  x, y T
}
 
func (v *Vector[T]) Add(x, y T) {
     v.x += T(x)
     v.y += T(y)
}
 
func (v *Vector[T]) String() string {
     return fmt.Sprintf("{x: %v, y: %v}", v.x, v.y)
}
 
func NewVector[T constraints.Ordered](x, y T) *Vector[T] {
     return &amp;Vector[T]{x: x, y: y}
}
 
func main() {
     v := NewVector[float64](1, 2)
     v.Add(2, 3)
    fmt.Println(v)
}

 总结

尽管最新的proposal冗长而详尽,但总结起来如下:

  • 函数和类型可以具有类型参数,该类型参数使用可选约束(接口类型)定义,约束描述了这些参数所需的方法和允许的类型。

  • 当使用类型参数调用函数时,类型推断通常会允许用户省略类型参数。

  • 泛型函数只能使用约束允许的所有类型支持的操作

  • 此设计完全向后兼容,但建议对func F(x(T))的含义进行更改。

适用性

此外,标准库中将会引入一系列新的package。

  • 一个新slices包将会被引入,它与现存的bytes和strings包类似,用于操作任何类型元素的slice。新的maps和chans包将会提供一些简单的算法,用于操作任意类型的元素。set包也会被引入。

  • 一个新constraints包将会提供一系列标准约束,如“所有整数类型”或“所有数值类型”这类约束。

  • 诸如container/list,container/ring这些包,或者是诸如sync.Map,sync/atomic.Value之类,将会升级到编译时类型安全(使用新的名字或新的版本 )。

  • math包将会为数值类型提供一系列简单的标准算法,比如呼声很高的Min和Max函数。

  • 可能会开发新的特殊容器,这些容器是编译时类型安全的,也可能会增加泛型的sort包。

复杂性

Golang的一大优点是它的简单性。显然,这种设计使语言更加复杂,对于泛型推出,无论采用什么技术,都会增加Golang的复杂性,提升其学习门槛,代码的可读性也可能会下降,官方对其增加的复杂性的解释如下:

  • 对于阅读编写良好的通用代码而不是编写代码的人来说,增加的复杂性很小。

  • 预计大多数包不会定义泛型类型或函数,但许多包可能会使用其他地方定义的泛型类型或函数。

  • 在常见情况下,泛型函数的工作方式与非泛型函数完全相同。

效率

官方目前尚不清楚人们期望从通用代码中获得什么样的效率,他们将其划分为泛型函数和泛型类型。

  • 可以使用基于接口的方法编译泛型函数。这将优化编译时间,因为函数只编译一次,但会有一些运行时间成本。

  • 对于每组类型参数,泛型类型可能被编译多次。这显然会带来编译时间成本,但不应该有任何运行时间成本。编译器还可以选择使用类似于接口类型的方法来实现泛型类型,使用专用方法访问依赖于类型参数的每个元素

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值