0. Java Go区别 精简版:
0.1 语法
0.1.1 访问权限
Java使用public、protected、private、默认等几种修饰符来控制访问权限;
golang通过大小写控制包外可访问还是不可访问。
0.1.2 异常处理
- java中错误(Error)和异常(Exception)被分类管理,二者的区别是:
-
Error(错误):是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。
-
EXCEPTION(异常):
- 运行时异常:都是RuntimeException类及其子类异常,如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)
- 非运行时异常 (编译异常):IOException、SQLException等以及用户自定义的Exception异常
- golang中只有error,一旦发生错误逐层返回,直到被处理。Golang中引入两个内置函数panic和recover来触发和终止异常处理流程,同时引入关键字defer来延迟执行defer后面的函数。golang弱化了异常,只有错误,在意料之外的panic发生时,在defer中通过recover捕获这个恐慌,转化为错误以code,message的形式返回给方法调用者,调用者去处理,这也是go极简的精髓;
0.2 面向对象
-
继承:
Java的继承通过extends关键字完成,不支持多继承;Go中没有显式的继承,可以使用组合方式实现继承;从该层面讲支持多继承;
-
封装
-
多态:
java的多态,必须满足继承,重写,向上转型;任何用户定义的类型都可以实现任何接口,所以通过不同实体类型对接口值方法的调用就是多态。
在Go语言中通过接口实现多态,对接口的实现只需要某个类型T实现了接口中的方法,就相当于实现了该接口。所有的类都默认实现了空接口;
-
接口:
java等面向对象编程的接口是侵入式接口,需要明确声明自己实现了某个接口。
Golang的非侵入式接口不需要通过任何关键字声明类型与接口之间的实现关系,只要一个类型实现了接口的所有方法,那么这个类型就是这个接口的实现类型。
0.3 性能
0.3.1 编译部署
-
Java通过虚拟机编译,使用JVM跨平台编译;
-
Go中不存在虚拟机,针对不同的平台,编译对应的机器码
0.3.2 并发
- Java中,通常借助于共享内存(全局变量)作为线程间通信的媒介,通常会有线程不安全问题,使用了加锁(同步化)、使用原子类、使用volatile提升可见性等解决;
- Golang中使用的是通道(channel)作为协程间通信的媒介,这也是Golang中强调的:不要通过共享内存通信,而通过通信来共享内存。golang使用GMP并发模型,多个goroutine之间通过Channel来通信,chan的读取和写入操作为原子操作,所以是安全的。
0.3.3 垃圾回收与内存管理
-
Java基于JVM虚拟机的分代收集算法完成GC;
-
golang内存释放是语言层面,对不再使用的内存资源进行自动回收,使用多级缓存,非分代,并发的三色标记算法。
一下文章转载自 丰巢技术团队 - 各有千秋:浅谈Go和Java的语法特性对比
1. 接口
在面向对象语言中,接口是一个绕不开的话题和特性,我们首先通过一段代码来看一下Go中的接口是如何设计和使用的:
在代码中定义了两个结构体:Teacher和Student;
定义了一个接口:Person,接口中声明了一个方法:notice();
在Teacher和Student中都存在notice()方法的实现,并且方法签名与Person中的notice()一致;
main包中的全局函数sendMsg(p Person) ,通过输入参数为Person的接口,来调用实现Person接口的notice方法;
函数sendMsg(p Person),是一个通过接口实现的多态应用;
1.1 Go中实现接口
package main
import "fmt"
type Teacher struct {
Name string
}
type Student struct {
Name string
}
type Person interface {
notice()
}
func (t Teacher) notice() {
fmt.Println(t.Name, "hello")
}
func (s Student) notice() {
fmt.Println(s.Name, "hello")
}
//sendMsg接收一个实现了Person接口的struct
func sendMsg(p Person) {
p.notice()
}
func main() {
t := Teacher{"Teacher Liu"}
s := Student{"Student Li"}
sendMsg(t)
sendMsg(s)
}
1.2 Java实现接口
Java的实现代码
-
Java中至少需要3个文件来实现;
-
Java使用interface关键字来定义接口;
-
Java在类中使用implements关键字来显式的实现接口;
-
Java中的方法是定义在Class里面的;
Person.java中定义Person接口代码
public interface Person {
public void notice();
}
Teacher.java中定义Teacher类
public class Teacher implements Person{
public String name;
public Teacher(String name){
this.name = name;
}
@Override
public void notice() {
System.out.println(name+" hello");
}
}
Student.java中定义Student类和main方法等
public class Student implements Person{
public String name;
public Student(String name){
this.name = name;
}
@Override
public void notice() {
System.out.println(name+" hello");
}
public static void sendMsg(Person p){
p.notice();
}
public static void main(String[] args){
Teacher t = new Teacher("Teacher Liu");
Student s = new Student("Student Li");
sendMsg(t);
sendMsg(s);
}
}
1.3 接口小结
-
Go中使用 type interface方式定义,Java使用interface关键字来定义接口;
-
Go的接口是隐式实现,没有类似于Java中implements的明确方式,只要struct中的方法覆盖了接口的所有方法,便可以认定struct实现了接口;
-
Go支持空接口,任何类都默认实现了空接口;
-
Go的接口实现更加灵活;
-
Java中的方法是定义在Class里面的;
2. 继承
下面的内容是wikipedia关于继承概念的解释
2.1 Go中继承
在Go中其实是没有继承的,但是我们可以通过匿名组合来实现继承的效果。下面的Go代码中通过匿名组合的方式来实现了继承的效果。
定义了两个“父类”,Person和Job;
定义了一个“子类”,Teacher;
Person中有一个方法ShowName(),Job中有一个方法ShowJob(),Teacher中也有一个ShowName方法;
在Struct Teacher中,通过匿名组合的方法,实现了Teacher继承自Person和Job;
package main
import (
"fmt"
)
type Person struct{}
type Job struct{}
func (p *Person) ShowName() {
fmt.Println("I'm a person.")
}
func (j *Job) ShowJob() {
fmt.Println("I'm a job.")
}
type Teacher struct {
Person
Job
}
func (t *Teacher) ShowName() {
fmt.Println("Teacher Liu.")
}
func main() {
t := Teacher{}
t.Person.ShowName()
t.ShowName()
t.ShowJob()
}
输出内容为
I'm a person.
Teacher Liu.
I'm a job.
2.2 继承小结
-
Go中使用匿名引入的方式来实现struct继承;
-
Go中“子类”方法如何和“父类”方法重名(匿名对象和外层对象的方法重名),默认优先调用外层方法;
-
Go中可以指定匿名struct以调用内层方法;
-
Go使用组合的方式,实现了多重继承;
3.闭包
维基百科中关于闭包的解释:
3.1 Java 中的闭包
3.1.1 Java8之前的闭包支持主要是依靠匿名类来实现的
public class ClosureBeforeJava8 {
int y = 1;
public static void main(String[] args) {
final int x = 0;
ClosureBeforeJava8 closureBeforeJava8 = new ClosureBeforeJava8();
Runnable run = closureBeforeJava8.getRunnable();
new Thread(run).start();
}
public Runnable getRunnable() {
final int x = 0;
Runnable run = new Runnable() {
@Override
public void run() {
System.out.println("local varable x is:" + x);
//System.out.println("member varable y is:" + this.y); //error
}
};
return run;
}
}
3.1.2 Java8对于闭包的支持
public class ClosureInJava8 {
int y = 1;
public static void main(String[] args) throws Exception{
final int x = 0;
ClosureInJava8 closureInJava8 = new ClosureInJava8();
Runnable run = closureInJava8.getRunnable();
Thread thread1 = new Thread(run);
thread1.start();
thread1.join();
new Thread(run).start();
}
public Runnable getRunnable() {
final int x = 0;
Runnable run = () -> {
System.out.println("local varable x is:" + x);
System.out.println("member varable y is:" + this.y++);
};
return run;
}
}
3.1.3 Java闭包小结
-
通过lamda表达式的方式可以实现函数的封装,并可以在jvm里进行传递;
-
lamda表达式,可以调用上层的方法里的局部变量,但是此局部变量必须为final或者是effectively final,也就是不可以更改的(基础类型不可以更改,引用类型不可以变更地址);
-
lamda表达式,可以调用和修改上层方法所在对象的成员变量;
3.2 Golang 中的闭包
package main
import "fmt"
func main() {
ch := make(chan int ,1)
ch2 := make(chan int ,1)
fn := closureGet()
go func() {
fn()
ch <-1
}()
go func() {
fn()
ch2 <-1
}()
<-ch
<-ch2
}
func closureGet() func(),func(){
x := 1
y := 2
fn := func(){
x = x +y
fmt.Printf("local varable x is:%d y is:%d \n", x, y)
}
return fn
}
代码输出如下:
local varable x is:3 y is:2
local varable x is:5 y is:2
3.3 Go闭包小结
-
Go的闭包在表达形式上,理解起来非常容易,就是在函数中定义子函数,子函数可以使用上层函数的变量;
-
Go的封装函数可以没有限制的使用上层函数里的自由变量,并且在不同的Goroutine里修改的值,都会有所体现;
-
在闭包(fn和fn2)被捕捉时(return fn,fn2),自由变量(x和y)也被确定了,此后不再依赖于被捕捉时的上下文环境(函数closureGet());
4.channel VS BlockingQueue
4.1 Go channel
不要通过共享内存来通信,而应该通过通信来共享内存
Go提供一种基于消息机制而非共享内存的通信模型。消息机制认为每个并发单元都是自包含的独立个体,并且拥有自己的变量,但在不同并发单元间这些变量不共享。每个并发单元的输入和输出只有一种,那就是消息。channel是Golang在语言级提供的Goroutine间的通信方式,可以使用channel在两个或多个Goroutine之间传递消息。channel是类型相关的,即一个channel只能传递一种类型的值,需要在声明channel时指定。可以认为channel是一种类型安全的管道。下面是channel的声明和定义的相关代码:
// 声明一个channel
var chanName chan ElementType
// 定义一个无缓冲的channel
chanName := make(chan ElementType)
// 定义一个带缓冲的channel
chanName := make(chan ElementType, n)
// 关闭一个channel
close(chanName)
-
向无缓冲的channel写入数据会导致该Goroutine阻塞,直到其他Goroutine从这个channel中读取数据。
-
向带缓冲的且缓冲已满的channel写入数据会导致该Goroutine阻塞,直到其他Goroutine从这个channel中读取数据。
-
向带缓冲的且缓冲未满的channel写入数据不会导致该Goroutine阻塞。
-
从无缓冲的channel读出数据,如果channel中无数据,会导致该Goroutine阻塞,直到其他Goroutine向这个channel中写入数据。
-
从带缓冲的channel读出数据,如果channel中无数据,会导致该Goroutine阻塞,直到其他Goroutine向这个channel中写入数据。
-
从带缓冲的channel读出数据,如果channel中有数据,该Goroutine不会阻塞。
-
总结:无缓冲的channel读写通常都会发生阻塞,带缓冲的channel在channel满时写数据阻塞,在channel空时读数据阻塞。
package main
import "fmt"
import "time"
func main() {
timeout := make(chan bool)
go func() {
time.Sleep(3 * time.Second) // sleep 3 seconds
timeout <- true
}()
// 实现了对ch读取操作的超时设置。
ch := make(chan int)
select {
case <-ch:
case <-timeout:
fmt.Println("timeout!")
}
}
-
Golang中的select关键字用于处理异步IO,可以与channel配合使用;
-
Golang中的select的用法与switch语法非常类似,不同的是select每个case语句里必须是一个IO操作;
-
select会一直等待等到某个case语句完成才结束;
在上面的代码中,我们用到了select来实现了读取channel超时的处理,那么,我们能不能使用select来进行发送消息的超时处理呢?
package main
import "fmt"
import "time"
func main() {
timeout := make(chan bool)
go func() {
time.Sleep(3 * time.Second) // sleep 3 seconds
timeout <- true
}()
// 实现了对ch写取操作的超时设置。
ch := make(chan int)
select {
case ch <-1 :
fmt.Println("send message success.")
case <-timeout:
fmt.Println("timeout!")
}
}
switch是可以有default语句的,那么select是否也有default的语句呢?
package main
import "fmt"
import "time"
func main() {
timeout := make(chan bool)
go func() {
time.Sleep(3 * time.Second) // sleep 3 seconds
timeout <- true
}()
// 实现了对ch写取操作的超时设置。
ch := make(chan int)
select {
case ch <-1 :
fmt.Println("send message success.")
case <-timeout:
fmt.Println("timeout!")
default :
fmt.Println("default case.")
}
}
4.2 Java BlockingQueue
在Java中,BlockingQueue是一个接口,它的实现类有ArrayBlockingQueue、DelayQueue、 LinkedBlockingDeque、LinkedBlockingQueue、PriorityBlockingQueue、SynchronousQueue等,它们的区别主要体现在存储结构上或对元素操作上的不同,但是对于take与put操作的原理,却是类似的。
生产者:
-
offer(E e):如果队列没满,立即返回true;如果队列满了,立即返回false;
-
put(E e):如果队列满了,一直阻塞,直到队列不满了或者线程被中断;
-
offer(E e, long timeout, TimeUnit unit):在队尾插入一个元素,如果队列已满,则进入等待,直到出现以下三种情况:等待时间超时、当前线程被中断、队列中的部分元素被消费;
消费者:
-
poll():如果没有元素,直接返回null;如果有元素,出队。不会阻塞当前线程;
-
take():如果队列空了,一直阻塞,直到队列不为空或者线程被中断;
-
poll(long timeout, TimeUnit unit):如果队列不空,出队;如果队列已空且已经超时,返回null;如果队列已空且时间未超时,则进入等待,直到出现以下三种情况:等待时间超时、当前线程被中断、队列中出现了新的元素;
4.3 小结
-
Go channel 和 Java BlockingQueue 都是实现生产者和消费者模式;
-
channel是在语言层面的实现,BlockingQueue是在sdk层面的支持;
-
Java程序使用线程池,要注意设置队列的大小,否则无界队列容易使得内存被耗光直到程序崩溃;
-
使用put和offer(E e, long timeout, TimeUnit unit)必须要做好防护,防止因为消费慢而造成的阻塞主线程的情况发生;
5. 并发数据结构
-
Java 并发数据结构:ConcurrentHashMap、CopyOnWriteArraySet、ConcurrentLinkedQueue、BlockingQueue、ConcurrentSkipListMap…
-
Go 并发数据结构:sync.Map
接下来,我拿两个数据库产品做了一些对比,一个是Golang+rust写的TiDB,另一个是Java写的ElasticSearch。
5.1 读写锁
在ES中只有11个地方使用到了读写锁,而TiDB(TiDB的计算模块)和PD(TiDB的调度模块)中有较多的地方使用到了读写锁。下面的代码是PD中在获取当前的region所在的leader store的源码:
// GetLeaderStore returns all Stores that contains the region's leader peer.
func (bc *BasicCluster) GetLeaderStore(region *RegionInfo) *StoreInfo {
bc.RLock()
defer bc.RUnlock()
return bc.Stores.GetStore(region.GetLeader().GetStoreId())
}
在TiDB和PD中有较多类似于上面的代码部分,在读的时候用读锁,在写的时候用写锁。根本原因,我觉得是Golang中没有对于并发数据结构的支持,那么这些数据一致性和稳定性的保障都留给了业务开发人员,因此在TiDB这样的底层数据库产品里面,会有很多的读写锁场景出现。
5.2 并发场景下使用map
map是我们在编码过程一定会使用到的数据结构,在jdk1.5之前的时代,如果因为HashMap使用不当,在并发环境下,一定会出现死链的情况,间接的造成了程序的崩溃。同理在Golang中,我们在并发环境下使用普通的map时,一定要加读写锁的,否则会造成程序崩溃退出。下面是PD中获取某个store的代码:
// GetStore searches for a store by ID.
func (bc *BasicCluster) GetStore(storeID uint64) *StoreInfo {
bc.RLock()
defer bc.RUnlock()
return bc.Stores.GetStore(storeID)
}
// GetStore returns a copy of the StoreInfo with the specified storeID.
func (s *StoresInfo) GetStore(storeID uint64) *StoreInfo {
store, ok := s.stores[storeID]
if !ok {
return nil
}
return store
}
// StoresInfo contains information about all stores.
type StoresInfo struct {
stores map[uint64]*StoreInfo
}
这段代码如果在Java中,因为jdk底层有大量的并发数据结构的支持,是可以有一些优化点的:
-
bc.RLock() 表明了这个锁的级别是cluster级别的,当我们对于cluster内的所有操作,都需要使用到这个锁,锁的粒度比较粗,我们可以尝试降低锁的粒度;
-
所有一致性和稳定性有可能出问题的地方,都要加锁处理,程序的复杂度过大;
-
在Java语言里面,可以使用ConcurrentHashMap,实现在业务代码中无锁处理;
下面是ES在RestClient中的一段使用ConcurrentHashMap的代码:
private final ConcurrentMap<HttpHost, DeadHostState> blacklist = new ConcurrentHashMap<>();
static Iterable<Node> selectNodes(NodeTuple<List<Node>> nodeTuple, Map<HttpHost, DeadHostState> blacklist,
AtomicInteger lastNodeIndex, NodeSelector nodeSelector) throws IOException {
...
for (Node node : nodeTuple.nodes) {
DeadHostState deadness = blacklist.get(node.getHost());
...
}
...
private void onResponse(Node node) {
DeadHostState removedHost = this.blacklist.remove(node.getHost());
if (logger.isDebugEnabled() && removedHost != null) {
logger.debug("removed [" + node + "] from blacklist");
}
}
private void onFailure(Node node) {
while(true) {
DeadHostState previousDeadHostState =
blacklist.putIfAbsent(node.getHost(), new DeadHostState(DeadHostState.DEFAULT_TIME_SUPPLIER));
...
}
failureListener.onFailure(node);
}
上面的代码体现了在Java中使用到ConcurrentHashmap时:
-
可以做到业务代码层面完全无锁语法,锁是在map内部实现的,简化代码复杂度;
-
ConcurrentHashmap的分段锁机制减少锁的粒度,提升并发性能;
-
通过putIfAbsent方法可以实现如果map中不存在key时,则加入value,否则不进行替换的原子操作;
5.3 小结
-
并发数据结构的支持是在sdk层面的差异,而非语言本身;
-
Java因为有并发大师Doug Lea的concurrent包,在并发数据结构的支持上,是领先于Go等其它语言的;
-
Go中在底层数据结构上实现同样并发性能的程序,对于业务开发程序员的技能要求更高;
-
在Golang中使用底层数据结构,经常需要使用到读写锁,Java中用的比较少;
-
Go中由于在业务代码中经常使用到读写锁,很容易扩大锁的粒度,造成性能的下降;
6. Goroutine VS thread
6.1 Go GPM 调度模型
本章节内容,主要来源于Golang布道师 Bill的 Scheduling In Go : Part II - Go Scheduler 当 Go 程序启动时,它会为主机上标识的每个虚拟核心提供一个逻辑处理器(P)。如果处理器每个物理核心可以提供多个硬件线程(超线程),那么每个硬件线程都将作为虚拟核心呈现给 Go 程序。
package main
import (
"fmt"
"runtime"
)
func main() {
// NumCPU 返回当前可用的逻辑处理核心的数量
fmt.Println(runtime.NumCPU())
}
每个 P 都被分配一个系统线程 M 。M 代表机器(machine),它仍然是由操作系统管理的,操作系统负责将线程放在一个核心上执行。
每个 Go 程序都有一个初始 G。G 代表 Go 协程(Goroutine),它是 Go 程序的执行路径。Goroutine 本质上是一个 Coroutine,但因为是 Go 语言,所以把字母 “C” 换成了 “G”,我们得到了这个词。你可以将 Goroutines 看作是应用程序级别的线程,它在许多方面与系统线程都相似。正如系统线程在物理核心上进行上下文切换一样,Goroutines 在 M 上进行上下文切换。
最后一个重点是运行队列。Go 调度器中有两个不同的运行队列:全局运行队列(GRQ)和本地运行队列(LRQ)。每个 P 都有一个LRQ,用于管理分配给在P的上下文中执行的 Goroutines,这些 Goroutine 轮流被和P绑定的M进行上下文切换。GRQ 适用于尚未分配给P的 Goroutines。其中有一个过程是将 Goroutines 从 GRQ 转移到 LRQ。Go最新的1.14版本中又对GPM调度模型进行了优化,增加了抢占式调度的安全点。
我个人觉得GPM调度模型是Go最精华的内容之一,Java社区对于协程已经呼唤了很多年,但是现在还没有看到能可以在语言层面落地的迹象。
6.2 Java thread
在这里,我也列举一些在Java中使用thread的一些小细节:
-
Java线程栈的空间大小默认是1M,从内存的角度讲,jvm也很难管理大量的线程,Goroutine栈空间大小可以做到几k;
-
Java可以使用threadlocal来存储和当前线程关联的内容,例如:log4j、pinpoint、skywalking等框架;
-
Java是有threadid的,Golang是不提供Goroutineid的;
-
因为Java的线程模型,所以我们在编写并发的网络编程时,需要借助netty这样的网络框架;
-
每一个多线程的Java服务,后面一定有各式各样的线程池为它服务;
7. 总结
本文通过接口、继承、闭包、Go channel、并发数据结构和协程等6个方面来分别对比分析了Go和Java中对于不同特性的实现方法。并针对闭包、并发数据结构等进行了详细的使用场景和在不同情况下的优缺点进行了对比分析。在Go和Java的语言差异上,肯定不止这6个方面,本文只是选择了在开发过程中,经常遇到的几个方面进行了对比分析。希望能给到对于其中一种语言比较熟悉,但是想要快速学习另一种语言的同学,起到一个思路启发的作用。
参考:
Java:详解Java中的异常(Error与Exception)
面试官:说说golang和java有哪些区别?