详解如何利用Golang泛型提高编码效率

 更新时间:2023年04月02日 09:59:57   作者:jxwu  
Golang的泛型已经出来有一段时间了,大家应该或多或少对它有所了解。虽然Golang的泛型在功能上确实比较简单,而且确实可能会增加代码的复杂度,过度使用可能还会降低代码可读性。本文就来介绍一下Golang泛型的相关知识吧

前言

Golang的泛型已经出来有一段时间了,大家应该或多或少对它有所了解,甚至已经在应用中使用它。虽然Golang的泛型在功能上确实比较简单,而且确实可能会增加代码的复杂度,过度使用可能还会降低代码可读性。

但不可否认泛型确实让我们在使用Golang的时候能够抽取一些通用的代码,避免代码的重复拷贝,提高代码性能(避免类型转换),提高编码的效率和体验,提高代码可维护性。

这篇文章主要是介绍我使用Golang泛型做过的事情。

工具函数

虽然标准库里面已经提供了大量的工具函数,但是这些工具函数都没有使用泛型实现,为了提高使用体验,我们可以使用泛型进行实现。

比如数值算法里很经典的math.Max()math.Min()都是float64类型的,但是很多时候我们使用的是intint64这些类型,在Golang引入泛型之前,我们经常像下面这样根据类型实现,产生大量模板代码:

func MaxInt(a, b int) int {
	if a > b {
		return a
	}
	return b
}

func MaxInt64(a, b int64) int64 {
	if a > b {
		return a
	}
	return b
}

// ...其他类型

而使用泛型则我们只需要一个实现:

func Max[T constraints.Ordered](a, b T) T {
	if a > b {
		return a
	}
	return b
}

其中constraints.Ordered表示可排序类型,也就是可以使用三路运算符的类型[>, =, <],包含了所有数值类型和string。可以通过go get golang.org/x/exp引入。

代码地址

其他的像json解析参数校验slices等也可以通过泛型进行实现。

数据结构

Golang自带的泛型容器有slices和map,这两个数据结构其实可以完成大部分工作了,但是有时候我们可能还需要其他的数据结构,比如说优先级队列、链表等。

虽然Golang在container包下有heaplistring三个数据结构,但说实话使用起来不是很方便,特别是元素类型全是interface{},使用这些结构就需要各种类型转换。因此我们可以简单的拷贝这些代码,然后使用泛型进行改造,比如heap:

我们不但使用泛型进行实现,还把heap默认改为使用slice是实现,这样只需要实现一个LessFunc,而不是5个。

package heap

type LessFunc[T any] func(e1 T, e2 T) bool

type Heap[T any] struct {
	h        []T
	lessFunc LessFunc[T]
}

func New[T any](h []T, lessFunc LessFunc[T]) *Heap[T] {
	heap := &Heap[T]{
		h:        h,
		lessFunc: lessFunc,
	}
	heap.init()
	return heap
}

// 移除堆顶元素
func (h *Heap[T]) Pop() T {
	n := h.Len() - 1
	h.swap(0, n)
	h.down(0, n)
	return h.pop()
}

// 获取堆顶元素
func (h *Heap[T]) Peek() T {
	return h.h[0]
}

// 添加元素到堆
func (h *Heap[T]) Push(x T) {
	h.push(x)
	h.up(h.Len() - 1)
}

代码地址

其他的数据结构还包括listsetpqueue等。

模板代码

在后台业务代码里面,我们经常会有很多个业务处理函数,每个业务处理函数我们基本都会通过一些代码封装成一个HTTP接口,这里其实基本上都是模板代码,比如说对于一个使用gin实现的HTTP服务,每个接口我们都需要进行以下处理:

  • 指定HTTP方法、URL
  • 鉴权
  • 参数绑定
  • 处理请求
  • 处理响应

可以发现,参数绑定、处理响应几乎都是一样模板代码,鉴权也基本上是模板代码(当然有些鉴权可能比较复杂)。

因此我们可以编写一个泛型模板,把相同的部分抽取出来,用户只需要实现不同接口有差异的指定HTTP方法、URL和处理请求逻辑即可:

// 处理请求
func do[Req any, Rsp any, Opt any](reqFunc ReqFunc[Req],
	serviceFunc ServiceFunc[Req, Rsp], serviceOptFunc ServiceOptFunc[Req, Rsp, Opt], opts ...Opt) gin.HandlerFunc {
	return func(c *gin.Context) {
		// 参数绑定
		req, err := BindJSON[Req](c)
		if err != nil {
			return
		}
		// 进一步处理请求结构体
		if reqFunc != nil {
			reqFunc(c, req)
		}
		var rsp *Rsp
		// 业务逻辑函数调用
		if serviceFunc != nil {
			rsp, err = serviceFunc(c, req)
		} else if serviceOptFunc != nil {
			rsp, err = serviceOptFunc(c, req, opts...)
		} else {
			panic("must set ServiceFunc or ServiceFuncOpt")
		}
		// 处理响应
		ProcessRsp(c, rsp, err)
	}
}

这样,现在一个接口基本上只需要一行代码即可实现(不包括具体业务逻辑函数):

	// 简单请求,不需要认证
	e.GET("/user/info/get", ginrest.Do(nil, GetUserInfo))
	// 认证,绑定UID,处理
        reqFunc := func(c *gin.Context, req *UpdateUserInfoReq) {
		req.UID = GetUID(c)
	} // 这里拆多一步是为了显示第一个参数是ReqFunc
	e.POST("/user/info/update", Verify, ginrest.Do(reqFunc, UpdateUserInfo))

代码地址,实现了一个基于gin的RESTful风格模板。

对象池/缓存

Golang标准库自带了一个线程安全、高性能、还能够根据对象热度自动进行释放的对象池sync.Pool,然而作为对象池,我们一般只会往里面放一种类型的对象,但sync.Pool里面的元素还是interface{}类型,因此我们可以简单的封装sync.Pool,让它里面的元素有具体类型:

这里其实就是简单的对象sync.Pool进行包装,然后添加了一个ClearFunc()在回收对象的时候进行一些清理操作,比如说byte切片我们需要让它的已用长度归零(容量还是不变)。

// 创建新对象
type NewFunc[T any] func() T

// 清理对象
type ClearFunc[T any] func(T) T

type Pool[T any] struct {
	p         sync.Pool
	clearFunc ClearFunc[T]
}

func New[T any](newFunc NewFunc[T], clearFunc ClearFunc[T]) *Pool[T] {
	if newFunc == nil {
		panic("must be provide NewFunc")
	}
	p := &Pool[T]{
		clearFunc: clearFunc,
	}
	p.p.New = func() any {
		return newFunc()
	}
	return p
}

// 获取对象
func (p *Pool[T]) Get() T {
	return p.p.Get().(T)
}

// 归还对象
func (p *Pool[T]) Put(t T) {
	if p.clearFunc != nil {
		t = p.clearFunc(t)
	}
	p.p.Put(t)
}

作为字节数组对象池使用:

	newFunc := func() []byte {
		return make([]byte, size, cap)
	}
	clearFunc := func(b []byte) []byte {
		return b[:0]
	}
	p := New(newFunc, clearFunc)
	bytes := p.Get() // 这里bytes类型是[]byte
	p.Put(bytes)

代码地址

对于缓存也是同理,目前大部分缓存库的实现都是基于interface{}或者是byte[],但是我们还是更加喜欢直接操作具体类型,因此我们可以自己使用泛型实现(或改造)一个缓存库。我自己也实现了一个泛型缓存策略库,里面包含LRU、LFU、ARC、NearlyLRU、TinyLFU等缓存策略。

总结

可以看到,其实Golang泛型主要提供了一种代码抽象、封装的能力,让我们能够写出更能复用的代码,避免代码到处拷贝,从而能够提高代码的可维护性,可读性,还能从避免类型转换中得到一点性能提升。

到此这篇关于详解如何利用Golang泛型提高编码效率的文章就介绍到这了,更多相关Golang泛型内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 详解Go中如何进行进行内存优化和垃圾收集器管理

    详解Go中如何进行进行内存优化和垃圾收集器管理

    这篇文章主要为大家详细介绍了Go中如何进行进行内存优化和垃圾收集器管理,文中的示例代码讲解详细,具有一定的学习价值,感兴趣的小伙伴可以了解下
    2023-11-11
  • golang利用redis和gin实现保存登录状态校验登录功能

    golang利用redis和gin实现保存登录状态校验登录功能

    这篇文章主要介绍了golang利用redis和gin实现保存登录状态校验登录功能,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2024-01-01
  • go语言开发环境配置(sublime text3+gosublime)

    go语言开发环境配置(sublime text3+gosublime)

    网上google了下go的开发工具,大都推荐sublime text3+gosublime,本文就介绍了go语言开发环境配置(sublime text3+gosublime),具有一定的参考价值,感兴趣的可以了解一下
    2022-01-01
  • golang的Pseudo-versions使用问题解析

    golang的Pseudo-versions使用问题解析

    这篇文章主要为大家介绍有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪了golang的Pseudo-versions使用问题解析,
    2023-07-07
  • Golang开发之接口的具体使用详解

    Golang开发之接口的具体使用详解

    在 Golang 中,接口是一种类型,它是由一组方法签名组成的抽象集合。这篇文章主要为大家介绍了Golang接口的具体使用,希望对大家有所帮助
    2023-04-04
  • 浅析Golang中make和new的用法区别

    浅析Golang中make和new的用法区别

    在Go语言中,有两个比较雷同的内置函数,分别是new和make方法,二者都可以用来分配内存,那他们有什么区别呢?下面就跟随小编一起来学习一下吧
    2024-02-02
  • 浅谈Go语言不提供隐式数字转换的原因

    浅谈Go语言不提供隐式数字转换的原因

    本文主要介绍了浅谈Go语言不提供隐式数字转换的原因,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-03-03
  • Golang中gorm无法将字段更新为空值

    Golang中gorm无法将字段更新为空值

    本文主要介绍了Golang中gorm无法将字段更新为空值,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-05-05
  • 深入理解Golang Channel 的底层结构

    深入理解Golang Channel 的底层结构

    这篇文章主要介绍了深入理解Golang Channel 的底层结构,Go 语言的 channel 底层是什么数据结构?下面我们就一起来深入解析一下 channel,需要的朋友可以参考下
    2022-01-01
  • golang之数组切片的具体用法

    golang之数组切片的具体用法

    本文主要介绍了golang之数组切片的具体用法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-07-07

最新评论