序言
众所周知,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 exist\n", elem)
return ERR_ELEM_EXIST
}
}
*this = append(*this, elem)
fmt.Printf("Slice:Add elem: %v succ\n", 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 exist\n", elem)
return ERR_ELEM_NT_EXIST
}
fmt.Printf("Slice:Remove elem: %v succ\n", 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 exist\n", elem)
return ERR_ELEM_EXIST
}
}
*this = append(*this, elem)
fmt.Printf("Slice:Add elem: %v succ\n", 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 exist\n", elem)
return ERR_ELEM_NT_EXIST
}
fmt.Printf("Slice:Remove elem: %v succ\n", 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)不同的是两个元素是否相等的判断方式不一样:
- 基本类型(int或string)直接通过”==“运算符来判断;
- 用户自定义类型万千种种,数组切片算法中不可能知道,所以需要通过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 exist\n", elem)
return ERR_ELEM_EXIST
}
}
*this = append(*this, elem)
fmt.Printf("Slice:Add elem: %v succ\n", 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 exist\n", elem)
return ERR_ELEM_NT_EXIST
}
fmt.Printf("Slice:Remove elem: %v succ\n", 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的强大魅力,希望对读者也有一定的启发。