导读:对于软件开发的编程语言,其实没有万能灵药。
本文作者详细介绍了他使用Java和Go这两种编程语言,一个是传统语言,一个是新兴语言的工作方式。
实话说,我很喜欢Java这门语言。近几年来,我在公司里积累了大量关于EJB2,DB2与Oracle等后端开发的专业知识。
现在我转向到基于自然语言处理的开发方向,如Spring Boot、Redis、RabbitMQ、Open NLP还有UIMA等技术。
因此说来,我选择的语言是Java,这个语言一直有旺盛的生命力,写起程序来也很有意思。
开始测试Go!
在2017年初,我接到了一个很有意思的项目。这个项目围绕着自动化系统来监控农作物和花花草草等植物们。
原先的开发团队源代码部署在三个不一样的系统中:Windows,MacOS和ARM,他们使用Go语言做为网关。
坦白讲,当时我对Go语言一点儿也不熟悉,随着项目的发展,我的工作内容包括对新技术的学习以及产品实施。项目现有代码库结构复杂,我面临的挑战比较大。使用Go写的针对于物联网平台支持程序,要在三个异构系统上部署、测试与运维。这个系统使用单例模式编写,系统耦合严重,模块之间相互杂揉,可以说是一处错误,多处崩溃。我用Java的开发风格重新构建了网关的新版本,但后让我弄的比原来更丑陋、混乱了。
我后来升职为一家公司任技术总监。
我试着不再使用Java的方式来开发Go了,尽量更多的使用Go的语言特性来做,全面拥抱该语言。
如此下来我才真正发现,Go的确是一个创新且全面的编程语言,我的团队开始将它用它来开发各种各样的项目。
不容置疑的是,与任何编程语言一样,Go有优点也有不少缺点,我这个人不太会撒谎,有时候我还蛮想念Java的。
依照以往我的编程经验,软件开发方面没有灵丹妙药,编程语言就是典型。以下我来讲一下心得体会,使用一门传统语言和创新语言新手是怎么干活的。
Go与Java的相似之处
Go语言与Java都从属于C语言家族,既然是远房亲戚,它们有着相似的语法。所以,Java开发者阅读Go写的代码并不会太难,反之亦然。
Go语句在语句末尾并不使用;分号结束。不用担心,除了极少数情况,我阅读Go写的代码反倒感觉更清晰易读。
Go与Java都使用我最喜欢的功能之一,那就是垃圾收集器(GC),以防止内存泄漏。
C/C++程序员都知道,自己在写程序时要注意避免内存泄漏,垃圾回收机制让内存管理自动化,从而简化程序员在代码中处理类似的功能。
Go语言的GC工具的表现很优秀,它的处理时间在1.8版本中不到1ms即可完成。
在Go语言中,其GC的设置参数并不多,有一个唯一的GC变量用来设置初始时垃圾回收目标的百分比。在Java中,有4个不同的垃圾收集器,每一个都要有大量的设置工作。
Java和Go都被视为跨平台语言,但是Java需要Java虚拟机(JVM)来解析编译后的代码,而Go可以简单地将代码编译为给定平台的二进制文件。
Java与Go相比,Go比Java依赖度更低,因为Go每次为一个平台编译代码时都要创建一个二进制文件。从测试和DevOps角度来看,分别编译不同平台的二进制文件非常的费时,有时候使用GO组件时,跨平台的Go编译在一些情况下并不起作用。但是,使用Java则可以在有JVM的任何地言使用一个Jar。
Go占用内存很小,而且并不需要安装和管理虚拟机的关联依赖,以及复杂的注意事项。
接下来谈一下反射功能。Java的反射功能很强大,而Go的反射更偏复杂。Java是一种纯面向对象语言,所有内容都被视为对象。
在Java使用反射,需要为对象创建一个类,并从此类中获取所需要的信息。如下代码所示:
Class cls = obj.getClass();
Constructor constructor = cls.getConstructor();
Method[] methods = cls.getDeclaredFields();
接下来就可以访问构造函数、方法和属性,然后去调用和设置它们。
在Go语言中,则没有类的概念,它的结构中仅包含已声明的字段。因此,我们需要它的“反射”包来提供相关信息。
type Foo struct {
A int `tag1:"First Tag"
tag2:"Second Tag"`
B string
}
f := Foo{A: 10, B: "Salutations"}
fType := reflect.TypeOf(f)
switch t.Kind(fType)
case reflect.Struct:
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
// ...
}
}
我认为,这并不是一个大问题,因为Go中没有用于结构的构造函数,因此结果是许多原始类型,且必须分别处理,还需要认真考虑指针。
在Go中,我们还可以通过指针或值传递某些东西。Go语言结构具有字段的功能,而不是方法。
再来说一下两个语言的辅助功能。
Java具有Private、Protected以及Public这3个修饰符,用来对数据、方法和对象提供不同范围的存取。
Go语言中,有着和Java类似的语句,它们是exported/unexported修饰符。在Go中如果没有修饰符,以大写字母开头的所有内容都能够导出,并且在其它软件包中可见。unexported,以小写变量或函数仅在当前包中可见。
Go与Java大方面的差异
Go语言并不是一种面向对象(OOP)语言。它没有像Java中的继承,没有通过继承这一特性实现系统的多态性。
Go语言没有对象,只有结构体。但它可以通过提供接口实现一些面向对象的模式。同样道理,可以将将结构彼此嵌入,但是嵌入式结构无法访问宿主结构的数据和方法。
Go中使用组合,而不使用继承来组合一些所需的行为和数据。
Go是一种命令式语言,而Java则是一种声明式语言。
Go中没有像依赖式注入那样的东西,在Go中必须将所有内容明确的包装在一块。因此,在Go中编程末尽量少用一些魔术方法,一切都是可见的。
Go程序员要了解Go代码如何使用内存、文件系统以及其它关联资源的全部机制。
从另一方面,Java需要开发人员更多的关注自定义开发的业务逻辑部分,诸如如何创建、过滤、更改和存储数据。
从系统基础架构和数据库管理系统而言,所有这些都是通过配置和通过Spring Boot等通用框架来完成。
人们总是矛盾心理,我们对基础架构的部分感到枯燥乏味,因此这部分功能交给框架。这会给我们带来方便,但对于程序员来说,控制权在框架,也限制人们对整个流程优化的能力。
再讨论一下两个语言的变量定义,以及顺序。在Java中定义变量类似于以下:
String name;
在Go语言,这样来写:
name string
我在第一次使用Go时,没有;号的代码让我有些困惑,就像写文章没写句号一样,感觉没有写完。
使用Go语言的优势
Go有着简单且且优雅的并发能力。Go语言有着强大的并发模型,称之为“通信顺序过程”或CSP。在Go中使用n-to-m嗅探器,该嗅探器允许n个系统线程中发生m个并发执行。
可以利用Go语言的关键字:go(与该语言的名字相同)启动例程。比如下面的字符串:
go doMyWork()
此时函数doMyWork()将同时开始执行。
在Go中进程之间的通信可以通过共享内存(并不推荐)和共享通道来完成。它允许开发者使用GoMaxProcs环境变量定义一个多内核的健壮且流畅有并发系统。
Go提供了一种特殊模式-race(竞赛)来运行二进制文件,并同时检查运行情况。通过此机制来证明软件是并发安全的
run-race myapp.go
以上命令格式,即以竞赛检测模式运行一个应用程序。
我个人很喜欢Go提供的即开即用功能(golang.org/dl),它提供一个很好的例子,如sync同步功能(golang.org/pkg/sync)包。
比如针对实现Singleton模式实现,可以如下代码表示:
package singleton import ("sync")
type singleton struct { }
var instance *singleton
var once sync.Once
func GetInstance() *singleton {
once.Do(func() {
instance = &singleton{}
})
return instance
}
同步包还为并发实现映射、互斥锁、条件变量和等待提供了一种结构。它的另一个包atomic(golang.org/pkg/sync/atomic)提供了并发安全转换与数学运算。
实际上,Go提供了并发代码所需要的全部。
Duck typing
“如果你说话像鸭子,走路像鸭子,那它一定就是只鸭子”。这句话在Go的世界里是正确的。
在Go语言中,不需要定义某种接口,直接实现结构即可。在Java中,对象比较显式声明并实现了接口。
探查器。Go提供的性能分析工具,使用非常方便、快捷和容易。Go中的事件探查器有助于显示程序中的内存分配和CPU使用情况,还可以在可视化图形中加以说明,这对优化应用程序特别有用。
Java可以从Java VisualVM这些探查器中来开始,但它们不像Go的探查器那样简单,而且这些工具的功效取决于JVM,其获取的统计信息和垃圾收集与JVM的具体工作有关。
C 与Go
Go中允许对C语言进行简单且强大的集成。开发者可以在Go项目中编写有C语言代码的平台相关应用。从底层来讲,CGo可以让开发人员创始调用C代码的Go程序包。为了排除/包含指定平台的C代码段,如各种构建器选项,这些代码段可以实现应用程序的多平台化。
函数作为参数使用
Go函数可以用作变量,传递给另一个函数或用作结构的字段。Go语言的这种多功能性的确让人耳目一新。
而Java是从1.8开始结合了lambda的使用,但它并不是真正的函数,而是一个单功能的对象。它的这一特性就是想实现Go中使用函数的行为,而Go在一开始就
存在于语言特性中。
明晰的代码风格。在Go语言社区中,有着众多热情和能力强大的支持者,社区中有大量对Go的使用示例和操作的最佳实践和方法。
其地址为https://golang.org/doc/effective_go.html
在Go的函数中可以返回多个参数,这是一个非常有用和优秀的方式。如下代码:
package main
importt "fmt"
func returnMany() (int,string,error){
return 1,"example",nil
}
func main(){
i,s,err :=returnMany()
fmt.Printf("Returned %s %s %v",i,s,err)
}
Go语言的缺点
Go除了接口外,没有多态特性。Go中没有多态性,这表示如果在同一个程序包中,有两个函数具有不同的参数,但是含义相同,这时必须给它们修改为不同的名称。请看如下之代码:
func makeWorkIntt(number int){
fmt.Printf("Work done number %d",number)
}
func makeWorkStr(title string){
fmt.Printf("Work done title %s",title)
}
其实这些方法都做同样的事情,只是方法名称不同,而且代码比较丑陋。
此外,在Go中没有继承的多态性。如果嵌入到结构,则嵌入式结构只知道自己的方法,而不会知道宿主结构的方法。这对于一些有其它语言经验开发者有一定的挑战性,他们在之前使用的OOP语言主要使用继承处理,当使用GO语言时会有一些困惑。
随着时间的流转,我自己开始意识到继承式的多态化处理只是一种思维方式,结构式处理很可靠,也更明显,且运行时间可变。
关于错误处理,处理什么返回错误信息,以及如何返回错误。作为开发者,我们需要每次都返回错误并传递相关错误。有时候错误会隐藏,这确实是麻烦的所在,要记住检查错误并把它们传递出去。
在Java中异常处理要方便得多,比如RuntimeException,都可以不加入函数的签名中即可使用。
public void causeNullPointerException() {
throw new NullPointerException("demo");
}
/*
...
*/
try {
causeNullPointerException();
} catch(NullPointerException e) {
System.out.println("Caught inside fun().");
throw e; // rethrowing the exception
}
此外,Go语言没有泛型支持,虽然这一特性会增加复杂性,Go的创建者也认为代价高昂。在Go语言创建时,必须针对不同的类型重复使用自己的或生成代码。
没有注释
在Go中可以用代码生成部分注释,但是不幸的是,在运行时注释根本不能替换,这是有原因的——Go不是声明性的,且代码中不应包含任何魔术。
我喜欢在Java中使用注释,它们让代码更加优雅、简单、简约。这些注释可以生成大量文件,在做HTTP结点提供某些元数据时非常有用。在GO语言中,必须手动或直接制作Swagger文件,或作为结点功能提供特殊注释。Java中的注释是一种魔术方式,人们通常不必关心它们的工作方式。
再来谈一下Go中的依赖管理。在本文之前我曾写过一篇GO依赖项管理的文章。Go依赖管理环境在相当长一段时间中存在一些麻烦。最初,除了“Gopgk”外没有任何依赖项管理,后来又发布“Vendor”后被“vgo”取代。现在可以手动和使用go命令(如go get命令)来修改go.mod文件描述符来处理。
不幸的是,这种修改会引发依赖关系不稳定。
Go没有开箱即用的源镜像依赖关系管理机制,而Java有着诸如Maven和Gradle之类著名的依赖管理声明类工具。它们也可能用来构建,部署和处理其它集成用途。
我们在实际开发中使用Makefile、Docker-composer和bash脚本自定义构建所需的依赖关系管理,会让持续交付和集成的过程、稳定性变复杂。
在Go中,引用软件包的名称要包含托管的GitHub域名。例如:
import “github.com/pkg/errors”
这块实在有点奇怪,而且非常不方便,如果不更改整个项目代码库的导入,就无法用自己的实现替换其它人的代码。
而在Java中,导入通常以公司名字开头。像这样:
import by.spirascout.public.examples.simple.Helper;
当然,我想在GO中的这些依赖管理问题都只是暂时的,将来会以最有效的方式解决。
总结
Go语言中最有趣的一块是,它遵循一种代码命名准则,称为代码可读性方法的心理学。使用Go语言,可以使用单独的方法来构建清晰且可维护的代码,虽然看起来像是一些单词组合在一起,但实际上清晰可读。
一些使用Go进行Web开发的公司,它们将项目展示给我,代码体现了Go的快速,强大且易于理解,非常适合小型服务和并发处理。而对大型,复杂的系统,比如功能更复杂的服务以及微服务系统,Java暂时保持在世界顶级编程语言的地位。
尽管Java SE已经成为Oracle的收费商品,而Go则是技术社区的孩子。已经产生多个品牌的JVM,而Go的工具集则是相同的。
可以确定的是,不同的任务需要不同的工具,语言也是如此。