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

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

这篇文章推荐一个库,https://github.com/jianfengye/collection。 这个库是我在开发业务过程中 Slice 的频繁导致业务开发效率低,就产生了要做一个 Collection 包的想法。本文说说我开发这个库的前因后果

Golang 适不适合写业务?

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

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

我想,符合业务语义的语言才有未来!!

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

如图,如果说高级语言是拉近了机器语言和业务语义的距离,那么开发Collection包的愿景也是希望拉近 Golang 这门高级语言和 业务语言的距离。

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

展示

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

Collection包目前支持的元素类型:int, int64, float32, float64, string, struct。除了struct数组使用了反射之外,其他的数组并没有使用反射机制,效率和易用性得到一定的平衡。

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

  1. NewIntCollection(objs []int) *IntCollection
  2. NewInt64Collection(objs []int64) *Int64Collection
  3. NewFloat64Collection(objs []float64) *Float64Collection
  4. NewFloat32Collection(objs []float32) *Float32Collection
  5. NewStrCollection(objs []string) *StrCollection
  6. NewObjCollection(objs interface{}) *ObjCollection

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

下面做一些Collection中函数的展示。

友好的格式展示

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

  1. a1 := Foo{A: "a1"}
  2. a2 := Foo{A: "a2"}
  3. objColl := NewObjCollection([]Foo{a1, a2})
  4. objColl.DD()
  5. /*
  6. ObjCollection(2)(collection.Foo):{
  7. 0: {A:a1}
  8. 1: {A:a2}
  9. }
  10. */
  11. intColl := NewIntCollection([]int{1,2})
  12. intColl.DD()
  13. /*
  14. IntCollection(2):{
  15. 0: 1
  16. 1: 2
  17. }
  18. */

查找功能

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

  1. Search(item interface{}) int

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

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

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

排重功能

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

  1. intColl := NewIntCollection([]int{1,2, 3, 3, 2})
  2. uniqColl := intColl.Unique()
  3. if uniqColl.Count() != 3 {
  4. t.Error("Unique 重复错误")
  5. }
  6. uniqColl.DD()
  7. /*
  8. IntCollection(3):{
  9. 0: 1
  10. 1: 2
  11. 2: 3
  12. }
  13. */

获取最后一个

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

  1. intColl := NewIntCollection([]int{1, 2, 3, 4, 3, 2})
  2. last, err := intColl.Last().ToInt()
  3. if err != nil {
  4. t.Error("last get error")
  5. }
  6. if last != 2 {
  7. t.Error("last 获取错误")
  8. }
  9. last, err = intColl.Last(func(item interface{}, key int) bool {
  10. i := item.(int)
  11. return i > 2
  12. }).ToInt()
  13. if err != nil {
  14. t.Error("last get error")
  15. }
  16. if last != 3 {
  17. t.Error("last 获取错误")
  18. }

Map & Reduce

Map

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

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

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

如果希望在某此调用的时候中止,就在此次调用的时候设置Collection的Error,就可以中止,且此次回调函数生成的结构不合并到最终生成的ICollection。

  1. intColl := NewIntCollection([]int{1, 2, 3, 4})
  2. newIntColl := intColl.Map(func(item interface{}, key int) interface{} {
  3. v := item.(int)
  4. return v * 2
  5. })
  6. newIntColl.DD()
  7. if newIntColl.Count() != 4 {
  8. t.Error("Map错误")
  9. }
  10. newIntColl2 := intColl.Map(func(item interface{}, key int) interface{} {
  11. v := item.(int)
  12. if key > 2 {
  13. intColl.SetErr(errors.New("break"))
  14. return nil
  15. }
  16. return v * 2
  17. })
  18. newIntColl2.DD()
  19. /*
  20. IntCollection(4):{
  21. 0: 2
  22. 1: 4
  23. 2: 6
  24. 3: 8
  25. }
  26. IntCollection(3):{
  27. 0: 2
  28. 1: 4
  29. 2: 6
  30. }
  31. */

Reduce

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

对Collection中的所有元素进行聚合计算。

如果希望在某次调用的时候中止,在此次调用的时候设置Collection的Error,就可以中止调用。

  1. intColl := NewIntCollection([]int{1, 2, 3, 4})
  2. sumMix := intColl.Reduce(func(carry IMix, item IMix) IMix {
  3. carryInt, _ := carry.ToInt()
  4. itemInt, _ := item.ToInt()
  5. return NewMix(carryInt + itemInt)
  6. })
  7. sumMix.DD()
  8. sum, err := sumMix.ToInt()
  9. if err != nil {
  10. t.Error(err.Error())
  11. }
  12. if sum != 10 {
  13. t.Error("Reduce计算错误")
  14. }
  15. /*
  16. IMix(int): 10
  17. */

排列

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

  1. intColl := NewIntCollection([]int{2, 4, 3})
  2. intColl2 := intColl.Sort()
  3. if intColl2.Err() != nil {
  4. t.Error(intColl2.Err())
  5. }
  6. intColl2.DD()
  7. /*
  8. IntCollection(3):{
  9. 0: 2
  10. 1: 3
  11. 2: 4
  12. }
  13. */

合并

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

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

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

核心

继承

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结束的片段。

  1. intColl := NewIntCollection([]int{1, 2, 3, 4, 5})
  2. retColl := intColl.Slice(2)
  3. if retColl.Count() != 3 {
  4. t.Error("Slice 错误")
  5. }
  6. retColl.DD()
  7. retColl = intColl.Slice(2,2)
  8. if retColl.Count() != 2 {
  9. t.Error("Slice 两个参数错误")
  10. }
  11. retColl.DD()
  12. retColl = intColl.Slice(2, -1)
  13. if retColl.Count() != 3 {
  14. t.Error("Slice第二个参数为-1错误")
  15. }
  16. retColl.DD()
  17. /*
  18. IntCollection(3):{
  19. 0: 3
  20. 1: 4
  21. 2: 5
  22. }
  23. IntCollection(2):{
  24. 0: 3
  25. 1: 4
  26. }
  27. IntCollection(3):{
  28. 0: 3
  29. 1: 4
  30. 2: 5
  31. }
  32. */

是否使用可选方法我纠结了很久,因为这种可选参数毕竟还是不够美观的。不过后来还是想到了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。熟练使用之后,它一定会让你的业务开发效率提升一个档次的。

一个让业务开发效率提高10倍的golang库的更多相关文章

  1. 使用这些idea插件让开发效率提高5倍

    idea 有很多非常好用的插件,用好了这些插件能够极大的提高开发效率 插件用的好,bug 就追不上了我

  2. (原创)发布一个c++11开发的轻量级的并行Task库TaskCpp

    TaskCpp简介 TaskCpp是c++11开发的一个跨平台的并行task库,它的设计思路来源于微软的并行计算库ppl和intel的并行计算库tbb,关于ppl和tbb我在前面有介绍.既然已经有了这 ...

  3. Parse:App开发必备 让应用开发效率提高上百倍

    Parse一个应用开发工具, 是由Y Combinator所孵化的创业公司.使用Parse能把效率提高10倍到100倍.通常情况下,从开发用户到推广用户需要花几周时间,用了Parse则只需几小时.[U ...

  4. 封装一个简单好用的打印Log的工具类And快速开发系列 10个常用工具类

    快速开发系列 10个常用工具类 http://blog.csdn.net/lmj623565791/article/details/38965311 ------------------------- ...

  5. Web 应用性能提升 10 倍的 10 个建议

    转载自http://blog.jobbole.com/94962/ 提升 Web 应用的性能变得越来越重要.线上经济活动的份额持续增长,当前发达世界中 5 % 的经济发生在互联网上(查看下面资源的统计 ...

  6. 程序员需要经纪人吗?10x 最好的程序员其生产力相当于同行的 10 倍~

    原文地址 10x 起源于技术界一个流行的说法,即最好的程序员是超级明星,其生产力相当于同行的 10 倍: Google 园区以好玩的设施闻名:小憩舱.球坑.按摩.干洗.随便吃到饱的自助餐.(为了拍人才 ...

  7. 存算分离下写性能提升10倍以上,EMR Spark引擎是如何做到的?

    ​引言 随着大数据技术架构的演进,存储与计算分离的架构能更好的满足用户对降低数据存储成本,按需调度计算资源的诉求,正在成为越来越多人的选择.相较 HDFS,数据存储在对象存储上可以节约存储成本,但与此 ...

  8. SmartIDE v0.1.18 已经发布 - 助力阿里国产IDE OpenSumi 插件安装提速10倍、Dapr和Jupyter支持、CLI k8s支持

    SmartIDE v0.1.18 (cli build 3538) 已经发布,在过去的Sprint 18中,我们集中精力推进对 k8s 远程工作区 的支持,同时继续扩展SmartIDE对不同技术栈的支 ...

  9. 【今日推荐】移动 Web 开发的10个最佳 JavaScript 框架

    选择正确的 JavaScript 框架,对于开发移动 Web 应用程序是至关重要的,也是移动应用程序开发的一项重要任务.开发人员可以使用框架实现的功能高效地达到他们的开发目标.这些预实现的组件采用优秀 ...

随机推荐

  1. hdu5389 Zero Escape

    Problem Description Zero Escape, is a visual novel adventure video game directed by Kotaro Uchikoshi ...

  2. Android TextView,EditText要求固定行数自动调整TextSize

    最近项目有个需求要求文本最多显示3行,继续输入则字体变小,删除已经输入的文字,那么字体变大,不管变大变小都不能超过3行.网上怎么找也找不到相关的解决方案,自己动手,丰衣足食了! 说一下算法思路,后面给 ...

  3. mysql创建应用账号

    -- 赋予某个库全部权限use mysql;grant all privileges on test_db.* to test_user@'%' identified by 'Aa123456';gr ...

  4. 手机端自适应布局demo

    原型如下: 要求如下:适应各种机型 源码如下: <!DOCTYPE html > <html>     <head>         <meta http-e ...

  5. _Decoder_Interface_init xxxxxx in amrFileCodec.o

    Undefined symbols for architecture arm64: "_Decoder_Interface_init", referenced from: Deco ...

  6. WPF入门(四)->线形区域Path内容填充之填充图(ImageBrush)

    原文:WPF入门(四)->线形区域Path内容填充之填充图(ImageBrush) 前面我们提到了LinearGradientBrush可以用来画渐变填充图,那么我们同时也可以使用ImageBr ...

  7. WPF入门(三)->几何图形之椭圆形(EllipseGeometry)

    原文:WPF入门(三)->几何图形之椭圆形(EllipseGeometry) 我们可以使用EllipseGeometry 来绘制一个椭圆或者圆形的图形 EllipseGeometry类: 表示圆 ...

  8. QQ登录, 腾讯开放平台和QQ互联的坑

    原文:QQ登录, 腾讯开放平台和QQ互联的坑 版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/u012881042/article/details/7 ...

  9. 在 Oracle 中新建 SDE 用户

    --1.创建用户(SDE)和密码(SDE) CREATE USER SDE IDENTIFIED BY SDE --2.创建表空间(SDE) CREATE TABLESPACE SDE DATAFIL ...

  10. JavaScript window.location物

    演示样例 注意 方法 常常使用window.location.它的结构总是记不住.简单梳理下.方便以后查询. 演示样例 URL:http://b.a.com:88/index.php? name=ka ...