一个让业务开发效率提升10倍的golang库

一个让业务开发效率提升10倍的golang库

此文除了是标题党,没有什么其余问题。前端

这篇文章推荐一个库,https://github.com/jianfengye/collection。 这个库是我在开发业务过程当中 Slice 的频繁致使业务开发效率低,就产生了要作一个 Collection 包的想法。本文说说我开发这个库的来龙去脉git

Golang 适不适合写业务?

最近一个逻辑很是复杂的业务,我用 Golang 来开发。开发过程不断在问一个问题,Golang 适不适合写业务?github

业务说到底,是一大堆的逻辑,大量的逻辑都是在几个环节:获取数据,封装数据,组织数据,过滤数据,排序结果。获取/封装数据,即从 DB 中根据查询 SQL,获取表中的数据,并封装成数据结构。组织数据,例如,当我有两份数据源,我须要将两份数据源按照某个字段合并,那么这种组织数据的能力也是很是须要的。过滤数据,我获取的字段有10个,可是我只须要给前端返回3个就够了;排序结果,返回的结构按照某种顺序。这些都是咱们在写业务中,每一个业务逻辑都会遇到的问题。一款适合作业务的语言必定是在这些环节上都提供足够的便利性的。golang

图片

我想,符合业务语义的语言才有将来!!数组

什么是业务语义呢?就是咱们开发人员和产品人员交流的语言。感觉一下,好比 “将这个名单中成绩按照从大到小排列,而且成绩大于60的最后一个学生找出来” 这么一句话的需求,就是咱们经常和产品人员交流的语言。而咱们开发中使用到的语言/框架/库,又是一种思惟和语言。当咱们接到上述的需求,若是咱们头脑中浮现的逻辑是“我要使用快速排序,而后在快速排序循环中能直接找到成绩大于60的,还要是最后一个,因此我可能须要有个 min 变量”。那么我只能说,或许你的代码运行效率足够高,可是一旦业务复杂了,你的代码开发效率必定很低。像上述的需求,咱们按照伪码来讲,最但愿是有一门语言能支持:collection().sortDesc().Last(score > 60) 这样符合业务语义的代码。安全

如图,若是说高级语言是拉近了机器语言和业务语义的距离,那么开发Collection包的愿景也是但愿拉近 Golang 这门高级语言和 业务语言的距离。数据结构

Collection包目标是用于替换golang原生的Slice,使用场景是在大量不追求极致性能,追求业务开发效能的场景。框架

图片

展现

业务开发最核心的也就是对数组的处理,Collection封装了多种数据数组类型。函数

Collection包目前支持的元素类型:int, int64, float32, float64, string, struct。除了struct数组使用了反射以外,其余的数组并无使用反射机制,效率和易用性获得必定的平衡。性能

使用下列几个方法进行初始化Collection:

NewIntCollection(objs []int) *IntCollection

NewInt64Collection(objs []int64) *Int64Collection

NewFloat64Collection(objs []float64) *Float64Collection

NewFloat32Collection(objs []float32) *Float32Collection

NewStrCollection(objs []string) *StrCollection

NewObjCollection(objs interface{}) *ObjCollection

全部的初始化函数都是很方便的将要初始化的slice传递进入,返回了一个实现了ICollection的具体对象。

下面作一些Collection中函数的展现。

友好的格式展现

首先业务是很须要进行代码调试的,这里封装了一个 DD 方法,能按照友好的格式展现这个 Collection

a1 := Foo{A: "a1"}
a2 := Foo{A: "a2"}

objColl := NewObjCollection([]Foo{a1, a2})
objColl.DD()

/*
ObjCollection(2)(collection.Foo):{
    0:  {A:a1}
    1:  {A:a2}
}
*/

intColl := NewIntCollection([]int{1,2})
intColl.DD()

/*
IntCollection(2):{
    0:  1
    1:  2
}
*/

查找功能

在一个数组中查找对应的元素,这个是很是常见的功能

Search(item interface{}) int

查找Collection中第一个匹配查询元素的下标,若是存在,返回下标;若是不存在,返回-1

注意 此函数要求设置compare方法,基础元素数组(int, int64, float32, float64, string)可直接调用!

intColl := NewIntCollection([]int{1,2})
if intColl.Search(2) != 1 {
    t.Error("Search 错误")
}

intColl = NewIntCollection([]int{1,2, 3, 3, 2})
if intColl.Search(3) != 2 {
    t.Error("Search 重复错误")
}

排重功能

将Collection中重复的元素进行合并,返回惟一的一个数组。

intColl := NewIntCollection([]int{1,2, 3, 3, 2})
uniqColl := intColl.Unique()
if uniqColl.Count() != 3 {
    t.Error("Unique 重复错误")
}

uniqColl.DD()
/*
IntCollection(3):{
    0:  1
    1:  2
    2:  3
}
*/

获取最后一个

获取该Collection中知足过滤的最后一个元素,若是没有填写过滤条件,默认返回最后一个元素

intColl := NewIntCollection([]int{1, 2, 3, 4, 3, 2})
last, err := intColl.Last().ToInt()
if err != nil {
    t.Error("last get error")
}
if last != 2 {
    t.Error("last 获取错误")
}

last, err = intColl.Last(func(item interface{}, key int) bool {
    i := item.(int)
    return i > 2
}).ToInt()

if err != nil {
    t.Error("last get error")
}
if last != 3 {
    t.Error("last 获取错误")
}

Map & Reduce

Map

Map(func(item interface{}, key int) interface{}) ICollection

对Collection中的每一个函数都进行一次函数调用,并将返回值组装成ICollection

这个回调函数形如: func(item interface{}, key int) interface{}

若是但愿在某此调用的时候停止,就在这次调用的时候设置Collection的Error,就能够停止,且这次回调函数生成的结构不合并到最终生成的ICollection。

intColl := NewIntCollection([]int{1, 2, 3, 4})
newIntColl := intColl.Map(func(item interface{}, key int) interface{} {
    v := item.(int)
    return v * 2
})
newIntColl.DD()

if newIntColl.Count() != 4 {
    t.Error("Map错误")
}

newIntColl2 := intColl.Map(func(item interface{}, key int) interface{} {
    v := item.(int)

    if key > 2 {
        intColl.SetErr(errors.New("break"))
        return nil
    }

    return v * 2
})
newIntColl2.DD()

/*
IntCollection(4):{
    0:  2
    1:  4
    2:  6
    3:  8
}
IntCollection(3):{
    0:  2
    1:  4
    2:  6
}
*/

Reduce

Reduce(func(carry IMix, item IMix) IMix) IMix

对Collection中的全部元素进行聚合计算。

若是但愿在某次调用的时候停止,在这次调用的时候设置Collection的Error,就能够停止调用。

intColl := NewIntCollection([]int{1, 2, 3, 4})
sumMix := intColl.Reduce(func(carry IMix, item IMix) IMix {
    carryInt, _ := carry.ToInt()
    itemInt, _ := item.ToInt()
    return NewMix(carryInt + itemInt)
})

sumMix.DD()

sum, err := sumMix.ToInt()
if err != nil {
    t.Error(err.Error())
}
if sum != 10 {
    t.Error("Reduce计算错误")
}

/*
IMix(int): 10
*/

排列

将Collection中的元素进行升序排列输出

intColl := NewIntCollection([]int{2, 4, 3})
intColl2 := intColl.Sort()
if intColl2.Err() != nil {
    t.Error(intColl2.Err())
}
intColl2.DD()

/*
IntCollection(3):{
    0:  2
    1:  3
    2:  4
}
*/

合并

Join(split string, format ...func(item interface{}) string) string

将Collection中的元素按照某种方式聚合成字符串。该函数接受一个或者两个参数,第一个参数是聚合字符串的分隔符号,第二个参数是聚合时候每一个元素的格式化函数,若是没有设置第二个参数,则使用fmt.Sprintf("%v")来该格式化

intColl := NewIntCollection([]int{2, 4, 3})
out := intColl.Join(",")
if out != "2,4,3" {
    t.Error("join错误")
}
out = intColl.Join(",", func(item interface{}) string {
    return fmt.Sprintf("'%d'", item.(int))
})
if out != "'2','4','3'" {
    t.Error("join 错误")
}

核心

继承

Collection 包的核心思想也就是继承。可是在 Golang 中的继承,特别是抽象类是没有办法实现的。我这里使用了实现了自身接口的属性Parent来实现的。

首先定义 ICollection 接口,在这个接口中定义好全部的方法。其次建立了 AbsCollection 这个 struct。首先它自身实现了 ICollection 方法,其次,它有个 Parent 属性实现了 ICollection方法,这个 Parent 属性是存放指向真正的实现类的方法,好比 IntCollection。最后,IntCollection/Float32Collection 等都是实现了 AbsCollection。这里显式写实现了 AbsCollection 有几个好处,一个是强制必须实现 ICollection的方法,其次,一些在具体实现类中不同的方法,能够在实现类中重写了。而且最后,为每一个实现类实现了一个New方法。

图片

图片

IMix

固然,因为是强类型语言,不少函数在定义的时候,返回值是没法肯定类型的,固然这里能够简单的使用一个interface来作,可是这样易用性其实又下降了,每次函数调用就必须坐下类型判断。再加上后续回说到的 error 处理的问题。因此我设计了一个 IMix 接口,由实现了这个接口的对象来进行类型转换,ToString, ToInt64 等。固然我也为 IMix 设计了 DD() 方便调试的方法。

图片

AbsCollection

上面说了继承,AbsCollection 是我定位的抽象类,它的思想是一辈子二,二生万物的思想。就是有一些原子方法(好比Insert方法)是根据不一样的数组对象而不一样的。这些方法在AbsCollection 层的实现就是调用 Parent 的具体实现方法。而其余的 AbsCollection 中的通用方法则使用这些原子进行实现。

一共给具体的父实现类定义了6个方法,后续一旦有新的类型添加的需求,只须要保证他能实现了这6个方法便可使用其余的方法了。

图片

图片

特点

下面说说这个包设计的一些特点。

可选参数

Collection 使用了大量的可选参数,好比 Collection.Slice方法。

Slice(...int) ICollection

获取Collection中的片断,能够有两个参数或者一个参数。

若是是两个参数,第一个参数表明开始下标,第二个参数表明结束下标,当第二个参数为-1时候,就表明到Collection结束。

若是是一个参数,则表明从这个开始下标一直获取到Collection结束的片断。

intColl := NewIntCollection([]int{1, 2, 3, 4, 5})
retColl := intColl.Slice(2)
if retColl.Count() != 3 {
    t.Error("Slice 错误")
}

retColl.DD()

retColl = intColl.Slice(2,2)
if retColl.Count() != 2 {
    t.Error("Slice 两个参数错误")
}

retColl.DD()

retColl = intColl.Slice(2, -1)
if retColl.Count() != 3 {
    t.Error("Slice第二个参数为-1错误")
}

retColl.DD()

/*
IntCollection(3):{
    0:  3
    1:  4
    2:  5
}
IntCollection(2):{
    0:  3
    1:  4
}
IntCollection(3):{
    0:  3
    1:  4
    2:  5
}
*/

是否使用可选方法我纠结了好久,由于这种可选参数毕竟仍是不够美观的。不事后来仍是想到了Collection这个包的设计宗旨是方便业务开发。那么业务开发使用者使用的爽的程度才是这个包应该关心的,因此也就大量使用了这种对使用者灵活友好,可是略不美观的实现方式。

图片

链式调用 & 错误处理

链式调用是我在实现这个包的时候一直坚持的。由于复杂的业务逻辑,链式调用的写法阅读性是很高的。因此在全部能返回数组的函数中,我都返回了 ICollection 接口。以方便于后续调用。

图片

可是 Golang 中还有一个 error 的处理问题。每一个函数调用其实都是有可能有错误的,这个错误若是直接返回,那么链式调用必然就不可行了。我采用的方式是火丁[文章]中说到的错误处理机制。当错误出现的时候,我把错误挂载在当前或者返回的 IColleciton,或者返回的 IMix 中。而且提供了 Error() 方法来让外部用户获取确认这个链式调用是否有错误。

图片

这样的错误处理机制是我如今能想到的最好的处理机制了(在 Go 2.0 handle error没有出来以前)。它一方面兼顾了链式调用,一方面能进行错误检查。固然这种方式的错误检查机制等于弱化,不是在每次调用函数的时候强制用户检查了,而是在链式调用以后,建议用户检查。可是回到 Collection 库的愿景,这样的实现会让使用者更为温馨。

compare

数组固然有个compare函数,这个函数我设计做为匿名函数放在 AbsCollection 中,具体的实如今每一个实现类的 New 函数中进行设置。我也将这个 compare 函数的设置权限做为 SetCompare() 函数放给外部设置。主要考虑到扩展性,若是后续你的 Collection 是包的本身定义的一个复杂的 Object方法,那么你彻底能够按照某个字段进行排序。

ObjCollection

对象数组是我最耗费精力的一个实现类。它大量使用了反射。可是这个是能够扩展的。因为接口中的方法的输入输出彻底是 ICollection 接口。好比在初期,你使用 Collection 自带的 NewObjCollection 实例化了一个 ICollection, 或许你对使用了反射的 Insert,Pluck 方法的效率不是很是满意,那么,你只须要本身实现一个 ACollection, 而且本身实现上文说的6个方法,继承AbsCollection,那么,你就能够很方便的使用 Colleciton的其余方法,且没有反射。

New复制slice指针仍是数组?

这个是我很后面加的,在 New 一个Collection的时候,Collection 中的数组元素,是选择将参数中的数组指针复制到 Colleciton 中,仍是将参数中的整个数组复制到 Collection 中呢?后来我选择了后者。主要是考虑到安全性,NewCollection 的时候我复制一份,后续若是有对这个数组进行修改的操做,不会影响原先传入的参数Slice。为了一些安全性,牺牲一些内存,我认为仍是值得的。

图片

心路历程及后续

这个 Collection 包我也先后利用业余时间开发了挺久了。主要是实现的思想不断在变化,从最初的我将 error 以直接panic的方式保持链式调用,到但愿实现一个 IMap 数据结构,到使用的是数组,仍是指针等,包括名字我也从最初的IArray 改为ICollection(我但愿从使用这个包开始,Collection就成为了这个包的关键字,全部接口和函数一旦设计到数组的概念的时候就使用Collection这个关键字)。

图片

写一个通用库其实并非那么容易的事情,最重要的是思想还有设计感。

这个库我目前就在我本身的项目组进行推广和使用。文中的PPT就是我在项目组推广时使用的PPT。目前已经打了1.0.1的tag。后续会持续优化,而且作一些文档补充。但愿能成为最适合业务开发的 Collection 包。

再次推广下这个项目 https://github.com/jianfengye/collection ,欢迎使用和提PR。熟练使用以后,它必定会让你的业务开发效率提高一个档次的。