到今天我使用java语言11多年多了,java伴随我开发了很多服务器项目,伴随我开发过android,对于java我有比较深厚的感情。我开始关注go是在2017年,那时我就职于某广告公司,公司的竞价系统因为需要高并发使用go开发。
从2018年开始创业,因为java非常熟悉,市场上java程序员非常多,方便招聘,首选java开发,从2018下半年开始,毅然决然新的项目使用go开发,是什么原因让我下定决心呢?
java的运行原理很简单,把源文件编译生成一种二进制中间码,存储在class文件中,然后再通过运行与操作系统平台环境相对应的Java虚拟机来运行class文件,执行编译产生的字节码,调用class文件中实现的方法来满足程序的Java API调用。后续android为了提升速度出了其他的字节码文件,和虚拟机不在本次讨论范围
java的前身是1992开发的Oak语言,1996年1月发布JDK 1.0,1999年4月发布HotSpot虚拟机,HotSpot成为了JDK 1.3及之后所有版本的Sun JDK的默认虚拟机
go发布于2009年,可以说java是20世纪的产物,go是21世纪的产物。跨世纪了[惊!]。
切换
我2008年在学习java的时候还是个学生,是有c和c++基础的(vc写过计算机,读取手机短信这种水平)。 用了3个月的时间才大致弄明白java做的事情。 弄明白了各种关键字作用,和面向对象的机制。实际去企业开发的时候还是会遇到各种问题。而限制使用范围的关键字也只用public和private。很少用连续的继承。
2018年我带着团队学习使用go的时候,只用2周时间大家就都掌握了,能填充代码开发了。 如果你是一个java程序员,从java到go的距离只有2周时间
开发
// java
@RestController
@RequestMapping(value = "/xx")
public class LotteryController {
@RequestMapping(value = "/xx1", method = RequestMethod.POST, produces = "text/html;charset=UTF-8")
public String xXXX1(HttpServletRequest httpRequest) {
return "success";
}
@RequestMapping(value = "/xx2", method = RequestMethod.POST, produces = "text/html;charset=UTF-8")
public String xXXX2(HttpServletRequest httpRequest) {
return "success";
}
}
使用iris框架写个post方法
// go
// 启动文件
app.Post("/xx/xx1", controller.Xxxx1)
app.Post("/xx/xx2", controller.Xxxx2)
// --------------------------------------
// 具体文件中方法实现
func Xxxx1(ctx context.Context) {
ctx.Text("success")
}
func Xxxx2(ctx context.Context) {
ctx.Text("success")
}
这个简单的例子可以看出 可以看出java spring的方法是注解在实现类中的,go是通过写在代码中的更方便。而且所有入口在一个文件中更方便查找。
对于杠精来说可以不用spring注解,使用spring配置文件,或者servlet。。。。
在处理IO时java用try catch finally,finally中还得分开try catch 关闭。我早就忍不了,我都怀疑强迫症的人会无限的写下去。。。
java启个线程
new Thread(new Runnable() {
@Override
public void run() {
// do
}
}).start();
而go的语法就精简很多了,启动个协程运行,只要在掉用的时候加关键字go。
通过代码我们可以看出go的代码非常精简
运行
go的协程设计确实更适合高并发,java开一个线程就对应于一个系统线程。go开一个协程不对应一个系统线程。一个系统线程内存消耗一般2M,而协程只需要2k。1k个线程是2G内存。2G的内存能放多少协程啊
go的runtime与用户代码一起打包成可执行文件,java的代码需要依赖于虚拟机,open jdk 8的jre有100多Mb呢。
go的协程没有那么神秘,可以说实现的很简单。使用G-P-M模型
G:Goroutine用于存储运行堆栈、状态和任务函数可复用,需要绑定到P才能被掉用,Goroutine会排队等待执行。
P:Processor逻辑处理器,提供执行环境,内存分配状态,Goroutine队列等。P的数量由用户设置的GoMAXPROCS决定,最大数256,P的数量决定了并行执行G队列的数量。物理CPU核数 >= P的数量
M:Machine 系统内核线程的抽象,代表真正执行的计算资源,在绑定有效的P之后,进入schedule循环,schedule循环机制是P的G队列中获取要被执行的G,M的数量由Go RunTime调整,M不保留G的状态,这样G在推出M之后,下次可能由其他的M执行
Sched:Go调度器。负责维护G的总队列,和P中存在的G队列和状态信息,创建M,清理回收M等工作
性能测试
是骡子是马拉出来溜溜
使用ab命令压测同一台机器(2C4G 操作是根据id查询数据库中的一条记录) 请求次数100000 线程数150,为了避免干扰启动go服务的时候关闭java服务,反之亦然。
java环境: spring boot 2.2.5 + mybatis 3.5.3 hikari连接池 连接数200
tomcat设置:JAVA_OPTS="-Xms256m -Xmx1024m -XX:PermSize=64m -XX:MaxPermSize=128m"
java第一次
java第二次
java第三次
java第四次
java第五次
java的统计情况
go环境: iris v11.1.1 + gorm v1.9.10
go第一次
go第二次
go第三次
go第四次
go第五次
go的统计情况
说明 通过压测我们可以看出Java的第一次压测劣势非常明显,java是编译成class文件, 第一次执行需要通过虚拟机进行类加载,之后的执行可以用加载好的文件执行。所以我去掉java的第一次重新计算了一下平均值
java内存 :tomcat启动之前机器已使用内存是769M,启动之后是1.20G,压测时内存峰值1.35G,后面几次压测之后,内存回收,回到1.21G
go内存 :go启动之前机器已使用内存是768M,启动之后是775M, 压测时内存峰值804M,,内存回收,回到784M
数据项 | java | go |
---|---|---|
总耗时平均 | 22.36s | 20.77s |
每秒处理数 | 4474 | 4812 |
每条处理耗时 | 33.55ms | 31.16ms |
传输速率 | 1044k/s | 1122k/s |
压测过程中cpu使用率 | 96%-99% | 96%-99% |
启动内存占用 | 459Mb | 7Mb |
压测过程最高占用内存 | 613Mb | 36Mb |
执行结束后内存占用 | 470Mb | 16Mb |
性能测试部分go完胜
打包编译速度
我使用的是macOS cpu i5 内存 8G
go 编译linux下的包用时 <2s
java maven 打war包用时 20s
docker image
构建docker image 的大小与方便程度决定了上线部署的速度
为了测试都基于ubuntu镜像
数据项 | java | go |
---|---|---|
最终镜像大小 | 151M | 33.8M |
操作步骤 | 1 添加jre和tomcat 2设置环境变量 3添加war包 | 添加可执行文件 |
优化空间 | 未知 | 使用scratch基础镜像体积更小 |
2019 发生了什么
go发布了两个版本1.12 和1.13 ,从1.13开始我才觉得go能正常使用了。之前go的使用有些弊端,首先打包就是个麻烦事。1.13版本Go Modules 默认成为了Golang 官方版本原生的包管理方式,包管理问题算是画上了一个句号。
还有一件非常重要的大事,之前在国内使用某系包,都会收到墙的影响,大家都是使用github上的源代替官方源,2019go有了官方承认的代理https://goproxy.cn,引包不再成为问题。
java发布了jdk12
Shenandoah GC算法,没发布在openjdk中。G1优化1(可中断 mixed GC)G1优化2 (归还不使用的内存),可以看出官方还是想改进垃圾回收。毕竟靠垃圾回收优化养活了Zing悲哀不悲哀
2019 的双11 阿里90%的容器都使用了Wisp2,这个Wisp2的最重要的一点就是给java安上了协程。真是笑出眼泪。
后续
java的库非常全面,也很好用。java程序员非常好招聘。垃圾回收一致被大家诟病,java也一直在这方面努力。
go后发优势明显,不过在2019之前因为墙和包管理的问题,非常难用。go的轮子还没有造那么多。不过正在快速补充进来。