为 Java 开发者准备的 Go 教程 01:漫游了
大家好,我是 polarisxu。
在正式工作之前,一直使用 Java,虽然这些年对 Java 的关注变少了,但很显然,Java 用户群体特别大。不过,我也知晓,有不少 Java 用户在学 Go。我尝试写一系列文章,为 Java 开发者讲解 Go 语言。
这是第一篇,从大的层面简单对比下 Go 和 Java,算作是一次漫游。
整体上,Java 和 Go 之间有许多明显而微妙的区别,包括语言层面和运行时层面。我们这里主要在语言层面漫游。
这里的比较,没有贬低哪门语言的意思,旨在客观指出各自的特点。
Go 和 Java 都是 C 系语言,但 Go 更接近 C,包括风格、库等。
01 Go 是编译型语言,而 Java 是半解释的
与 C/C++ 一样,Go 语言源码会直接编译成目标计算机体系结构的机器语言。而 Java 源码编译成虚拟机语言,即字节码,并由 Java 虚拟机(JVM)进行解释(interpreted)。为了提高性能,字节码通常在运行时被动态编译成机器语言。JVM 本身是为特定的操作系统和硬件体系结构构建的。
而且 Go 是静态编译,一旦编译完成,Go 程序只需要一个操作系统就可以运行。Java 程序在运行之前需要计算机上安装有 JRE(特定版本)。许多 Java 程序可能还需要额外的第三方代码。
所以,虽然 Go 和 Java 都是跨平台的,但实现形式还是很不一样。
02 Go 和 Java 程序结构类似
这两门语言都支持包含方法和字段的数据结构的概念。在 Go 中,它们被称为 struct(结构体),而在Java中,它们被称为 class(类)。这些结构被收集到称为包(package)的分组中。在这两门语言中,包都可以分层排列(即嵌套包)。
Java 包顶层只包含类型声明。Go 包可以各种声明,如变量、常量、函数以及派生类型声明。
两门语言都通过导入(import)来访问不同包中的代码。在 Java 中,可以选择使用导入的类型(String 或 Java.lang.String)。在 Go 中,所有导入的名称必须始终是限定的(虽然可以本地导入,但不建议使用)。
03 Go 和 Java 代码风格的差异
这方面涉及到的内容不少,无法一一列出。这里提一些:
1)Go 与众不同,变量声明,类型放在后面。语言通常省略分号。
Java:int x, y, z;
Go:var x, y, z int
2)Java 方法只能返回一个值。Go 函数/方法可以返回许多值。
3)Java 方法和字段必须在它们所属的类型内声明。Go 方法是在所属类型之外定义的。Go 支持独立于任何类型的函数和变量。Java 没有真正的静态共享变量;静态字段只是某些类(相对于实例)的字段。Go 支持在可执行映像中分配的真正静态(全局)变量。
4)Java 只允许其他类型(类、枚举和接口)的类型扩展,而 Go 可以基于任何现有类型创建新类型,包括基本类型(如整数和浮点)和其他用户定义的类型。Go 可以支持这些自定义类型中的任何一种。
5)Go 和 Java 接口的工作方式非常不同。在 Java 中,类(或枚举)实现接口时,必须显式指定。在 Go 中,任何类型都可以通过实现接口的方法来实现接口,即隐式实现接口,所谓的鸭子类型。
6)Java 通过 try/catch 处理异常。Go 中是 error,另外有 panic 和 recover。
04 Java 是面向对象语言,而 Go 不完全是
面向对象的三大特性:封装、继承和多态。
Go 没有继承的概念,认为组合优于继承。不过,在 Go 中,可以通过内嵌来模仿部分类似继承的功能,但本质还是组合。
此外,Go 只在接口层面有多态,没有方法重载。
05 Java 不少特性是基于 Annotation 的,Go 没有 Annotation
许多 Java 库(特别是框架,比如Spring),都充分利用了 Java 的注解(Annotation)。注解提供元数据(通常在运行时使用),以修改库提供的行为。Go 没有注解,因此缺少此功能。Go 可以使用代码生成(go generate)获得与注释类似的结果。Go 有一种简单的注解形式,称为 tag,可用于自定义某些库行为,如 JSON 或 XML 格式。
06 Go 和 Java 都使用 GC 管理内存
这两门语言都使用 stack 和 heap 来保存数据。栈主要用于函数局部变量,堆用于其他动态创建的数据。在 Java 中,所有对象都在堆上分配。在 Go 中,堆上只分配可在函数生命周期之外使用的数据(通过逃逸分析确认)。在 Java 和 Go 中,堆都是垃圾收集的;堆对象由代码显式分配,但总是由垃圾收集器回收。
Java 没有指向对象的指针的概念,只引用位于堆中的对象。Go 允许访问指向任何数据值的指针(或地址)。在大多数情况下,Go 的指针可以像 Java 引用一样使用。
Go 的垃圾收集实现比 Java 的更简单,通常 Go GC 需要调优的情况较少(没有太多选项可配置)。
07 Go 和 Java 都支持并发,但方式不同
Java 有线程(Thread)的概念。而 Go 是 Goroutine,它是由语言提供的。Goroutine 通常被称为轻量级线程。Go 运行时支持使用比 JRE 支持的线程多得多的 Goroutine。
Java 支持同步控制。Go 具有类似的库函数。Go 和 Java 都支持跨 Thread/Goroutine 安全更新的原子值的概念。两者都支持显式锁定库。
Go 提供了通信顺序进程(CSP)的概念,作为 Goroutine 在没有显式同步和锁定的情况下进行交互的主要方式。Goroutine 通过 channel 进行通信,channel 是有效的管道(FIFO 队列),通常与 select 语句相结合使用。
08 Go 运行时比 JRE 更简单
Go 的运行时比 JRE 提供的运行时小得多。没有 JVM 等价物,但两者中都存在类似的组件,如垃圾收集。Go 没有字节码解释器。
Go 拥有大量的标准库。Go 社区提供了更多库。但是 Java 标准库和社区库在功能的广度和深度上都远远超过了当前的 Go 库(毕竟 Java 比 Go 早太多年了,而且生态确实好)。尽管如此,Go 库仍然足够丰富,可以开发许多有用的应用程序,特别是服务端程序。
所有使用过的库都会嵌入到 Go 可执行文件中,即前面提到的静态编译。可执行文件是运行程序所需的一切(而 Java 库在首次使用时动态加载)。这使得 Go 程序二进制文件通常比 Java 二进制文件(单个 “main” 类)大,但当加载 JVM 和所有依赖类时,Java 的总内存占用通常更大。
09 Go 程序的构建过程是不同的
Java程序是在运行时构造的类的合并,通常来自多个源(供应商)。这使得Java程序非常灵活,特别是当通过网络下载时。Go 程序是在执行之前静态构建的。启动时,可执行映像中的所有代码都可用。这提供了更大的完整性和可预测性,部署特别方便,但牺牲了一些灵活性。这使得 Go 更适合集装箱化部署。
Go 程序通常由 “go builder” 构建,这是一种结合了编译器、依赖项管理器、链接器和可执行构建器等的工具。它包含在标准 Go 安装中。Java 类被单独编译(由 Java 开发工具包(JDK)提供的 javac 工具编译),然后通常被组装成包含相关类的归档文件(JAR/WAR)。程序是从一个或多个存档中加载的。档案的创建,特别是包括任何依赖项,通常由独立于标准 JRE 的程序(如 Maven)完成。
其他方面,比如 Java 和 Go 都是过程式语言,此外,在函数式编程方面,Go 一开始,函数就是一等公民,而 Java 8 才有较好的支持。
参考
这个系列主要参考以下资料: