版本 | 日期 | 备注 | |
---|---|---|---|
1.0 | 2023.5.15 | 文章首发 |
本文首发于泊浮目的掘金:https://juejin.cn/user/1468603264665335
0. 概要
最近因为业务需要在学Go语言,虽然之前也用过别的语言,但主力语言一直是Java。在这里也想主要想用Java做对比,写几篇笔记。
这篇主要是讲语言本身及较为表面的一些对比。
这里的对比用的是Java8,Go的版本是1.20.2。
1. Compile与Runtime
- 在静态、动态链接支持方面,两者相同。
- Go在Runtime时,程序结构是封闭的。但Java并不是,基于Classloader的动态加载,实现许多灵活的特性,比如Spring,FlinkSQL。但这样做会让Java应用的Startup时间更长。
- Java的Runtime久经打磨,也是面向长时间应用设计。
- Go直接编译成可执行文件,而Java是先编译成Class文件,然后JVM去解释执行。
有兴趣的同学可以看我之前的的一篇笔记:《笔记:追随云原生的Java》
2. 命名规范
- Go语言在变量命名规范遵循的是C语言风格,越简单越好。
- Java建议遵循见名知意。
-
比如:
- 下标:Java建议index,Go建议i
- 值:Java建议value,Go建议v
- 我认为语言上偏简单的设计,则对工程师的能力要求更强。
3. 变量的零值支持
- 这点其实是Go在设计时,认为C在这上面有所不足——未被显式初始化的对象,其值是不确定的(当然有些编译器支持做初始化)。
-
Go对于变量默认值的描述是:当通过声明或调用new为变量分配存储空间,或者通过复合文字字面量或调用make创建新值,且不提供显式初始化时,Go会为变量或值提供默认值。
- 所有整型类型:0
- 浮点类型:0.0
- 布尔类型:false
- 字符串类型:””
- 指针、interface、切片(slice)、channel、map、function:nil
- 另外,Go的零值初始是递归的,即数组、结构体等类型的零值初始化就是对其组成元素逐一进行零值初始化。
- 对比Java,我认为这是个仁者见仁智者见智的事。default值并不一定是你想要的值,如果你忘记初始化了,在runtime时直接炸出来也是个好的选择,而不是把问题藏起来。当然,对Go默认值熟悉的人,这对他们来说可以少写一点代码,很舒服。
4. 标准库对于工程能力的支持
- 无论是Format还是Test以及模块管理,Go都是开箱即用的,比较舒服。如果标准库的确很好用、社区的迭代能力强,那这是个好事,现在看来就是。
- Java对于这块都是经过了长期的发展,相关的工具链比较成熟,相当于是物竞天择留下来的。
5. Composite litera(复合字面值)
可能没人听过这个说法,举几个例子:
m := map[int]string {1:"hello", 2:"gopher", 3:"!"}
复合字面值由两部分组成:一部分是类型,比如上述示例代码中赋值操作符右侧的map[int]string;另一部分是由大括号{}包裹的字面值。
在声明对象时,也有类似的用法:
// $GOROOT/src/net/http/transport.go
var DefaultTransport RoundTripper = &Transport{
Proxy: ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
Go推荐使用field:value的复合字面值形式对struct类型变量进行值构造,这种值构造方式可以降低结构体类型使用者与结构体类型设计者之间的耦合,这也是Go语言的惯用法。
这个是真的很香,Groovy和Kotlin也有类似的支持,很方便使用。尤其是构造函数特别长的时候,你可能说builder也可以做到,但谁不想少写几行代码呢。
6. 对于编程范式的支持
- 在Java中,类是一等公民。支持继承、组合,对函数式编程有一定的支持。
- 在Go中,函数是一等公民(多返回值),对于函数式编程支持较为完备。对面向对象完全通过组合来实现。
- 从传参是否可以传函数上,我们就可以看出Go的支持比Java好。Java传参中,传一个函数,其实是通过一个匿名对象来传,而Go是真正的一个函数。Kotlin在这块相对Java好了点,至少在编写时。
- 在Java中,你想写个工具函数,也要先声明一个类再写进去。略Verbose,其实这个类我们不会把它new出来,只是为了放个函数,所以我们写了个类。但实际用的时候,
XxxUtils.method
,前面的Xxx其实有一定的提醒作用,可以作为一个上下文来猜测里面的逻辑。但是如果我们在method里写清楚,当然也可以做到同样的功效,所以这点来说Go是比较舒服的。 - Go的对象方法声明方式比较特殊:
//声明一个类型
type MyInt int
//绑定一个方法
//func后面的()里,相当于声明了这个方法绑定的类型。在Go语言里叫做recevier,一个函数只能有一个recevier,且不能是指针、接口类型。
//不能横跨Go包为其他包内的自定义类型定义方法。
func (i MyInt) String() string {
return fmt.Sprintf("%d", int(i))
}
//在编译期,会把方法的第一个参数变成recevier。很简单的实现。有点像Koltin中的Extension Properties。
- Go的Interface是隐式的,只要你实现了对应的方法,就是这个Interface的实现。这个在一开始使用的时候会很不适应,但这个松耦的一种体现——隐式的interface实现会不经意间满足依赖抽象、里氏替换、接口隔离等设计原则。
- Go并没有继承。类似的做法叫做类型嵌入(type embedding)的方式。简单来说就是你有一个T1,T2类型,他们有各自的方法,当你声明一个T类型时并包含了T1,T2类型的field,那么T就拥有了T1,T2的方法。这个实现的确比继承舒服多了,继承很容易写出一些坏味道的代码。这是一种委派思想的实现(delegate)。JS中原型链从外表看起来也有点像这种。
- Go的方式支持多返回值。这点是比较舒服的,如果在Java中要返回多个值,就要考虑封装成对象了,比如Tuple2,Tuple3…之类的,这在其他的一些JVM语言中随处可见。
7. 异常流:Error与Exception
- Go里面的error相当于Java的可检异常,Panic相当于Java的RuntimeException和Error。
- 如果你觉得Go里面大量的
if err != nil
让代码嵌套时,可以看看一些优化if else的技巧,比如我博客里有。 - 总的来说,像是在不同的实现做同一件事。也是个仁者见仁智者见智的事。
8. 并发
- Java用的POSIX原语义的线程。而Go是自己实现了一套用户态的线程,或者说叫协程。
- POSIX原语义的线程总体来说易用性没这么好,需要牢记一些知识点才可以避免踩坑。Go在这点上比较友好。
- 性能上,由于实践时一般Java会用线程池,所以创建、销毁的代价还好。其实Go也有自己的线程池,用线程去绑多个协程。但在上下文切换上,的确是POSIX原语义的线程代价会大点。
- 为了避免一个协程把线程独占住,在编译期、以及一些标准库API上都要做缜密的设计。