年度语言 golang 使用感受

3,643 阅读11分钟

首先,无意进行语言之争,毕竟,PHP是世界上最好的语言,没有之一。这个话题可以停下来了。

2016年已经过去,16年的年度语言给了go语言,而正好这一年我都是用go用得比较多,而且版本从1.2一直用到了1.8,有一些感受,来说说我对这个年度编程语言的一些粗浅理解吧。之前也写过一篇go语言的文章,但是那时候用得还不是很多,有些特性没有用上,所以理解上和今天的有些不同。

这篇文章就不分什么优势和劣势了,想到哪里说到哪里。

指针还是很重要

先看一个小坑,可能很多初次接触go的会遇到,go的range迭代用得也很多,下面这个例子不知道你之前遇到过没有,其实值是不会变的,还是1,2,3。

type a struct {
    b int
}
func main() {
    m := make([]a, 0)
    m = append(m, a{b: 0, c: 0})
    m = append(m, a{b: 1, c: 1})
    m = append(m, a{b: 2, c: 2})
    for _, e := range m {
        e.b = 9
    }
    for _, x := range m {
        fmt.Printf("%v\n", x.b)
    }
}

在range中,后面那个元素是值传递,这个很关键,所以修改不了元素的内容,而且如果元素很大的话,迭代的开销还是挺大的,所以要么你就变成for idx, _ := range m这样的形式,用下标更新,要么就变成m := make([]*a, 0)这样的指针,这样虽然传的还是值,不过是个指针的值,一是开销小,二是可以直接修改元素内容了。

所以说,指针在go中还是不可或缺的一个存在,这也是为什么像我这种之前都是做C和C++的人喜欢go的原因,因为还是可以指针满天飞,写出只能自己看懂的代码出去装逼,然后告诉别人,还是有指针性能好啊。

如果你之前对指针没概念,或者一直没怎么理解指针,那go可能要用好还是要花点时间的,go确实入门很容易,但用好也不是那么容易,之前我开始用的时候,没仔细想过这方面的东西,而且特意减少了指针的使用,害怕出现C中的野指针的情况,后来越写越觉得不是那个味道,go把指针这个功能保留下来还是让你用起来的,后来写的代码就又开始偏C风格了,指针到处飞。

虽然如此,但为了安全性的考虑,go的指针还是有一些局限性的,各个类型之间的转换是不行的,像C语言那样把各种类型的变量通过指针转来转去是很难直接做到的,但是还是给有这种需求的人给开了个口子,那就是unsafe包,看这个包的名字就知道是警告你,这是不安全的啊,挂了别来找我,我出这个包只是为了给你装逼用的。

比如我们有个需求,需要把一个结构体数组序列成一个byte数组后,还需要还原回来,一般的做法是序列化的方式,序列化成json或者用gob序列化成二进制,然后在反序列化回来,代码一般是这样的。

//do some append 
jsonbyte, err := json.Marshal(YYY)
//do some thing
structArray,err:=json.Unmarshal(jsonbyte,&XXX)

先不说序列化和反序列化都要耗费计算资源,影响速度,而且还有数据的拷贝,这对于一个性能装逼语言写的高性能服务怎么能忍,那只能祭出指针神器了,并且还得用unsafe包来加光环才行,一般情况下,序列化的过程中那次拷贝跑不掉,你总不需要需要序列化到本身吧,所以序列化的时候直接转成byte数组,当然,需要记录长度。

buffer := new(bytes.Buffer)
err = binary.Write(buffer, binary.LittleEndian, YYY)
lens=len(YYY)
resBytes:=buffer.Buffer()

这时候,YYY结构体数组就序列化成了resBytes这个byte数组了,长度是lens,反序列化的时候,直接用指针和unsafe包就行了,整个过程没有数据拷贝,也没有序列化和反序列开销,就像下面代码一样。

XXX := *(*[]structNode)(unsafe.Pointer(&reflect.SliceHeader{
        Data: uintptr(unsafe.Pointer(&resBytes[0])),
        Len:  int(lens),
        Cap:  int(lens),
    }))

当然,这种适合的是structNode结构体里面的元素是定长的,如果里面的元素还有byte数组或者string的话,甚至是int这种和操作系统体系结构相关的元素,就真的是unsafe了,呵呵。

上面两个例子,告诉我们,指针在golang中保留下来后,对性能有强需求的开发还是有好处的,并且,unsafe包开放出来的功能,至少能让你知道数据在底层到底存到什么地址上,心里面也有底了。

坑爹的map

读写安全

对于map不是协程安全这一点,还是有些想吐槽的,其他语言很多也不是线程安全的,这本来没什么可说的,自己写代码的时候注意一下吧,但是golang本身就是以协程在语言中集成,开协程特别容易的语言,而且是鼓励大家多多使用协程的思维来编程,但是作为一个基础的集成到语言本身的数据结构,竟然不是协程安全的,我去,您至少提供一个协程安全的版本让大家去选啊,虽然加读写锁比较容易实现,但是也有几个问题:

  • 有锁就必然需要考虑出现死锁的情况,而且问题还不好查。
  • 由于加入了defer保留字,很多人在使用锁的时候基本上就是把开锁和关锁写在一起了,这样有时候代码改来改去,逻辑流程变了,容易导致死锁。
  • 有人说可以把map自己封装成结构体嘛,开关锁就看不到了,但是很多时候,刚开始写的代码是不需要多协程的,这时候你用的map都是内部的map,当你发现需要锁这个map的时候,只有两种选择,一是把map封装成结构体,然后把所有的用了这个map的地方都改掉,二是在外面加一把锁,把会有冲突的地方锁起来,第一种方式改动有些大,第二种方式可能会产生bug。

要是有一个可以选的map实现方式就好了,要竞争的时候选读写安全的,不竞争的时候选简单粗暴的。

内存池的小坑

很多时候,我们会因为GC的问题,想自己做一个内存池,比较主流的做法就是用管道的方式来申请释放内存,现在也有sync.pool包了。

但是用管道的方式来做内存池,只适合数组类型的数据,不适合map,因为数组的话,你只需要把len置为0,cap不变,吐出去就行了,这样会减少内存的申请开销,但是map的话,不删除key,这个key永远在,所以想用内存池来申请map是不行的。

当然,一般情况下也没有语言能支持map的内存池,只不过因为go的管道概念,让大家都觉得什么都可以往里面丢,做内存池的时候顺便就把map给支持了,这个坑就大了。呵呵,我就是。。。。。。

map和结构体

如果一个map的value是一个结构体的话,那你不能用map[key].sturct.ele=XX给这个map中的这个结构体的元素赋值,还好是编译性的语言,会蹦一条编译cannot assign to错误出来,算个小坑吧,由于map会在使用的过程中不断的申请新内存,拷贝对象到新内存中,所以直接的寻址是不支持的。

关于泛型

没有泛型是很多人觉得go语言不够人情味的一个地方,我也是其中之一,居然没有泛型,你叫人怎么写出装逼的,简洁的代码??!!而且golang的设计者们居然说不准备支持泛型(不过目前好像改口了,说Go2.0会考虑支持泛型,呵呵),这点简直了,为什么不支持泛型,难道interface{}就够用了?不停的类型判断必然导致代码的难看和性能的损失,这点都想不清楚吗?但是。。。。。

但是如果我们仔细想想泛型的实现就稍微理解了他们了,首先,泛型的实现有两种方式,一种是C++的模板方式,一种是JAVA的类型擦除(好像叫这个名字吧)方式,我们来看看这两种方式的泛型,再来猜猜看golang为什么不支持了。

  • C++方式的泛型实现是通过模板的,简单的说就是编译的时候通过分析这个泛型函数的调用方,然后产生出对应的函数,这样做的好处和坏处都很明显。
    • 好处就是不需要运行的时候进行类型的判断从而节省了运行时的时间。
    • 坏处主要有两点,一是编译时间变长,二是如果类型很多的话,会造成最后的生成代码变得很多。
  • JAVA的泛型是通过类型擦除的方式来实现的,我本身不是写JAVA的,对这部分研究也不是很清楚,只知道他不是编译时替换类型的,而是把类型都擦除了,比如都变成obj了,在运行时需要的时候再转回来(对java这段描述不是很确定哈),个人觉得就是先把类型转成*void,这不就擦除了么,然后用的时候再转回来就行了哈。优势和劣势也很明显
    • 好处就是代码不会膨胀了。
    • 劣势就是这样的话,运行时还是需要做类型的判断,增加了消耗,可能还会不安全,因为只要是运行时判断,你就有可能对一个int类型插入一个string。

好了,我们简单的说了一下泛型的原理,那么如果go要实现泛型的话,基本上就是这两种方式,第二种方式是不是感觉和interface有种似曾相识的赶脚呢?恩,看上去一样,还是有本质区别的,第二种java那种方式是JIT实现,而interface是runtime的运行时实现,效率差得不是一点半点的。如果用第一种方式进行编译时的模板扩展呢?同样会遇到代码增多的情况,golang的目标文件本来就是把所有东西都集成进行来了,本来就很大了,再这么整一下,估计目标文件更大了。

我觉得即便golang开放泛型,估计也是用第一种方式,因为如果用运行时的方式的话,给runtime调度器平增不少压力,而golang肯定不会用JIT吧,所以第二种实现方式估计有点够呛。

一些其他的

对于GC,就不吐槽了,因为毕竟,真有GC问题的话,我就用CGO了,呵呵,或者说在设计的时候就会直接考虑某些模块用C来做了,而且目前的go版本,GC已经很不错了,大部分应用没啥问题了,golang把协程集成进语言中,势必导致大家不计性能问题,奔放的开协程,那这个坑就只能google自己来填了,新版本(1.8)对GC的支持已经很好了,但是,对性能有强要求的服务,某些代码,还是用C吧,哈哈。

当然,要是go有一个不带gc的实现,自己来管理内存的版本,那就好了,语法比C舒服,还有协程和管道这些东西,要是能自己管理堆内存的话,那就完美了。

最后,还有一些没有说完的,这篇就不说了,下次接着聊聊channel和goroutine,以及和C的混合编程。


如果你觉得不错,欢迎转发给更多人看到,也欢迎关注我的公众号,主要聊聊搜索,推荐,广告技术,还有瞎扯。。文章会在这里首先发出来:)扫描或者搜索微信号XJJ267或者搜索西加加语言就行