一句话总结:Java是一门很好的语言,Golang是一门很有意思的语言。
理念上的区别
Java诞生于企业信息化年代,那个时候软件工程师还是一份很严肃的职业,所以Java的设计从骨子里透着严谨和认真,并且力求完备周全,无论是虚拟机的规范,还是API文档都可以看出来在Gasling的带领下,Java团队构建了一个非常严谨而完备的技术平台,让Java Developer能够在不需要了解太多的底层细节就能写出安全可靠的代码,相比之前的C/C++大幅度降低了技术风险,符合普通人直觉的OOP设计以及Design Pattern的总结,也让很多初级程序员能够快速上手,把业务需求转换为代码实现,从而带来了整个Java世界的繁荣。
但是到了互联网时代之后,Java的很多设计让很多代码变得冗长而无趣,对于高水平程序员来说亟需一种表达和控制能力更强,也更简洁的语言,从而更好地提高开发的效率。所以Google结合了C和脚本语言和现代计算机语言理论发展,带来了Golang,由于有了前人的经验和踩过的坑,所以Golang从一开始就秉承着不一样的理念。从Golang Proverbs我们就可以看出来,其与Java有着一些有趣的区别。
Don't communicate by sharing memory, share memory by communicating.
首先Golang强调了通讯而非共享,在Java里面我们遵从的是OOP封装数据暴露方法的原则,但是对象是可以传递的,而对象(状态)实质上就是一片内存里的数据,而对象传递和引用被滥用之后,一个对象可能会被很多地方引用,并且都有可能改变状态,也就很容易带来系统内的逻辑耦合。而Golang从设计之初就希望通过通讯的方式最小化共享的状态,不论是通过channel来传递一个小的消息,还是最小化interface的实现,都透着让程序员减少关联和引用的画外音。
Gofmt's style is no one's favorite, yet gofmt is everyone's favorite.
其次为了避免大家在Code Convention上浪费时间争论括号应该放在哪儿,用空格还是用tab,Golang直接规定了标准格式,看起来有点霸道,但是保证统一的风格之后,确实提升了代码的可读性,也简化了代码检查工具的负担,因为只要用官方工具即可。从这一条我们可以看出来Golang的设计者是实用主义者,不愿意在一些意义不大的地方浪费时间,而是希望程序员能够专注于解决问题。
A little copying is better than a little dependency.
这一条有点反直觉,特别是对于一些动辄复用的Java程序员来说应该是很难受的。Golang里面并不鼓励为了复用而复用,潜台词是:“如果只是简单的几行代码,那么直接复制过来用即可,不要想那么多”。从过去的Java从业经历来看,这一条太赞了,有太多的xxx.utils和代码规约为了一两行代码的复用带来额外学习和理解成本,引入的依赖导致为了一根绳子牵出一头牛,导致产品越来越臃肿而难以改变。
简单直接
还有一条Golang proverbs没有提,但是我觉得也是Golang的一个设计理念,就是简单直接,能够直接给你的,就不要包一层再给你,程序员自己需要对代码负责。比如struct的exported fields可以直接访问,而不需要getter/setter包一层。而由调用者决定接口,而非提供者定义接口的做法,让我们可以避免搞出一堆IXxx
,直接写实现即可。
实用主义而非完美主义
实用主义在Golang的设计中随处可见,不再追求完美之后就给大家带来了一些有争议但是也非常有意思的设计,当然可能也会害死强迫症患者。
比如Golang的命名规约中,大写字母开头是表示exported,大致相当于Java里面的public,但是问题是内置函数全部都是小写,为啥内置函数就可以例外呢?还有slice的append()
方法,必须写成s = append(s, ...)
,为啥不能给个类似s.append(...)
的语法糖呢?
一些Java程序员容易踩的坑
nil是有类型的
最开始的时候,把nil
当成了Java的null
,但是很快就被打脸,比如下面的代码:
// custom error type
type myErr struct{
// fields omitted
}
func err() *myErr {
return nil
}
func checkErr(err error) bool {
return err == nil
}
这里checkErr(err())
会返回false
,原因在于err()
返回的是*myErr
类型的nil
,而后面checkErr()
判断的是error
类型的nil
,显然是对不上的。
slice和map
slice
和map
的实际存储是一个引用,但不是一个指针,这样也很容易让初学者迷糊。比如s = append(s, ...)
,这个调用过后,可能s
还是指向原来的存储,也可能不是,取决于原来的存储容量是否能承载新加入的元素。这样就有了类似下面的代码片段(来自Go 101):
a := [...]int{
0, 1, 2, 3}
x := a[:1]
y := a[2:]
x = append(x, y...)
x = append(x, y...)
fmt.Println(a, x)
初学者肯定想不到最后打印的是什么内容,顺便说一下Go 101是一个非常好的学习Golang的资源,值得一读。
还有就是对于未初始化的map
,读是没有问题的,写就会报错,比如:
var m map[string]string // nil map
fmt.Println(m["foo"]) // no error
m["foo"] = "bar" // panic
而在Java里面,类似的代码:
Map m = null;
m.get("foo"); // NullPointerException
channel不能被close两次
一般而言,我们会认为一个资源可以被close多次,除了第一次以外其他都相当于无操作(no op),但是在golang里面,最基本的语言元素channel竟然只能close一次,之后再次close就会panic。
comparable不能比大小
Golang内置的comparable
接口,只能用== !=
来比较相等或者不等,如果要用> <
来比较大小的话,需要的是ordered
接口,这个让对于习惯Java的Comparable
的人有点迷糊,同时也更加深刻地理解了Golang最小化接口定义的决心。
pprof给出来的数据看不懂
一开始我把pprof获得的heap信息当成了Java heap dump来看,但是发现完全看不懂,后来发现pprof给出来的是内存分配的调用链路,而不是当前的object关系图,所以从Java过来的同学需要好好适应一下。
type embedding不是继承
type embedding的机制,在用起来的时候有点像是继承,但是千万不要被表象所迷惑,因为type embedding不是继承,而且差异非常大。比如如下代码:
type Base struct {
}
func (b *Base) m1() {
}
func (b *Base) m2() {
b.m1()
}
type Child struct {
*Base
}
func (c *Child) m1() {
}
从外面调用的角度来看,Child
的m1()
能够直接被使用,但是调用m2()
的时候,里面实际调用的是m1()
,这个对于从Golang入手的人来说非常好理解,但对于习惯了OOP的人来说非常反直觉。实际上所有围绕着Base
类型的方法,当出现引用方法定义中的类this
指针的那个变量时,引用的都是那个具体的类型,不会有重载。外部使用只不过是编译器给加了一个语法糖而已。也就是说:
var cc Child
cc.m2()
cc.Base.m2()
上面两个方法调用是完全等价的,前面那个只不过是编译器帮你省了一个变量名而已。
文档在哪里?
一开始上手的时候,我发现一个非常麻烦的问题,找不到文档,或者找到了也语焉不详,看不太明白。因为Golang的文档相比Java过于简洁了,官方库还好,很多非常流行的第三方库也是寥寥数语,而且缺乏概括性的介绍,如果单看文档很难了解如何使用,所以开始的时候还是挺怀念java doc的。
熟悉了之后发现还好,要了解代码的逻辑,最好的方法还是直接看源代码,因为golang的所有库都是提供源代码的,所以直接进去看就好了,很多细节直接读源代码很快就明白了,如果看文档或者搜索都是很难找到正确答案的。
Golang里舒服的设计
Golang有很多让程序员舒服的设计,这里主要讲一下Java里很难看到的。
简单的赋值表达
有点类似JavaScript,在Golang里面可以直接对结构进行赋值,比如:
type Foo struct{
field1 string
field2 int
}
foo := Foo{
field1: "bar",
field2: 1234,
}
相比在Java里面需要一堆的setter/getter给大的数据结构赋值或者复制很多字段,来得轻松了很多。
另外map
的读写也非常简洁,不需要大量的get() set()
让代码变得赏心悦目。
类型别名
Golang不允许跨包给别的类型添加方法,但是提供了type alias这种办法允许程序员在别的数据类型基础上重新定义方法,这个设计大幅度避免了为了扩展一个已有结构的功能,而不得不加入一个子类的冗余代码。即使是内置的数据类型,依然可以做type alias,golang自带的库里就有很多直接给基本类型设置type alias来实现不同逻辑功能的例子。比如我们需要一个64 bits的timestamp,可以这么设计:
type Timestamp int64
func (t Timestamp) String() string {
return fmt.Sprintf("%d", t)
}
func (t *Timestamp) AddSeconds(secs int) {
*t = (*t) + int64(secs)
}
因为type alias也是一个新的类型,编译器不会把这个类型当作原来的类型(除非强转),所以样调用方可以很方便地使用这个新的Timestamp
类型,并且不用担心引入一些因为忽略了实际类型导致的bug。
包管理
Golang是一门互联网时代的语言,所以任何一个Golang的包,都是直接从官方repository里面下到的,不需要额外引入类似maven这种仓库,这样好处是非常方便。但是也需要注意golang的非官方包也出现在准官方repository里面,但是并不意味着有官方的质量保证和承诺,我们需要自行研究才能知道这些引入的第三方库是否合适。
辅助工具
除了语言以外,Golang提供了很多开箱即用的工具,比如golint, gofmt,pprof等,这些工具大大简化了新手上路的负担,识别了很多基础错误和问题。
打包成单个执行文件
这个功能相比于Java的CLASSPATH方便太多了,不会出现在这个环境里能正确执行,换个环境就不行的问题,原因在于CLASSPATH可以随意配置,而单个可执行文件已经包含了所有依赖前不会变更,避免了很多环境配置带来的问题。相比python的依赖管理更是强出100条街。
一点感想
Golang设计之初就定位为非OOP的语言,所以所有的OOP相关的习惯在Golang都不适用,甚至会带来bug和问题,所以从Java到Golang首先要提醒自己的,就是忘记OOP相关的概念和代码模式,切换成我要直接操作一个数据结构,该怎么写代码这个思维模式。其次就是需要忘记各种接口定义和封装,特别是各种IXxx
类型的接口定义,也不要写一堆的utils,因为Golang其实没有办法阻止我们写出类似Java般冗余的代码,加上一堆的额外接口和复杂的结构之后,只会让代码变得非常难看。还有一点,package的命名非常重要,既要简单易懂,又不能占用用户常用的命名,非常考验程序员(特别是中国程序员)的词汇量和对英语的理解。
未完待续