|
作者 百度小程序团队introduction本文整理了很多的泛型应用技巧,结合具体的实际代码示例,特别是很多直接对Go语言内置的类库的实现进行改造,再通过两者在使用上直观对比,帮助大家对泛型使用思考上提供了更多思路,定会帮助大家在应用泛型能力上有很多的提升与启发。全文16699字,预计阅读时间42分钟。GEEK TALK01前言泛型功能是Go语言在1.18版本引入的功能,可以说是Go语言开源以来最大的语法特性变化,其改动和影响都很大, 所以整个版本的开发周期,测试周期都比以往要长很多。接下来为了大家更好的理解文章中的代码示例,先再简单介绍一下 Go语言在1.18版本加入的泛型的基本使用方法。从官方的资料来看,泛型增加了三个新的重要内容:函数和类型新增对类型形参(type parameters)的支持。将接口类型定义为类型集合,包括没有方法的接口类型。支持类型推导,大多数情况下,调用泛型函数时可省略类型实参(type arguments)。1.1 Type Parameter参数泛型类型(Type Parameter)可以说是泛型使用过程应用最多的场景了, 一般应用于方法或函数的形参或返回参数上。参数泛型类型基本的使用格式可参见如下:func?FuncName[P,?Q?constraint1,?R?constraint2,?...](parameter1?P,?parameter2?Q,?...)?(R,?Q,?...)说明: 参数泛型类定义后,可以用于函数的形参或返回参数上。下面是一个应用参数泛型类型的代码示例:// Min return the min onefunc Min[E int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64 | uintptr | ~string](x, y E) E { if x = 0}接入下来,实现一个支持泛型的排序函数,对任何类型的数组进行排序。func Sort[E any](data []E, cmp base.CMP[E]) { sortobject := sortable[E]{data: data, cmp: cmp} sort.Sort(sortobject)}至此,我们就已经实现一个通用的排序函数了, 应用这个函数,上面官方给出的排序实现就可以简化如下:type Person struct { Name string Age int}people := []Person{ {"Bob", 31}, {"John", 42}, {"Michael", 17}, {"Jenny", 26},}people = Sort(people, func(e1, e2 Person) int { return e1.Age - e2.Age})// Output:// [Michael: 17 Jenny: 26 Bob: 31 John: 42]可以看到, 应用泛型后,只需要简单的一个函数调用就可以了。完整的代码实现可参见:https://github.com/jhunters/goassist/blob/main/arrayutil/array.goGEEK TALK03巧用泛型,简化strconv.Append系列函数Go语言内置的strconv包的api也是日常开发经常使用的, 它提供的Append系列函数可以实现高效的字符串拼接功能,但因为Go语言不支持重载,所以会看到因为接受参数类型的不同,需要选择不同的函数。func AppendBool(dst []byte, b bool) []bytefunc AppendFloat(dst []byte, f float64, fmt byte, prec, bitSize int) []bytefunc AppendInt(dst []byte, i int64, base int) []bytefunc AppendQuote(dst []byte, s string) []bytefunc AppendQuoteRune(dst []byte, r rune) []bytefunc AppendQuoteRuneToASCII(dst []byte, r rune) []bytefunc AppendQuoteRuneToGraphic(dst []byte, r rune) []bytefunc AppendQuoteToASCII(dst []byte, s string) []bytefunc AppendQuoteToGraphic(dst []byte, s string) []bytefunc AppendUint(dst []byte, i uint64, base int) []byte所以我们不得不面临以下使用的窘境。// append boolb := []byte("bool:")b = strconv.AppendBool(b, true)fmt.Println(string(b))// append intb10 := []byte("int (base 10):")b10 = strconv.AppendInt(b10, -42, 10)fmt.Println(string(b10))// append quoteb := []byte("quote:")b = strconv.AppendQuote(b, `"Fran & Freddie's Diner"`)fmt.Println(string(b))接下来,我们用泛型来简化一下代码,让其只需要一个函数就能搞定, 直接上代码如下:// Append convert e to string and appends to dstfunc Append[E any](dst []byte, e E) []byte { toAppend := fmt.Sprintf("%v", e) return append(dst, []byte(toAppend)...)}再来看看应用后的效果,修改之前的示例:// append boolb := []byte("bool:")b = conv.Append(b, true)fmt.Println(string(b))// append intb10 := []byte("int:")b10 = conv.Append(b10, -42)fmt.Println(string(b10))// append quoteb = []byte("quote:")b = conv.Append(b, `"Fran & Freddie's Diner"`)fmt.Println(string(b))GEEK TALK04巧用泛型,实现通用heap容器,简化使用Go语言container/heap包提供了一个优先级队列功能, 以实现在Pop数里时,总是优先获得优先级最高的节点。同样的问题,如果要应用heap包的功能,针对不同的对象,必须要 实现 heap.Interface接口, ?包括5个方法。// The Interface type describes the requirements// for a type using the routines in this package.// Any type that implements it may be used as a// min-heap with the following invariants (established after// Init has been called or if the data is empty or sorted)://// !h.Less(j, i) for 0 0 { fmt.Printf("%d ", heap.Pop(h)) } // Output: // minimum: 1 // 1 2 3 5}看到上面写了这么多的代码才把功能实现, 想必大家都觉得太繁琐了吧??那我们用泛型来改造一下,大致思路如下:实现一个支持泛型参数的结构体heapST,实现heap.Interface接口。开放比较函数的功能,用于使用方来更灵活的设置排序要求。封装一个全新的带泛型参数传入Heap结构体, 来封装Pop与Push方法的实现。主要的代码实现如下:type heapST[E any] struct { data []E cmp base.CMP[E]}// implments the methods for "heap.Interface"func (h *heapST[E]) Len() int { return len(h.data) }func (h *heapST[E]) Less(i, j int) bool { v := h.cmp(h.data[i], h.data[j]) return v = h.data.Len() { return e, fmt.Errorf("out of index") } return h.data.data[index], nil}// Remove removes and returns the element at index i from the heap.// The complexity is O(log n) where n = h.Len().func (h *Heap[E]) Remove(index int) E { return heap.Remove(h.data, index).(E)}func (h *Heap[E]) Len() int { return len(h.data.data)}// Copy to copy heapfunc (h *Heap[E]) Copy() *Heap[E] { ret := heapST[E]{cmp: h.data.cmp} ret.data = make([]E, len(h.data.data)) copy(ret.data, h.data.data) heap.Init(&ret) return &Heap[E]{&ret}}// NewHeap return Heap pointer and init the heap treefunc NewHeap[E any](t []E, cmp base.CMP[E]) *Heap[E] { ret := heapST[E]{data: t, cmp: cmp} heap.Init(&ret) return &Heap[E]{&ret}}完整的代码获取:https://github.com/jhunters/goassist/blob/main/container/heapx/heap.go接入来可以改写之前的代码, 代码如下:// An IntHeap is a min-heap of ints.type IntHeap []int// This example inserts several ints into an IntHeap, checks the minimum,// and removes them in order of priority.func Example_intHeap() { h := heapx.NewHeap(IntHeap{2, 1, 5}, func(p1, p2 int) int { return p1 - p2 }) h.Push(3) for h.Len() > 0 { fmt.Printf("%d ", h.Pop()) } // Output: // 1 2 3 5}可以看到改写后,代码量大量减少,而且代码的可读性也大大提升. ?完整的使用示例可参见:https://github.com/jhunters/goassist/blob/main/container/heapx/heap_test.goGEEK TALK05巧用泛型,提升Pool容器可读性与安全性Go语言内存的sync包下Pool对象, 提供了可伸缩、并发安全的临时对象池的功能,用来存放已经分配但暂时不用的临时对象,通过对象重用机制,缓解 GC 压力,提高程序性能。需要注意的是Pool 是一个临时对象池,适用于储存一些会在 goroutine 间共享的临时对象,其中保存的任何项都可能随时不做通知地释放掉,所以不适合当于缓存或对象池的功能。Pool的框架代码如下:type Pool struct { // New optionally specifies a function to generate // a value when Get would otherwise return nil. // It may not be changed concurrently with calls to Get. New func() interface{} // contains filtered or unexported fields}// Get 从 Pool 中获取元素。当 Pool 中没有元素时,会调用 New 生成元素,新元素不会放入 Pool 中。若 New 未定义,则返回 nil。func (p *Pool) Get() interface{}// Put 往 Pool 中添加元素 x。func (p *Pool) Put(x interface{})官方Pool的API使用起来已经是非常方便,下面是摘取官方文档中的示例代码:package sync_testimport ( "bytes" "io" "os" "sync" "time")var bufPool = sync.Pool{ New: func() any { // The Pool's New function should generally only return pointer // types, since a pointer can be put into the return interface // value without an allocation: return new(bytes.Buffer) },}// timeNow is a fake version of time.Now for tests.func timeNow() time.Time { return time.Unix(1136214245, 0)}func Log(w io.Writer, key, val string) { b := bufPool.Get().(*bytes.Buffer) b.Reset() // Replace this with time.Now() in a real logger. b.WriteString(timeNow().UTC().Format(time.RFC3339)) b.WriteByte(' ') b.WriteString(key) b.WriteByte('=') b.WriteString(val) w.Write(b.Bytes()) bufPool.Put(b)}func ExamplePool() { Log(os.Stdout, "path", "/search?q=flowers") // Output: 2006-01-02T15:04:05Z path=/search?q=flowers}从上面的代码,可以看到一个问题就是从池中获取对象时,要强制进行转换,如果转换类型不匹配,就会出现Panic异常,这种场景正是泛型可以很好解决的场景,我们改造代码如下, 封装一个全新的带泛型参数传入 Pool ?结构体:package syncximport ( "sync" "github.com/jhunters/goassist/base")type Pool[E any] struct { New base.Supplier[E] internal sync.Pool}// NewPoolX create a new PoolXfunc NewPool[E any](f base.Supplier[E]) *Pool[E] { p := Pool[E]{New: f} p.internal = sync.Pool{ New: func() any { return p.New() }, } return &p}// Get selects an E generic type item from the Poolfunc (p *Pool[E]) Get() E { v := p.internal.Get() return v.(E)}// Put adds x to the pool.func (p *Pool[E]) Put(v E) { p.internal.Put(v)}接下来,使用新封装的Pool对象改写上面的官方示例代码:var bufPool = syncx.NewPool(func() *bytes.Buffer { return new(bytes.Buffer)})// timeNow is a fake version of time.Now for tests.func timeNow() time.Time { return time.Unix(1136214245, 0)}func Log(w io.Writer, key, val string) { b := bufPool.Get() // 不再需要强制类型转换 b.Reset() // Replace this with time.Now() in a real logger. b.WriteString(timeNow().UTC().Format(time.RFC3339)) b.WriteByte(' ') b.WriteString(key) b.WriteByte('=') b.WriteString(val) w.Write(b.Bytes()) bufPool.Put(b)}func ExamplePool() { Log(os.Stdout, "path", "/search?q=flowers") // Output: 2006-01-02T15:04:05Z path=/search?q=flowers}完整的代码实现与使用示例可参见:https://github.com/jhunters/goassist/tree/main/concurrent/syncxGEEK TALK06巧用泛型,增强sync.Map容器功能sync.Map是Go语言官方提供的一个map映射的封装实现,提供了一些更实用的方法以更方便的操作map映射,同时它本身也是线程安全的,包括原子化的更新支持。type Map func (m *Map) Delete(key any) func (m *Map) Load(key any) (value any, ok bool) func (m *Map) LoadAndDelete(key any) (value any, loaded bool) func (m *Map) LoadOrStore(key, value any) (actual any, loaded bool) func (m *Map) Range(f func(key, value any) bool) func (m *Map) Store(key, value any)接入来我们要用泛型功能,给sync.Map增加如下功能:所有的操作支持泛型,以省去对象强制转换功能引入泛型后,保障了key与value类型的一致性,可以扩展支持 Key或Value是否存在, 查询最小最大Key或Value的功能另外还增加了StoreAll 从另一个map导入, ToMap转成原生map结构, ?Clear清空map, ? 以数组结构导出key或value等实用功能增加后Map的API列表如下:type Map func NewMap[K comparable, V any]() *Map[K, V] func (m *Map[K, V]) Clear() func (m *Map[K, V]) Copy() *Map[K, V] func (m *Map[K, V]) Exist(key K) bool func (m *Map[K, V]) ExistValue(value V) (k K, exist bool) func (m *Map[K, V]) ExistValueWithComparator(value V, equal base.EQL[V]) (k K, exist bool) func (m *Map[K, V]) Get(key K) (V, bool) func (m *Map[K, V]) IsEmpty() (empty bool) func (m *Map[K, V]) Keys() []K func (m *Map[K, V]) MaxKey(compare base.CMP[K]) (key K, v V) func (m *Map[K, V]) MaxValue(compare base.CMP[V]) (key K, v V) func (m *Map[K, V]) MinKey(compare base.CMP[K]) (key K, v V) func (m *Map[K, V]) MinValue(compare base.CMP[V]) (key K, v V) func (m *Map[K, V]) Put(key K, value V) V func (m *Map[K, V]) Range(f base.BiFunc[bool, K, V]) func (m *Map[K, V]) Remove(key K) bool func (m *Map[K, V]) Size() int func (m *Map[K, V]) ToMap() map[K]V func (m *Map[K, V]) Values() []V完整的API列表在此阅读:http://localhost:4040/pkg/github.com/jhunters/goassist/container/mapx/部分泛型代码后的代码如下:// Map is like a Go map[interface{}]interface{} but is safe for concurrent use// by multiple goroutines without additional locking or coordination.// Loads, stores, and deletes run in amortized constant time.// By generics feature supports, all api will be more readable and safty.//// The Map type is specialized. Most code should use a plain Go map instead,// with separate locking or coordination, for better type safety and to make it// easier to maintain other invariants along with the map content.//// The Map type is optimized for two common use cases: (1) when the entry for a given// key is only ever written once but read many times, as in caches that only grow,// or (2) when multiple goroutines read, write, and overwrite entries for disjoint// sets of keys. In these two cases, use of a Map may significantly reduce lock// contention compared to a Go map paired with a separate Mutex or RWMutex.//// The zero Map is empty and ready for use. A Map must not be copied after first use.type Map[K comparable, V any] struct { mp sync.Map empty V mu sync.Mutex}// NewMap create a new mapfunc NewMap[K comparable, V any]() *Map[K, V] { return &Map[K, V]{mp: sync.Map{}}}// NewMapByInitial create a new map and store key and value from origin mapfunc NewMapByInitial[K comparable, V any](mmp map[K]V) *Map[K, V] { mp := NewMap[K, V]() if mmp == nil { return mp } for k, v := range mmp { mp.Store(k, v) } return mp}// Exist return true if key existfunc (m *Map[K, V]) Exist(key K) bool { _, ok := m.mp.Load(key) return ok}// ExistValue return true if value existfunc (m *Map[K, V]) ExistValue(value V) (k K, exist bool) { de := reflectutil.NewDeepEquals(value) m.Range(func(key K, val V) bool { if de.Matches(val) { exist = true k = key return false } return true }) return}// ExistValue return true if value existfunc (m *Map[K, V]) ExistValueWithComparator(value V, equal base.EQL[V]) (k K, exist bool) { m.Range(func(key K, val V) bool { if equal(value, val) { exist = true k = key return false } return true }) return}// ExistValue return true if value existfunc (m *Map[K, V]) ExistValueComparable(v base.Comparable[V]) (k K, exist bool) { m.Range(func(key K, val V) bool { if v.CompareTo(val) == 0 { exist = true k = key return false } return true }) return}// MinValue to return min value in the mapfunc (m *Map[K, V]) MinValue(compare base.CMP[V]) (key K, v V) { return selectByCompareValue(m, func(o1, o2 V) int { return compare(o1, o2) })}// MaxValue to return max value in the mapfunc (m *Map[K, V]) MaxValue(compare base.CMP[V]) (key K, v V) { return selectByCompareValue(m, func(o1, o2 V) int { return compare(o2, o1) })}// MinKey to return min key in the mapfunc (m *Map[K, V]) MinKey(compare base.CMP[K]) (key K, v V) { return selectByCompareKey(m, func(o1, o2 K) int { return compare(o1, o2) })}// MaxKey to return max key in the mapfunc (m *Map[K, V]) MaxKey(compare base.CMP[K]) (key K, v V) { return selectByCompareKey(m, func(o1, o2 K) int { return compare(o2, o1) })}完整的代码与使用示例参见:[1]https://github.com/jhunters/goassist/blob/main/concurrent/syncx/map.go[2]https://github.com/jhunters/goassist/blob/main/concurrent/syncx/example_map_test.goGEEK TALK07总结应用泛型可以极大的减少代码的编写量,同时提升可读性与安全性都有帮助,上面提到的对于泛型的使用技巧只能是很少一部分,还有更多的case等待大家来发现。另外对泛型感兴趣的同学,也推荐大家阅读这个开源项目 https://github.com/jhunters/goassist 里面有非常多经典的泛型使用技巧,相信对大家理解与掌握泛型会有很多帮助。?END参考资料:[1]?Go语言官方泛型使用介绍:https://golang.google.cn/doc/tutorial/generics[2]《using generics ?in go by Ian》Go语言泛型专题分享:https://www.bilibili.com/video/BV1KP4y157rn/?p=1&share_medium=iphone&share_plat=ios&share_session_id=B63420EF-45D0-4464-A195-F27C13188C75&share_source=WEIXIN&share_tag=s_i×tamp=1636179512&unique_k=ORZiX1[3]专为Go语言开发者提供一套基础api库,包括了非常多的泛型应用:https://github.com/jhunters/goassist推荐阅读:Diffie-Hellman密钥协商算法探究贴吧低代码高性能规则引擎设计浅谈权限系统在多利熊业务应用分布式系统关键路径延迟分析实践百度工程师教你玩转设计模式(装饰器模式)百度工程师带你体验引擎中的nodejs
|
|