Golang泛型程式設計初體驗
序言
眾所周知,Golang中不支援類似C++/Java中的標記式泛型,所以對於常用演算法,比如氣泡排序演算法,有些同學容易寫出邏輯上重複的程式碼,即整型是第一套程式碼,字串型是第二套程式碼,使用者自定義型別是第三套程式碼。
重複是萬惡之源,我們當然不能容忍,所以要消除重複,使得程式碼保持在最佳的狀態。本文通過一個實際使用的簡單演算法的演進過程,初次體驗了Golang的泛型程式設計,消除了重複程式碼,非常自然。
需求一:切片演算法支援整型
今天是星期二,天氣晴朗,萬里無雲,空氣清新,我在辦公室裡聽著音樂寫著程式碼,開始了今天的工作。
“小哥,陣列切片有沒有add和remove函式,可以方便的將元素新增和刪除?”
我抬頭一看,是小明,就回答道:“什麼型別的陣列切片?”
小明說:“整型。”
“陣列切片中的元素能不能有相同的?”我追問道。
“不能有相同的,我存的都是實體的Id。”小明肯定的回答。
“哦,這個簡單,我過會提供一個Slice類,有Add和Remove方法,支援整型。”我有點自信的回答。
小明說完謝謝後,回到了辦公位繼續工作。
一個小時後,我寫完了支援整型的切片演算法:
type Slice []int func NewSlice() Slice { return make(Slice, 0) } func (this* Slice) Add(elem int) error { for _, v := range *this { if v == elem { fmt.Printf("Slice:Add elem: %v already existn", elem) return ERR_ELEM_EXIST } } *this = append(*this, elem) fmt.Printf("Slice:Add elem: %v succn", elem) return nil } func (this* Slice) Remove(elem int) error { found := false for i, v := range *this { if v == elem { if i == len(*this) - 1 { *this = (*this)[:i] } else { *this = append((*this)[:i], (*this)[i+1:]...) } found = true break } } if !found { fmt.Printf("Slice:Remove elem: %v not existn", elem) return ERR_ELEM_NT_EXIST } fmt.Printf("Slice:Remove elem: %v succn", elem) return nil }
小明看了我的實現後,說:”我試用一下?"
“醜媳婦不怕見公婆。”我請他試用。
小明用了5分鐘,寫了下面的程式碼:
func main() { intSliceExec() } func intSliceExec() { fmt.Println("int slice start") slice := alg.NewSlice() slice.Add(1) fmt.Println("current int slice:", slice) slice.Add(2) fmt.Println("current int slice:", slice) slice.Add(2) fmt.Println("current int slice:", slice) slice.Add(3) fmt.Println("current int slice:", slice) slice.Remove(2) fmt.Println("current int slice:", slice) slice.Remove(2) fmt.Println("current int slice:", slice) slice.Remove(3) fmt.Println("current int slice:", slice) fmt.Println("int slice end") }
從試用程式碼中可以看出,整型陣列切片中最多有三個元素[1 2 3]
,元素2插入的第二次應該失敗,同理元素2刪除的第二次也應該失敗,整型陣列切片最後只剩下一個元素[1]
。
go run執行程式碼後,日誌如下:
int slice start
Slice:Add elem: 1 succ
current int slice: [1]
Slice:Add elem: 2 succ
current int slice: [1 2]
Slice:Add elem: 2 already exist
current int slice: [1 2]
Slice:Add elem: 3 succ
current int slice: [1 2 3]
Slice:Remove elem: 2 succ
current int slice: [1 3]
Slice:Remove elem: 2 not exist
current int slice: [1 3]
Slice:Remove elem: 3 succ
current int slice: [1]
int slice end
檢視日誌,結果符合期望。
需求二:切片演算法支援字串
週三下午,睡完午覺後精神有點小抖擻,瀏覽者郵件,突然發現公司又接了一個大單,於是吃了會精神食量。
”小哥,小哥!“
我抬頭一看,是小雷。
”咋的啦,哥們?“我好奇的問道。
”聽說你昨天實現了一個數組切片演算法,已支援整型,我現在想用字串型的陣列切片演算法,你能提供不?"小雷有點著急的問道。
我心裡一想,Golang支援Any型別,即interface{},同時字串和整型一樣都可以直接用”==“運算子比較兩個元素是否相等,所以你懂的。
”這個好實現,給我一首歌的時間就可以試用。“我說完後,就立刻修改起了程式碼。
兩分鐘後,我提供了新版本的程式碼:
type Slice []interface{}
func NewSlice() Slice {
return make(Slice, 0)
}
func (this* Slice) Add(elem interface{}) error {
for _, v := range *this {
if v == elem {
fmt.Printf("Slice:Add elem: %v already existn", elem)
return ERR_ELEM_EXIST
}
}
*this = append(*this, elem)
fmt.Printf("Slice:Add elem: %v succn", elem)
return nil
}
func (this* Slice) Remove(elem interface{}) error {
found := false
for i, v := range *this {
if v == elem {
if i == len(*this) - 1 {
*this = (*this)[:i]
} else {
*this = append((*this)[:i], (*this)[i+1:]...)
}
found = true
break
}
}
if !found {
fmt.Printf("Slice:Remove elem: %v not existn", elem)
return ERR_ELEM_NT_EXIST
}
fmt.Printf("Slice:Remove elem: %v succn", elem)
return nil
}
不難發現,改動很簡單,只將三個地方的int改成了interface{},一切都是這麼自然。
”哇塞,這麼快?半首歌我還沒聽完。“小雷開森的說。
”簡單設計,呵呵!“我們不約而同的說出了這個大家最愛說又最難做到的XP實踐。
這次有了拷貝這個強大的武器,小雷兩分鐘就寫完了試用程式碼:
func main() {
intSliceExec()
fmt.Println("")
stringSliceExec()
}
func stringSliceExec() {
fmt.Println("string slice start")
slice := alg.NewSlice()
slice.Add("hello")
fmt.Println("current string slice:", slice)
slice.Add("golang")
fmt.Println("current string slice:", slice)
slice.Add("golang")
fmt.Println("current string slice:", slice)
slice.Add("generic")
fmt.Println("current string slice:", slice)
slice.Remove("golang")
fmt.Println("current string slice:", slice)
slice.Remove("golang")
fmt.Println("current string slice:", slice)
slice.Remove("generic")
fmt.Println("current string slice:", slice)
fmt.Println("string slice end")
}
...
從試用程式碼中可以看出,字串型陣列切片中最多有三個元素[hello golang generic]
,元素golang插入的第二次應該失敗,同理元素golang刪除的第二次也應該失敗,字串型陣列切片最後只剩下一個元素[hello]
。
int slice start
Slice:Add elem: 1 succ
current int slice: [1]
Slice:Add elem: 2 succ
current int slice: [1 2]
Slice:Add elem: 2 already exist
current int slice: [1 2]
Slice:Add elem: 3 succ
current int slice: [1 2 3]
Slice:Remove elem: 2 succ
current int slice: [1 3]
Slice:Remove elem: 2 not exist
current int slice: [1 3]
Slice:Remove elem: 3 succ
current int slice: [1]
int slice end
string slice start
Slice:Add elem: hello succ
current string slice: [hello]
Slice:Add elem: golang succ
current string slice: [hello golang]
Slice:Add elem: golang already exist
current string slice: [hello golang]
Slice:Add elem: generic succ
current string slice: [hello golang generic]
Slice:Remove elem: golang succ
current string slice: [hello generic]
Slice:Remove elem: golang not exist
current string slice: [hello generic]
Slice:Remove elem: generic succ
current string slice: [hello]
string slice end
檢視日誌,結果符合期望。
需求三:切片演算法支援使用者自定義的型別
今天週四,眼看明天就週五了,打算中午出去吃個自助餐提高一下生活質量,於是叫著小方開著車就殺出去了。由於在一點半之前要回到公司上班,所以匆匆地找了一家自助餐店。
“哇靠,人真多!”小方這樣感嘆道。
“這個店應該搞成多種模式,比如選取大家常吃的幾種套餐(A,C,D),這樣百分之七十的上班族都會直接領套餐,就不會白白浪費排隊時間了。”我不著邊際的邊想邊說。
“自助餐還是更有吸引力,顧客可以任意搭配,做到真正的私人訂製,而套餐吃幾次就膩味了。”小方反駁著對我說。
...
緊趕慢趕,終於,終於在一點半前回到了公司,於是又開始編碼了。
“小哥,聽說你實現了一個數組切片演算法,既支援整型,又支援字串型,我這還有一個小小需求。”
我抬頭一看,是小方,就問“啥子需求?“
”我這邊有自定義的struct型別,也想用陣列切片演算法。“小方大方的提出需求。
”這個嘛,這個嘛,有點難度!“我邊思考邊迴應:”給我半個小時,讓我試試。“
”好的,小哥。“小方說完後露出了愜意的笑。
我們先自定義一個型別:
type Student struct {
id string
name string
}
Student型別有兩個資料成員,即id和name。id是學號,全域性我唯一;name是中文名字的拼音,可重複。
使用者自定義型別和基本型別(int或string)不同的是兩個元素是否相等的判斷方式不一樣:
1.基本型別(int或string)直接通過”==“運算子來判斷;
2.使用者自定義型別萬千種種,陣列切片演算法中不可能知道,所以需要通過interface提供的方法進行兩個元素是否相等的判斷。
我們接著定義一個interface:
type Comparable interface {
IsEqual(obj interface{}) bool
}
只要使用者自定義的型別實現了介面Comparable,就可以呼叫它的方法IsEqual進行兩個元素是否相等的判斷了,於是我們實現了Student型別的IsEqual方法:
func (this Student) IsEqual(obj interface{}) bool {
if student, ok := obj.(Student); ok {
return this.GetId() == student.GetId()
}
panic("unexpected type")
}
func (this Student) GetId() string {
return this.id
}
使用者自定義的GetId方法是必要的,因為Id不一定就是資料成員,可能是由多個數據成員拼接而成。
我們將陣列切片演算法的易變部分”v == elem"抽出來封裝成方法:
func isEqual(a, b interface{}) bool {
return a == b
}
於是陣列切片的Add方法和Remove方法就變成:
func (this* Slice) Add(elem interface{}) error {
for _, v := range *this {
if isEqual(v, elem) {
fmt.Printf("Slice:Add elem: %v already existn", elem)
return ERR_ELEM_EXIST
}
}
*this = append(*this, elem)
fmt.Printf("Slice:Add elem: %v succn", elem)
return nil
}
func (this* Slice) Remove(elem interface{}) error {
found := false
for i, v := range *this {
if isEqual(v, elem) {
if i == len(*this) - 1 {
*this = (*this)[:i]
} else {
*this = append((*this)[:i], (*this)[i+1:]...)
}
found = true
break
}
}
if !found {
fmt.Printf("Slice:Remove elem: %v not existn", elem)
return ERR_ELEM_NT_EXIST
}
fmt.Printf("Slice:Remove elem: %v succn", elem)
return nil
}
於是陣列切片演算法對於支援使用者自定義型別的改動僅僅侷限於isEqual函數了,我們通過介面查詢來完成程式碼修改:
func isEqual(a, b interface{}) bool {
if comparable, ok := a.(Comparable); ok {
return comparable.IsEqual(b)
} else {
return a == b
}
}
半個小時後,我完成了程式碼,叫小方過來試用。
因為有拷貝這個強大的武器,小雷三分鐘就寫完了試用程式碼:
func main() {
intSliceExec()
fmt.Println("")
stringSliceExec()
fmt.Println("")
structSliceExec()
}
func structSliceExec() {
fmt.Println("struct slice start")
xiaoMing := Student{"1001", "xiao ming"}
xiaoLei := Student{"1002", "xiao lei"}
xiaoFang := Student{"1003", "xiao fang"}
slice := alg.NewSlice()
slice.Add(xiaoMing)
fmt.Println("current struct slice:", slice)
slice.Add(xiaoLei)
fmt.Println("current struct slice:", slice)
slice.Add(xiaoLei)
fmt.Println("current struct slice:", slice)
slice.Add(xiaoFang)
fmt.Println("current struct slice:", slice)
slice.Remove(xiaoLei)
fmt.Println("current struct slice:", slice)
slice.Remove(xiaoLei)
fmt.Println("current struct slice:", slice)
slice.Remove(xiaoFang)
fmt.Println("current struct slice:", slice)
fmt.Println("struct slice end")
}
...
從試用程式碼中可以看出,使用者自定義型別的陣列切片中最多有三個元素[{1001 xiao ming} {1002 xiao lei} {1003 xiao fang}]
,元素{1002 xiao lei}
插入的第二次應該失敗,同理元素{1002 xiao lei}
刪除的第二次也應該失敗,使用者自定義型別的陣列切片最後只剩下一個元素[{1001 xiao ming}]
。
int slice start
Slice:Add elem: 1 succ
current int slice: [1]
Slice:Add elem: 2 succ
current int slice: [1 2]
Slice:Add elem: 2 already exist
current int slice: [1 2]
Slice:Add elem: 3 succ
current int slice: [1 2 3]
Slice:Remove elem: 2 succ
current int slice: [1 3]
Slice:Remove elem: 2 not exist
current int slice: [1 3]
Slice:Remove elem: 3 succ
current int slice: [1]
int slice end
string slice start
Slice:Add elem: hello succ
current string slice: [hello]
Slice:Add elem: golang succ
current string slice: [hello golang]
Slice:Add elem: golang already exist
current string slice: [hello golang]
Slice:Add elem: generic succ
current string slice: [hello golang generic]
Slice:Remove elem: golang succ
current string slice: [hello generic]
Slice:Remove elem: golang not exist
current string slice: [hello generic]
Slice:Remove elem: generic succ
current string slice: [hello]
string slice end
struct slice start
Slice:Add elem: {1001 xiao ming} succ
current struct slice: [{1001 xiao ming}]
Slice:Add elem: {1002 xiao lei} succ
current struct slice: [{1001 xiao ming} {1002 xiao lei}]
Slice:Add elem: {1002 xiao lei} already exist
current struct slice: [{1001 xiao ming} {1002 xiao lei}]
Slice:Add elem: {1003 xiao fang} succ
current struct slice: [{1001 xiao ming} {1002 xiao lei} {1003 xiao fang}]
Slice:Remove elem: {1002 xiao lei} succ
current struct slice: [{1001 xiao ming} {1003 xiao fang}]
Slice:Remove elem: {1002 xiao lei} not exist
current struct slice: [{1001 xiao ming} {1003 xiao fang}]
Slice:Remove elem: {1003 xiao fang} succ
current struct slice: [{1001 xiao ming}]
struct slice end
檢視日誌,結果符合期望。
小結
本文通過一種輕鬆愉快的方式闡述了實際使用的陣列切片演算法的演進過程,同時也是筆者使用Golang進行泛型程式設計的第一次旅行,再次領略了Golang中interface的強大魅力,希望對讀者也有一定的啟發。