1. 程式人生 > >一個讓業務開發效率提高10倍的golang庫

一個讓業務開發效率提高10倍的golang庫

一個讓業務開發效率提高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:

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。熟練使用之後,它一定會讓你的業務開發效率提升一個檔次的