站在Java程序员的角度对比下 Go语言 和 Java

2 / 186

先声明一下,我不是 Go 语言的专家。几周前我开始研究它,因此这里的陈述是第一印象。在本文的某些主观方面,我可能是错的。也许我稍后会写一篇对此的评论。如果你也是 Java 程序员,欢迎一起来交流,如果我在某些陈述中有误,也欢迎评论和纠正我。

Go 语言令人印象深刻

与 Java 相反,Go 语言被编译为机器代码并直接执行,这点与 C 语言非常相似,而 Java 是被编译为 JVM 可执行的二进制文件。Go 语言是一个面向对象的的计算机语言,但是在某种程度上他又非常像一个函数式编程语言,他更像是一个带了自动垃圾回收机制的 C 语言。如果我们认为编程语言的世界是线性的,那么它就介于 C 和 C ++ 之间。用 Java 程序员的眼光来看,有些事情有很大的不同,以至于学习起来都是具有挑战性的,甚至可以使人们对编程语言的结构、对象、类等东西有更深的理解。

我的意思是,如果您了解 Go 语言中如何实现OO,那么您可能还会了解 Java 有所不同的一些原因。

简而言之,如果您不耐烦:不要让自己对这种看似奇怪的语言结构感到惊讶。学习它,即使您没有要在 Go 中开发的项目,它也会增加您的知识和理解。

GC 和非 GC

内存管理在编程语言中至关重要。汇编使您可以做所有事情,或者更确切地说,它需要您完成所有事情。在 C 语言中,标准库中有一些支持函数,但是仍然需要您在调用 malloc 之前释放已分配的所有内存。自动化内存管理始于 C ++,Python,Swift 和 Java。Golang 也在此类别中。

Python 和 Swift 使用引用计数。当存在对一个对象的引用时,该对象本身拥有一个计数器,该计数器对指向该对象的引用数进行计数。没有向后的指针或引用,但是当新的引用获取该值并开始引用一个对象时,计数器将增加,而当引用变为 null / nil / 任何内容或引用另一个对象时,计数器将下降。因此,当计数器为零时,就没有对该对象的引用,可以将其丢弃。这种方法的问题在于,当计数器为正时,对象仍然可能无法到达。可能存在对象圆彼此引用,并且当该圆中的最后一个对象从静态,局部和其他可到达的引用中释放时,该圆开始像漂浮在水中的气泡一样漂浮在内存中:计数器均为正数,但对象不可访问。Swift 教程很好地解释了这种行为以及如何避免这种行为。但是重点仍然存在:您必须在某种程度上关心内存管理。

对于 Java 和其他 JVM 语言(包括 Python 的 JVM 实现),内存由 JVM 管理。有一个完整的垃圾收集,它不时在一个或多个线程中运行,与工作线程并行,或者有时停止那些(标记为停止运行)标记无法访问的对象,对其进行清除并压缩可能分散的内存。您只需要担心性能。

Golang 也在这一类别中,有一个很小的,很小的例外。它没有参考。它具有指针。差异至关重要。它可以与外部 C 代码集成,并且出于性能原因,运行时中没有像引用注册表这样的东西。执行系统不知道实际的指针。仍可以分析分配的内存以收集可达性信息,并且仍可以标记和清除未使用的 “对象”,但是无法四处移动内存以进行压缩。从文档上看,这对我来说并不明显,而且当我了解指针处理时,我正在寻找 Golang 向导实现压缩的魔力。我很抱歉学习,他们根本没有。没有魔术。

Golang 有一个垃圾回收器,但这不是 Java 中的完整 GC,因为没有内存压缩。不一定是坏事。它可以在很长一段时间内长时间运行服务器,而不会造成内存碎片。一些 JVM 垃圾收集器还跳过了压缩步骤,以减少清理旧版本时的 GC 暂停,并且仅作为最后的手段进行压缩。Go 中的最后一个步骤丢失了,在极少数情况下可能会引起一些问题。学习语言时,您不太可能会遇到问题。

局部变量

局部变量(有时是新版本中的对象)以 Java 语言存储在堆栈中。在 C,C ++ 和其他实现了调用堆栈的语言中也是如此。Golang 也不例外,除了…

除了可以简单地从函数返回指向局部变量的指针。这是 C 语言中的一个致命错误。在 Go 语言中,编译器意识到分配的 “对象”(我将在后面解释为什么使用引号)正在转义该方法,并相应地对其进行分配,以便它在函数和指针的返回中幸免于难。不会指向没有可靠数据的已经废弃的内存位置。

所以是绝对合法的:

package main
import (
    "fmt"
)
type Record struct {
    i int
}
func returnLocalVariableAddress() *Record {
    return &Record{1}
}
func main() {
    r := returnLocalVariableAddress()
    fmt.Printf("%d", r.i)
}

闭包

您可以在函数内部编写函数,也可以像使用函数语言(Go 是一种函数语言)一样返回函数,并且函数周围的局部变量充当闭包中的变量。


package main
import (
    "fmt"
)
func CounterFactory(j int) func() int {
    i := j
    return func() int {
        i++
        return i
    }
}
func main() {
    r := CounterFactory(13)
    fmt.Printf("%d\n", r())
    fmt.Printf("%d\n", r())
    fmt.Printf("%d\n", r())
}

函数返回值

函数不仅可以返回一个值,而且可以返回多个值。如果使用不当,这似乎是一个不好的做法。Python 做到了。Perl 做到了。它可能很好用。它主要用于返回值和 “nil” 或错误代码。这样,将错误编码为返回类型的旧习惯(通常返回 - 1 作为错误代码,并且在 C std 库调用中存在一些有意义的返回值的情况下返回一些非负值)被更具可读性的东西所代替。

分配侧的多个值不仅是功能。要交换两个值,您可以编写:

 a,b = b,a

面向对象

由于闭包和函数是一等公民,因此 Go 至少与 JavaScript 一样面向对象。但实际上不仅仅如此。Golang 具有接口和结构。但是它们并不是真正的课程。它们是值类型。它们按值传递,并且无论它们存储在内存中的什么位置,只有纯数据而没有对象标头或类似的东西。Go 中的结构与 C 中的结构非常相似 - 它们可以包含字段,但是它们不能彼此扩展,也不能包含方法。面向对象的方法略有不同。

不必将方法填充到类定义中,而可以在定义方法本身时指定结构。结构还可以包含其他结构,如果字段没有名称,则可以通过隐式成为其名称的类型来引用它。或者,您可以仅引用字段或方法,因为它们属于顶级结构。

例如

package main
import (
    "fmt"
)
type A struct {
    a int
}
func (a *A) Printa() {
    fmt.Printf("%d\n", a.a)
}
type B struct {
    A
    n string
}
func main() {
    b := B{}
    b.Printa()
    b.A.a = 5
    fmt.Printf("%d\n", b.a)
}

这几乎是一种继承。

当指定可以在其上调用方法的结构时,可以指定结构本身或指向该结构的指针。如果将方法应用于结构,则该方法将访问调用方结构的副本(此结构按值传递)。如果将方法应用于指向该结构的指针,则该指针将被传递(就像通过引用传递一样)。在后一种情况下,该方法还可以修改结构(在这种意义上,结构不是值类型,因为值类型是不可变的)。任何一种都可以用来满足接口的要求。在上面的示例中,Printa 应用于指向结构 A 的指针。Go 说 A 是该方法的接收者。

Go 语法对结构和指针也有点宽容。在 C 中,您可以拥有一个结构,并且可以编写 ba 来访问该结构的字段。如果指向 C 中的结构的指针,则必须编写 b-> a 才能访问同一字段。在指针 ba 的情况下是语法错误。Go 表示写 b-> a 是没有意义的(您可以从字面上解释)。当点运算符可以重载时,为什么用 -> 运算符填充代码?进行结构访问时,可以通过指针进行字段访问。很合逻辑。

因为指针在某种程度上和结构本身一样好,所以可以这样

package main
import (
    "fmt"
)
type A struct {
    a int
}
func (a *A) Printa() {
    if a == nil {
        fmt.Println("a is nil")
    } else {
        fmt.Printf("%d\n", a.a)
    }
}
func main() {
    var a *A = nil
    a.Printa()
}

是的,这就是重点 - 作为真正的 Java 程序员,您不应该惊慌。我们确实在 nil 指针上调用了一个方法!怎么会这样

输入变量而不是对象

这就是为什么我在 “对象” 周围使用引号。当 Go 存储一个结构时,它就是一块内存。它没有对象标头(尽管可以,因为它是实现的问题,而不是语言定义的问题,但实际上没有)。它是保存值类型的变量。如果变量类型是结构,则在编译时就已经知道。如果这是接口,则变量将指向该值,并同时引用其具有该值的实际类型。

如果变量 a 是接口而不是结构体的指针,则无法执行相同操作:会出现运行时错误。

接口实现

接口在 Go 中非常简单,而同时又非常复杂(或者至少与 Java 中的接口不同)。接口声明了一系列结构要实现与接口兼容的功能。继承的完成方式与结构相同。奇怪的是,如果某个结构实现了接口,则无需指定该结构。毕竟,实现接口的实际上不是结构,而是使用该结构或指向该结构的指针作为接收器的一组函数。如果实现了所有功能,则该结构将实现该接口。如果缺少其中一些,则说明实施不完整。

为什么在 Java 中而不是 Go 中需要'implements' 关键字?Go 不需要它,因为它已完全编译,没有什么比类加载器更能在运行时加载单独编译的代码了。如果一个结构应该实现一个接口,但不是,那么它将在编译时被发现,而无需明确分类该结构确实实现了该接口。如果使用反射(Go 拥有的反射),则可以克服此问题并导致运行时错误,但是'implements' 声明仍然无济于事。

语法紧凑

Go 代码紧凑且不容忍。在其他语言中,有些字符根本没用。自从 C 语言问世以来,我们已经习惯了这 40 年来,所有其他语言都遵循该语法,但这并不一定意味着这是最好的方法。自 C 以来,我们都知道,最好在'if' 语句中使用代码分支周围的 {和} 解决 “拖尾” 问题。(也许 Perl 是第一个要求这样做的主流 C 语言语法语言。)但是,如果必须使用花括号,就没有必要在括号之间加上条件了。如您在上面的代码中看到的:

...
    if a == nil {
        fmt.Println("a is nil")
    } else {
        fmt.Printf("%d\n", a.a)
    }
...

不需要,Go 甚至不允许。您可能还会注意到没有分号。您可以使用它们,但不需要。插入它们是对源代码的预处理步骤,并且非常有效。无论如何,大多数时候它们都很混乱。

您可以使用 ':=' 来声明一个新变量并为其赋值。在右侧,表达式通常定义类型,因此无需编写 “var x typeOfX = expression”。另一方面,如果导入程序包并分配一个以后不使用的变量,则是一个错误。由于可以在编译期间将其检测为代码错误,因此编译失败。很聪明。(尽管有时在导入要使用的软件包时会很烦人,并且在引用它之前我先保存了代码,IntelliJ 聪明地删除了导入内容只是为了帮助我。)

线程和队列

线程和队列内置在该语言中。它们称为 goroutines 和通道。要启动 goroutine,您只需编写 go functioncall()即可,该函数将在其他线程中启动。尽管标准 Go 库中有锁定 “对象” 的方法 / 功能,但本机多线程编程仍在使用通道。通道是 Go 中的内置类型,它是任何其他类型的固定大小 FIFO 通道。您可以将值推入通道,goroutine 可以将其拉出。如果通道已满,则推块处于阻塞状态,如果通道为空,则拉动将阻塞。

There Are Errors, No Exceptions. Panic!

Go 确实具有异常处理,但是不应像 Java 中那样使用它。异常称为 “紧急情况”,当代码中存在真正的紧急情况时使用。用 Java 术语来说,它类似于以 '... Error' 结尾的某些 throwable。如果存在某些例外情况并且可以处理某些错误,则系统调用将返回此状态,并且应用程序功能应遵循类似的模式。对于例如

package main
import (
    "log"
    "os"
)
func main() {
    f, err := os.Open("filename.ext")
    if err != nil {
        log.Fatal(err)
    }
    defer f.Close()
}

函数 “Open” 返回文件处理程序和 nil,或 nil 和错误代码。如果在 Go Playground 上执行它(单击上面的链接),则会显示错误。

这与我们使用 Java 进行编程时习惯的做法并不完全匹配。很容易错过一些错误条件并编写

package main
import (
    "os"
)
func main() {
    f := os.Open("filename.ext")
    defer f.Close()
}

只是忽略了错误。当我们对更长的命令链感兴趣时(如果这些命令中的任何一个产生了错误并且我们并不在乎哪个错误),检查每个可能返回错误的系统或应用程序调用时出错的可能性也很麻烦。

No Finally, Defer Instead

Java 与 try / catch / finally 功能一起实现的功能与异常处理紧密相关。在 Java 中,无论哪种方式,您都可以使用最终代码执行的代码。Go 提供了关键字 “defer”,使您可以指定一个函数调用,即使在发生恐慌的情况下,该方法也可以在方法返回之前被调用。这是解决该问题的一种方法,可以减少滥用的可能性。您不能编写仅延迟执行函数调用才能执行的任意代码。在 Java 中,您甚至可以在 finally 块中包含 return 语句,或者当在 finally 块中执行的代码也可能引发异常时,看到一团糟试图处理这种情况。去很容易。我喜欢。

Other…

起初似乎也很奇怪

  • 公共函数和变量都大写,没有关键字 “public”,“private”
  • 库的源代码将被导入到项目的源中(我不确定我是否理解正确)
  • 缺乏仿制药
  • 语言中以注释指令形式内置的代码生成支持(这实际上是 wtf)

通常,Go 是一种有趣的语言。即使在语言级别上,它也不能替代 Java。Java 和 Go 不应承担相同类型的任务 - Java 是企业开发语言,Go 是系统编程语言。与 Java 一样,Go 也在不断发展,因此我们可能会在将来看到一些变化。

本文由 码农俱乐部 翻译自:https://dzone.com/articles/comparing-golang-with-java 转载请在文章正文内容中注明出处!

  • 死不了 22天前

    搞 java 的, 有必要学习 go 语言吗?

    必要肯定谈不上,但是会 java 的去做 go 语言开发门槛真的很低。

  • 搞 java 的, 有必要学习 go 语言吗?