[十分钟系列] go1.18 泛型最佳实践实例(一)

2022年3月18日 398点热度 0人点赞 0条评论

图片大家好,我是小猛哥,好久没更新了,趁着 go1.18 刚刚发布,抓紧蹭一下泛型的热度,尽管各个公众号已经发的很多了,但还是要蹭,就硬蹭!

本文不重点讨论 《为什么 go 要规划并使用泛型》[1],也不会介绍《 go 泛型使用基础》[2]。而是将其作为预备知识「先学附录听话!」直接上泛型最佳实践范例,让大家更为清楚地了解到 go 泛型如何真正地应用到我们开发与生产中,而不是如空中楼阁,空有概念而不知其途。图片

优秀实践

filter 过滤元素

  • 该方法应用于按照自定义条件将切片中不合符条件的元素摘除并返回过滤后的切片。
  • 支持的泛型类型:any。这是因为过滤条件依赖于使用方自定义,因此也增加了泛型函数的通用性。
package main

import (
 "fmt"
)

func filterFunc[T any](a []T, f func(T) bool) []T {
 var n []T
 for _, e := range a {
  if f(e) {
   n = append(n, e)
  }
 }
 return n
}

func main() {
 vi := []int{123456}
 vi = filterFunc(vi, func(v int) bool {
  return v < 4
 })
 fmt.Println(vi)
}

find 查找元素

  • 该方法应用于在已知切片中查找给定元素是否存在,若存在则返回元素所在切片下标,不存在则返回
    -1。
  • 支持泛型类型:comparable。即属于相同泛型类型的不同元素之间必须可以比较是否相等。
// comparable is an interface that is implemented by all comparable types
// (booleans, numbers, strings, pointers, channels, arrays of comparable types,
// structs whose fields are all comparable types).
// The comparable interface may only be used as a type parameter constraint,
// not as the type of a variable.
type comparable interface{ comparable }
  • 根据上面的定义可知,布尔类型、数值类型、字符串类型、指针类型、通道类型、数组类型以及成员都是 comparable 的结构体类型都是可以比较的。
  • 相对的我们熟悉的切片类型,哈希表类型则无法做 equal 比较。
package main

func findFunc[T comparable](a []T, v T) int {
 for i, e := range a {
  if e == v {
   return i
  }
 }
 return -1
}

func main() 定义  面给到  定义 {
 print(findFunc([]int{123456}, 5))
}
  • 这里算法复杂度为 o(n),如果对于有序的泛型类型(即如下 Ordered)则应该通过二分的方式提升查询效率。
type Ordered interface {
 Integer | Float | ~string
}

sort 排序元素

  • 该方法应用于对已知切片中元素从小到大进行排序,并返回排序后的切片。
  • 支持泛型类型:Ordered。即元素是可以比较大小的,该定义已经在上面的 find 泛型方法的介绍中给出了,这里不再重复介绍。
package main

import (
 "fmt"

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

func sort[T []EE constraints.Ordered](arr T) T {
 for i := 0; i < len(arr)-1; i++ {
  for j := 0; j < len(arr)-i-1; j++ {
   if arr[j] > arr[j+1] {
    arr[j], arr[j+1] = arr[j+1], arr[j]
   }
  }
 }
 return arr
}

func main() {
 fmt.Println(sort([]int{312}))
 fmt.Println(sort([]float64{6.27.912.1}))
}
  • 上述排序是使用的什么算法以及复杂度如何?必须一眼看出来,看不出来的你的本科老师们要集体流泪到天亮了~~

chan 泛型通道

  • 该方法创建并返回一个泛型类型为 T 的堵塞同步 channel。其中泛型类型 T 由另一个泛型类型 E 推断产生。
  • 方法内部创建一个协程,协程的生命周期由上游传递的 ctx 结合多路复用 select 实现控制。
package main

import (
 "context"
 "fmt"
)

func makeChan[T chan EE any](ctx context.Context, arr []E) T {
 ch := make(T)
 go func() {
  defer close(ch)
  for _, v := range arr {
   select {
   case <-ctx.Done():
    return
   default:
   }
   ch <- v
  }
 }()
 return ch
}

func main() {
 for v := range makeChan(context.Background(), []string{"a""b""c"}) {
  fmt.Println(v)
 }
}
  • 主进程中 for-range chan 持续消费通道内生成数据,直到 ctx 结束 channel 被关闭后退出。

vector 泛型向量

  • 这段代码片主要给出了如何定义泛型类型以及如何定义该类型的泛型方法。其中 add 方法模拟向量加这一动作。
  • 支持泛型类型:Addable。因为模拟的是向量加这一动作,该动作包含标量加的动作,因此要求向量结构体包含成员必须是可加类型的。
type Addable interface {
 int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | uintptr | float32 | float64 | complex64 | complex128
}

type Vector[T Addable] 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 Addable](x, y T) *Vector[T] {
 return &Vector[T]{x: x, y: y}
}

func main() {
 v := NewVector[float64](1, 2)
 v.Add(2, 3)
 fmt.Println(v)
}

option 泛型模式

  • 对于函数式编程比较熟悉的同学。可能已经清楚了 option 模式。该模式通常应用于函数入参可变参数数量/类型等提供灵活的配置方式。
  • WithValue 这个眼不眼熟,像不像 grpc -> client.invoke 参数配置方法。
package main

import "fmt"

type Addable interface {
 int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | uintptr | float32 | float64 | complex64 | complex128
}

type Foo[T Addable] struct{ v T }

func (foo *Foo[T]) Add(v T) {
 foo.v += v
}

type Option[T Addable] func(*Foo[T])

func WithValue[T Addable](v T) Option[T] {
 return func(f *Foo[T]) { f.v = v }
}

func NewFoo[T Addable](options ...Option[T]) *Foo[T] {
 foo := new(Foo[T])
 for _, opt := range options {
  opt(foo)
 }
 return foo
}

func main() {
 foo := NewFoo(WithValue(3.14))
 foo.Add(4)
 // 7.140000000000001 (float64)
 fmt.Printf("%v (%T)\n", foo.v, foo.v)
}

总结

本文介绍若干 go1.18 泛型使用的最佳实践示例,在学习的过程中最为关键的还是如何将其应用于实际的项目开发中。

可能有同学有疑问在什么场景下能想到使用泛型开发呢?这其实需要大家在开发过程中逐渐察觉到代码中的“坏味道”,如相同功能的方法反复修改或添加不同类型参数,亦或CI工具检测出代码重复率有显著提升等。这些都值得我们警惕并思考能否使用泛型来防止代码继续腐败~~

图片

附录

  • 下载 go1.18 最新版本的同学请点击这里:https://go.dev/dl/。
  • 官方文档学习的同学戳这里:https://go.dev/doc/go1.18。

Reference

[1]

why-generics: https://go.dev/blog/why-generics

[2]

tutorial-generics: https://go.dev/doc/tutorial/generics

73650[十分钟系列] go1.18 泛型最佳实践实例(一)

这个人很懒,什么都没留下

文章评论