Golang泛型编程初体验

待兔
• 阅读 1337

序言

众所周知,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)不同的是两个元素是否相等的判断方式不一样:

  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 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的强大魅力,希望对读者也有一定的启发。

点赞
收藏
评论区
推荐文章
Jacquelyn38 Jacquelyn38
1年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
blmius blmius
1年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Stella981 Stella981
1年前
MacOS VSCode 安装 GO 插件失败问题解决
0x00问题重现Installinggolang.org/x/tools/cmd/guruFAILEDInstallinggolang.org/x/tools/cmd/gorenameFAILEDInstallinggolang.org/x/lint/golintFAILEDInst
Wesley13 Wesley13
1年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Easter79 Easter79
1年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
1年前
MySQL查询按照指定规则排序
1.按照指定(单个)字段排序selectfromtable_nameorderiddesc;2.按照指定(多个)字段排序selectfromtable_nameorderiddesc,statusdesc;3.按照指定字段和规则排序selec
Stella981 Stella981
1年前
Angular material mat
IconIconNamematiconcode_add\_comment_addcommenticon<maticonadd\_comment</maticon_attach\_file_attachfileicon<maticonattach\_file</maticon_attach\
Wesley13 Wesley13
1年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
京东云开发者 京东云开发者
2个月前
为什么mysql不推荐使用雪花ID作为主键
作者:毛辰飞背景在mysql中设计表的时候,mysql官方推荐不要使用uuid或者不连续不重复的雪花id(long形且唯一),而是推荐连续自增的主键id,官方的推荐是auto_increment,那么为什么不建议采用uuid,使用uuid究
helloworld_34035044 helloworld_34035044
6个月前
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为