导读 introduction 本文整理了很多的泛型应用技巧,结合具体的实际代码示例,特别是很多直接对go语言内置的类库的实现进行改造,再通过两者在使用上直观对比,帮助大家对泛型使用思考上提供了更多思路,定会帮助大家在应用泛型能力上有很多的提升与启发。 geek talk
01
前言
泛型功能是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 < y { return x } return y} 1.2 类型集合 type set
类型集合是为了简化泛型约束的使用,提升阅读性,同时增加了复用能力,方式他通过接口定义的方式使用。
编写格式参见如下:
type constraint1 interface { type1 | ~type2 | ...} 以下示例定义了有符号整型与无符号整型的泛型约束:
// signed is a constraint that permits any signed integer type.// if future releases of go add new predeclared signed integer types,// this constraint will be modified to include them.type signed interface { ~int | ~int8 | ~int16 | ~int32 | ~int64}// unsigned is a constraint that permits any unsigned integer type.// if future releases of go add new predeclared unsigned integer types,// this constraint will be modified to include them.type unsigned interface { ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr} 类型集合也支持继承的方式,简化复用。使用方式也接口的继承是完全一致。
以下示例定义把signed和unsigned进行了组合,用于表达对整型泛型约束的定义。
// integer is a constraint that permits any integer type.// if future releases of go add new predeclared integer types,// this constraint will be modified to include them.type integer interface { signed | unsigned} 1.3 类型推导
引入类型推导,可以简化我们代码工作,目前支持2种类型推导功能。
通过函数的实参推导出来具体的类型以前提到的min函数为例,可能通过传入的传数类型来判断推导。
var a, b uintminuint := min(a, b) // 不再需要这样写 min[uint](a, b)fmt.println(minuint)minint := min(10, 100) // 常量数字,go语言会默认为 int类型,不再需要这样写 min[int](a, b)fmt.println(minint)
geek talk
02
巧用泛型,实现通用排序函数
对一个数组进行排序是在业务开发中使用非常频繁的功能,go语言提供了sort.sort函数,提供高效的排序功能支持,但它要求目标数组必须要实现 sort.interface接口。// an implementation of interface can be sorted by the routines in this package.// the methods refer to elements of the underlying collection by integer index.type interface interface { // len is the number of elements in the collection. len() int // less reports whether the element with index i // must sort before the element with index j. // // if both less(i, j) and less(j, i) are false, // then the elements at index i and j are considered equal. // sort may place equal elements in any order in the final result, // while stable preserves the original input order of equal elements. // // less must describe a transitive ordering: // - if both less(i, j) and less(j, k) are true, then less(i, k) must be true as well. // - if both less(i, j) and less(j, k) are false, then less(i, k) must be false as well. // // note that floating-point comparison (the < operator on float32 or float64 values) // is not a transitive ordering when not-a-number (nan) values are involved. // see float64slice.less for a correct implementation for floating-point values. less(i, j int) bool // swap swaps the elements with indexes i and j. swap(i, j int)} 这样导致我们对不同元素类型的数组,都需要重复实现这个接口,编写了很多类似的代码。下面是官方给的一个排序的示例,可以看到实现这样的排序功能,要写这么多代码。
type person struct { name string age int}// byage implements sort.interface for []person based on// the age field.type byage []personfunc (a byage) len() int { return len(a) }func (a byage) swap(i, j int) { a[i], a[j] = a[j], a[i] }func (a byage) less(i, j int) bool { return a[i].age = 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.go
geek talk
03
巧用泛型,简化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 talk
04
巧用泛型,实现通用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 <= i < h.len() and 2*i+1 <= j <= 2*i+2 and j < h.len()//// note that push and pop in this interface are for package heap's// implementation to call. to add and remove things from the heap,// use heap.push and heap.pop.type interface interface { sort.interface push(x any) // add x as element len() pop() any // remove and return element len() - 1.} 下面的代码示例是来自go语言官方,实现了对int类型元素的优先级队列实现:
import ( container/heap fmt)// an intheap is a min-heap of ints.type intheap []intfunc (h intheap) len() int { return len(h) }func (h intheap) less(i, j int) bool { return h[i] 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.interfacefunc (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 < 0}func (h *heapst[e]) swap(i, j int) { h.data[i], h.data[j] = h.data[j], h.data[i] }func (h *heapst[e]) push(x any) { // push and pop use pointer receivers because they modify the slice's length, // not just its contents. v := append(h.data, x.(e)) h.data = v}func (h *heapst[e]) pop() any { old := h.data n := len(old) x := old[n-1] h.data = old[0 : n-1] return x}// heap base on generics to build a heap tree for any typetype heap[e any] struct { data *heapst[e]}// push pushes the element x onto the heap.// the complexity is o(log n) where n = h.len().func (h *heap[e]) push(v e) { heap.push(h.data, v)}// pop removes and returns the minimum element (according to less) from the heap.// the complexity is o(log n) where n = h.len().// pop is equivalent to remove(h, 0).func (h *heap[e]) pop() e { return heap.pop(h.data).(e)}func (h *heap[e]) element(index int) (e e, err error) { if index = 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.go
geek talk
05
巧用泛型,提升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-02t1505z 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-02t1505z path=/search?q=flowers} 完整的代码实现与使用示例可参见:https://github.com/jhunters/goassist/tree/main/concurrent/syncx
geek talk
06
巧用泛型,增强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.go
geek talk
07
总结
应用泛型可以极大的减少代码的编写量,同时提升可读性与安全性都有帮助,上面提到的对于泛型的使用技巧只能是很少一部分,还有更多的case等待大家来发现。另外对泛型感兴趣的同学,也推荐大家阅读这个开源项目 https://github.com/jhunters/goassist 里面有非常多经典的泛型使用技巧,相信对大家理解与掌握泛型会有很多帮助。
科技体制重点领域和关键环节改革取得实质性进展
车联网为信息通信产业带来的发展机遇
固态继电器的构成_固态继电器的优缺点
Win8 RTM最终版7月下旬公布 版本号将为8500
基于5G网络传输的无人机360度全景4K高清视频的即拍即传
巧用Golang泛型,简化代码编写
Temperature Sensor ICs Simplif
造工精致的单反相机“肢解”图
诺基亚如何真正释放5G力量和潜能方面的下一基石?
如何才能增强接线端子的实际防雷效果
小喇叭选择UV胶水的优势都有哪些
汽车生产车间中多个智能装备如何实现数据采集和远程监控
激光报警系统在监狱看守所周界的应用方案(下篇)
关于区块链矿工费的常见问题解答
自制最简单收音机
Maxim MAX17129低成本6串白光LED驱动方案
德赛西威携手行业伙伴协同共建车路云一体化的高质量融合发展
什么是IP地址?
如何判断需要几个触发器 如何判断触发器能否自启动
手持万用表购买指南