抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

为了学习Go语言, 我把之前工作中一个用的比较多的脚本用Go重写了. 所谓没有对比就没有伤害, 实际写下来的感受是, Python真的简单快捷易懂…好用的第三方模块多…中英文的学习资料更是非常容易找. 要不是为了性能…我真是不想换…

一只Python菜鸡眼里的Go

由于我之前比较大一点的程序都是Python和R写的, 均不属于强类型语言, 对变量的声明和使用的都比较灵活, 所以开发的主要难度集中在第三方模块的使用和处理自己设计中的逻辑bug上.

Go就不一样了…它是一种强类型的语言, 变量要先声明再使用, 且一旦声明, 后面不可再更改类型, 这在我前期开始上手的时候着实让我难受了好久…

虽然Go为了一定程度上增加易用性设计了接口这个灵活的类型, 但我毕竟是菜鸡出身, 在Python R Bash内都没有类似概念的情况下…明白这到底是个什么东西着实花了我不少时间.

在解决了基本的变量使用后, 还有错误处理的方式不同, 第三方模块较少以及缺少Magic写法等种种问题要一点一点解决… 最终导致这个改写拖了整整两个星期才初步完成…

虽然困难不少, 但是只要体验过它的速度, 相信很多人会和我一样激动异常(虽然本次我重写的时候顺带着对算法也做了优化, 但是那点优化是绝对不足以带来200倍的差距)

嘛, 世界变化这么快, 多学一样东西总是不会错的~

附上这个重写项目的地址: FUEX

以Python为参照快速上手Go

就像我学英语的时候总是扰不开从中文的角度进行思考, 我写这个练习项目的时候总是不可避免的想要按照Python里面的方式是来进行代码实现. 但是Python和Go毕竟是两种差异很大的语言, 一些Python里用到的东西在Go里只有类似物, 甚至是连类似物都没有, 这里就总结一下本次开发中涉及到的一些替代实现方案.

变量/数据类型

变量声明及初始化

在Python中没有单独的变量声明及初始化的步骤, 想使用的话直接var=value就可以使用, 并且变量的类型可以在后续处理中进行变更. 比如:

1
2
3
var1 = True
var2 = 1
var1 = var2 if var2 else True

在上面的代码中, var1和var2在赋值的过程中会被自动赋予变量类型(boolean, int), 且两者虽然类型不同, 仍然可以把var2的值传递给var1, 在传递完成后var1会从boolean变成int.

而Go中, 一个变量首先要被声明才能使用, 且一个变量声明后类型固定, 是无法把var2内的数值赋给var1

1
2
3
4
Var var1 bool // 变量声明
Var var2 int // 变量声明
var1 = True
var2 = 1

当然, 所有变量都要这么些的话确实有点麻烦, Go针对这个给出了简单的写法:var1 := 1, 这种写法在创建变量的同时给变量赋值, 同时变量的类型会自动指定. 不过由于是自动指定的, 如果对类型有什么特殊的需求, 可能还是自己声明比较好.

Go是强类型的语言

所谓强类型, 就是对变量的类型有细致的分类, 并且在程序执行过程中对传递和使用的变量类型作严格的限制(个人理解). 比如在Python中, 变量的类型也就数字, 字符串, 逻辑和空(None), 复杂一点的也就只有数字, 因为下面还分为了浮点数和整数. 甚至, Python中连常量都砍掉了, 只有变量, 只是在语言编写建议中推荐使用全大写字母来提示常量(实际上值是可以被更改的).

Go就不同了…首先是分了常量(Const)和变量(Var)的, 而在变量中, 虽然也是有整数, 浮点数, 逻辑这几类, 但是整数和浮点数下面又细分了不同长度, 比如什么int16, int32, int64, 甚至还有我目前不是很明白的uint…这些变量类型在使用过程中如上一点中所说一样, 是不会自动转换的, 也就是如果某个函数给了你int16, 下一个函数要的是int64, 还得自己手动转换一次…这些类型的转换对我一个习惯了Python的人来说真的非常难受.

另外需要注意的一点是, Go的字符(byte)和字符串(string)也是两个不同的类型, 字符固定使用单引号(‘), 字符串则固定使用双引号(“).

复杂数据类型—-数组/切片/集合/结构体

就像Python有Tuple, List, Dict, Set等存储多个元素/映射的数据类型, Go中也有数据类型满足数据存储和处理需要:

数组(Array): 数组类似于List或Turple, 单又与它们都不同. 首先Go中数组的长度是固定的, 一单声明即不可更改. 同时, 虽然数组内存储内容是可变的, 但存储物的类型必须是固定的.

切片(Slice)基本跟数组差不多, 但是其长度是可变的.

集合(Map)相当于Dict, 同样其Key和Value的类型是固定不可变更的. Map的Key可使用的类型比较丰富, 只要能进行逻辑比较==操作的都可以. 这一点倒是跟Dict的Key只要能Hash化就行非常类似

最后也是我的项目中最需要的就是结构体了. 它比较类似R中的list或者Python中的嵌套Dict, 其中能存储各式各样的信息.

强类型下的灵活处理—-接口(interface)

接口是我在接触Go时接触到的一个全新概念, 它与我之前听过的API其实是很不一样的东西, 我查了好半天才对它有了一丢丢的了解…这里也记录一下.

首先根据上一节的内容, 我们知道Go是强类型的. 这个强类型贯穿了整个语言体系, 会在某些时候导致一些不必要的麻烦. 比如我们在写函数的时候, 要定义函数的接收和返回变量, 输入和返回变量是要给定类型的. 而一旦类型确定了, 函数就智能接收/这个类型的变量, 不可以更改. 这就很蛋疼了, 我们写程序的时候, 多少都会有一些函数是要接收一些情况不明的对象, 然后根据对象的情况进行判断后做下一步处理的. 但是强类型的规则规定了接收的东西类型要确定, 这就逼着人写两个重复性很高的函数来接收不同类型的对象然后做类似的处理.

举一个实际的例子, vcfgo中的Info().Get()函数. 这个函数的作用是将vcf文件中INFO列的对应信息取出然后返回. 每一个INFO项目下可能会有多个值, 如果只有一个值, 则返回对应的字符串, 如果有多个, 则将他们放入一个以字符串为元素的列表返回. 这个看上去很简单, 但是在强类型的规则下是不能直接实现的. 因为在定义函数时, 就必须定义好返回变量的类型. 一个变量的类型如果定义成了字符串, 那它就不能存储字符串列表, 反之毅然. 那为了实现目标功能, 难道要让函数多一个返回值吗? 这种时候就是interface发挥作用的时候了. interface的定义是, 只要一个对象实现了interface中定义的方法, 那它就实现了这个接口, 可以被以这个类型的接口的方式传递. 回到刚才那个问题, 只要定义一个接口, 然后让要返回的东西都实现这个接口, 就可以将他们以接口的方式返回了.

最后需要注意的是, 由于返回的是接口, 所以他们的类型都是interface, 如果你需要进一步传递他们, 可能还需要用断言的方式将他们从接口中取出成为原来的类型. 比如我的项目里面就有这么一句:

1
2
3
if annObj, ok := anns.(string); ok { // 单个注释, 直接解析
annTarget = annObj
}

这句话中的ann就是Info().Get()函数返回给我的接口, 这个接口里可能是字符串, 也可能是一个字符串列表, 而我就使用anns.(string)尝试断言它里面是字符串, 如果断言正确, 则直接将取出的字符串赋值, 如果错误, 则进行另一种处理

文件操作

Python中的文件读写非常简单, 一条语句打开文件, 获得的对象是可迭代的, 直接for循环就可以对文件按行处理了. 写文件同理, 也是几条语句就能搞定. 同时Python还有with语句可以在操作完成后自动关闭文件句柄, 可谓非常方便了.

1
2
3
with open("file", "r") as f:
for line in f:
print(line)

相比之下Go在读写就麻烦了很多, 比如我想按行读取文件内容并打印的话:

1
2
3
4
5
6
7
8
9
10
11
12
13
file, err := os.Open(vcfFile)
defer file.Close() // 自动关闭文件文件, 相当于用python里用with
if err != nil {
panic(err)
}
reader := bufio.NewReader(file)
for {
str, err := reader.ReadString('\n')
if err == io.EOF {
break
}
fmt.Println(str)
}

上面步骤主要是先打开一个文件, 然后使用另外一个函数指定以'\n'作为分隔读取一行的内容, 然后将读到的内容打印到屏幕. 其实说来核心的东西可能跟Python差不多, 但是有些东西变成了自己处理, 代码自然就多了. 比如打开文件后的异常处理, Python如果读不到文件, 会自动抛出异常, 告诉你文件不存在, 所以退出. 而写Go则要自己手动panic, 把err中的错误显示出来进行提示并终止程序. Python中使用for便利打开的文件对象即可按行读取文件, 并且会在达到文件末时自动终止. 而Go中这个过程是自己手动写出来的, 甚至文件按'\n'分行都是自己指定的.

写文件的情况与读文件类似, 然后Go的写文件比读还要复杂一点, 这里先挖坑, 后面填.

嵌套数据结构

Python中常用的数据结构之间可以进行相互嵌套, 形成复杂的数据结构以满足实际的数据处理需要. 这种嵌套的数据结构非常自由, 因为dict/list/tuple/set对其中存放的东西没有特别的限制, 所以我们可以轻松的把一系列不同的东西组成一个整理. 这是我在某些数据处理时喜欢用Python而不是R的原因之一(R的一些数据结构内部存同类的东西). 以我这个项目里存储的refGene文件信息为例. 我的程序需要从数据库文件中读取出一个基因的转录本号码, 外显子起止位点, 转录方向, 基因名称等等一系列信息. 其中转录本号码是一个字符串, 外显子起止点里存储的是一个列表的整数, 转录方向是单个字符, 基因名称则又是字符串. 这一系列信息我都可以以Key: Value的形式存储在一个大字典中, 需要的时候根据Key来获取对应的值就可以了.

这在Go中是不可能的, 因为list对应的数组, dict对应的集合, 在Go中都是只能存储声明好的东西的:

1
2
outStrList := []string{"out1", "out2"} // 只能存string
gene2tran := make(map[string][]string) // Key和Value都必须是string

因此要做到类似Python里面那种不同类型数据的嵌套, 就智能使用结构体struct了:

1
2
3
4
5
6
7
8
9
10
11
// 我代码里面定义的存储转录本信息的结构体
type geneRecord struct {
exon [][]int64
intron [][]int64
transRegion [2]int64
cdsRegion [2]int64
chr string
strand string
geneSymbol string
}
tran2info := make(map[string]geneRecord) // map 套结构体

控制结构

Go中的控制结构甚至比Python还简单…判断用的if/else, 循环用的for, 多分支结构的switch——就只有这些了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// if else 和 for 举例
if cols[3] == "+" {
for idx, _ := range exonStarts {
exons = append(exons, []int64{exonStarts[idx], exonEnds[idx], exonFrames[idx]})
if idx != len(exonStarts)-1 {
introns = append(introns, []int64{exonEnds[idx], exonStarts[idx+1]})
}
}
} else {
for idx, _ := range exonStarts {
exons = reverSlice(append(exons, []int64{exonStarts[idx], exonEnds[idx], exonFrames[idx]}))
if idx != len(exonStarts)-1 {
introns = reverSlice(append(introns, []int64{exonEnds[idx], exonStarts[idx+1]}))
}
}
}

// switch举例
switch {
case AinA && BinB: // 目标fusion, 对应正确
locates["breakA"] = locAinA
locates["breakB"] = locBinB
return locates, "right", nil
case AinB && BinA: // 目标fusion, 对应颠倒
locates["breakA"] = locAinB
locates["breakB"] = locBinA
return locates, "wrong", nil
default:
err := errors.New("Not Fusion")
}

虽然东西是比较少, 但也够用了…

另外Go除了breakcontinue之外, 还提供了goto这种跳转性的控制语句. 不过我暂时没有使用过…

构建函数

Go中创建函数没有啥特别的…就是注意输入参数和返回值的类型要先声明好(下面例子中, reverSlice后面跟的是传入的参数, 括号外面的是返回值的声明), 声明好就不能变了. 如果需要灵活的输入和返回, 需要靠interface

1
2
3
4
5
6
7
func reverSlice(a [][]int64) [][]int64 {
for i := len(a)/2 - 1; i >= 0; i-- {
opp := len(a) - 1 - i
a[i], a[opp] = a[opp], a[i]
}
return a
}

错误处理

本质上来说, Go里面没有Python中那样专门的错误处理, 或者说, 并没有专门的异常抛出这一操作. 但是在使用Go的时候会发现, 很多函数的返回值会有两个, 比如打开文件的函数:

1
file, err := os.Open(yourFile)

上面的文件打开函数除了返回打开的文件对象, 还会返回另外一个变量, 这个变量我个人理解为类似Shell执行命令后状态码一样的东西, 一般情况下, 函数内程序执行正常的话, 这个变量会是nil. 而一旦函数内的部分执行出错, 返回的就会是一个error类的对象, 对象里包含了错误的说明字符. 对这个变量进行处理, 也就等同于在Python里面用Try: Except:

字符处理

Go的字符串处理基本依赖strings包, 包内基本的连接, 拆分, 替换, 前后去空白都有…实际使用感受类似于R中的stringr…就是东西都有…但是总觉得没那么好用.

另外Python中的格式化字符串在Go内暂时没有对等物. 所以无法像Python内弄一个硕大的文本模板然后填充内容.

JSON处理

Go也有专门的json包来读取json文件内容形成Go的数据结构, 和将Go对象转化为json字串. 不过可能还是类型限制大, 实际用起来比Python还是复杂一点…

1
2
3
4
5
6
7
8
tarTranFile := "RXA_gene_trans.json"
data, err := ioutil.ReadFile(tarTranFile)
if err != nil {
return
}
var targetTransFile interface{}
err = json.Unmarshal(data, &targetTransFile)
targetTrans := targetTransFile.(map[string]interface{})

可以看到我为了读取写好的json文件, 执行了打开文件, 载入内容, 将内容用特定函数处理, 然后因为返回的是interface, 所以最后又进行了类型断言.

类似的操作…在python中1~2行就可以完成了…

程序编译

与Python这种边翻译边执行的语言不同, Go语言是要先编译后使用的(虽然也有go run), 所以还要研究一下编译可执行文件的问题.
(挖坑占位)

Python与Go的独有物

这一部分记录Python和Go各自独有的一些东西(挖坑占位).

Python部分

  • 列表/字典生成式: 在Python中生成式是非常方便的可迭代对象生成/处理方式, 可以有效的简化代码(效率有没提升我不太清楚), 然而Go里就智能乖乖写循环了.
  • 字符串格式化: Python内的字符串格式化的好处就在于将常常的一段文本挖空后依次填充, 这种写法代码的可读性非常好. 可以一目了然的知道最后出现的字符串是个怎样的框架. 而Go中就只能自己拼接了.

Go部分

  • 接口(见上)
  • switch控制结构
  • 方便的并行化: Python虽然多线程, 多进程, 协程都有…但确实是没那么高效或者好用…这也是促使我学Go的一个原因了.

评论

留下友善的评论吧~