Go语言具体体现在哪里呢?

首先,我想做个免责声明,我不是 Go 语言专家。几周前我才开始学习,所以本文是我对 Go 的靠前印象。文中我的一些主观看法可能是错的。以后我可能会发文再探讨本文的一些观点。在此之前,先看看本文吧。如果你是一个 Java 开发者,很高兴与你分享我的感受和经历,更期待你的留言评论,如果我有一些错误阐述,请不吝指教。

不同于 Java,Go 编译生成机器码,并被直接执行,非常类似 C。因为它不是一个虚拟机,这与 Java 有着天壤之别。Go 支持面向对象,并在一定程度上支持函数式编程,因此它不仅仅是一种具备自动垃圾回收机制的类 C 语言。如果我们将程序语言发展看作线性的话(事实上不是),Go 介于 C 和 C++ 之间的某种状态。在 Java 开发者看来,Go 是如此的与众不同,以至于学习它本身就是一种挑战。通过对 Go 的学习,可以更深入理解程序语言的构造,对象及类等等都是如何实现的。这些知识在 Java 中同样适用。

我相信,如果你知道 Go 是如何实现面向对象的,你也会明白 Java 以不同的途径实现的一些原因。免得你觉得我絮絮叨叨,简言之吧:不要被 Go 中看起来怪异的结构吓到,即便你没有项目要用 Go 开发,也去了解它,这会增加你的知识和理解。

GC 还是不 GC,这是个问题

内存管理对于编程语言至关重要。汇编允许你操作所有东西,或者说要求你必须全权处理所有细节更合适。C 语言中虽然标准库函数提供一些内存管理支持,但是对于之前调用 malloc 申请的内存,还是依赖于你亲自 free 掉。从C++、Python、Swift 和 Java 开始,才在不同程度上支持内存管理,Go 语言也是他们中的一员。

Python 和 Swift 采用引用计数方案。当存在一个对象引用时,对象自身持有一个计数器,用于统计有多少个引用指向当前对象。对象中并没有反向引用或指针。当一个引用获取对象的值,并指向这个对象时,计数器自增;当一个引用变为 null/nil/其他值 时,计数器自减。很显然,当计数器为0时,这个对象就没有被引用,可以被作废了。这种方法的问题是,计数器大于0,但是对象却可能已失效。当对象彼此形成环形引用时,通过静态、局部或者其他有效引用释放环中最后一个对象时,整个引用环就悬在内存中,就像气泡悬浮在水中:所有对象的计数器都大于 0,但是所有对象都已失效。Swift 教程对这种情况做了很好的解释,并说明了避免的方法。可惜,结论还是那样:你始终需要在某种程度上关心内存管理。

对于 Java 和其他语言的 JVM (包括 JVM 的Python实现),内存是完全由 JVM 管理的。与工作线程同时运行着 1 个或者多个线程,周期性的运行全局垃圾回收,或者暂停所有线程(众所周知的 stop the world),标记所有失效对象,清理它们,并压缩可能存在的内存碎片。你较早需要操心的是性能问题。

Go 语言与上述情况大同,又有点小异。Go 中没有引用,只有指针,这是非常重要的区别。Go 语言可以被外部 C 代码集成,出于性能考虑,Go 运行时中也没有类似引用表之类的东西。真实的指针对调用者是不可知的。申请到的内存依然被分析,以获得对象有效性相关信息,无用“对象”依然可被标记和清理,但是内存不能通过移动实现压缩。我在文档中没有找到太多相关信息,由于我理解指针的处理机制,我一直期待 Go 语言存在某种实现内存压缩的天才魔法。我很失望的了解到,它根本没有内存压缩。毕竟,魔法不常有。

Go 包含垃圾回收机制,但是不是跟 Java 一样完整的垃圾回收机制,它不能进行内存压缩。这也未尝是一件坏事。它可以持续运行服务很长一段时间,而且不会产生内存碎片。某些 JVM 垃圾回收器也会跳过内存压缩,以减少垃圾回收造成的服务停顿,直到必要时才执行。Go 语言中,必要时才进行的这一步没有了,在个别情况下可能会引起一些问题。不过在你学习该语言时,不大可能需要考虑这个问题。

Java 语言中,局部变量(新版本中,有时候对象也是)被保存在栈中。C、C++等等其他类似实现调用栈的语言也是如此。Go 语言也差不多,除了… …

除了函数可以返回局部变量的指针。这种做法在 C 语言中绝对是致命错误。当 Go 编译器发现被创建的“对象”(晚点晚再解释用引号的原因)将会脱离函数作用域,它会妥善处理这种情况,保证该对象在函数返回后继续存活,其指针不会指向废弃的内存地址,获得不确定的数据。

这就是我为什么用引号的“对象”。Go保存的结构体,其实是内存中的一小片区域。其中不存在对象头信息(确实有可能存在,这与具体的实现有关,而非语言本身的规定,通常是没有类头信息的)。变量本身就保存着值的类型信息。如果变量类型是一个结构体,那么在编译阶段这些信息就是已知的。如果变量类型是接口,那么它就成为值的指针,与此同时引用该值真正的类型。

如果变量即不是接口也不是结构体的指针,你无法完成同样的功能:只会得到一个运行时错误。

Go 中的接口实现非常简单,同时也有非常复杂(换言之,至少与 Java 的实现差别很大)。接口定义了一组函数,如果希望结构体可以使用接口,结构体就应当实现这些函数。继承的实现与结构体类似。比较奇特的是,你不需要明确定义即将实现接口的结构体。从根本上讲,与其说结构体实现了接口,不如说接口中的函数将结构体或结构体指针当作接受者(reciver)。如果接口中所有函数都被实现了,那么结构体就实现了这个接口。如果部分函数没有实现,接口的实现就是不完整的。

为什么我们在 Go 中不需要 “implements” 关键字,而 Java 需要呢?Go 不需要它是因为 Go 完全编译的,其中不存在运行时加载独立编译的代码的类加载器。如果一个本来要实现接口的结构体没有实现接口,这个错误会在编译阶段就被发现,不需要明确说明这个结构体会实现接口。如果你使用反射技术(Go 是支持的),你就可以绕过这一点,并引发运行时错误,“implements” 声明对这种做法无能为力。

Go 中即不需要,也不允许用圆括号包含条件语句。也许你也发现了,语句中没有分号。你可以使用分号,但是不是必须的。在预编译阶段,它们会被自动插入代码中,非常高效。通常额外书写它们都会带来一些干扰。你可以用 ‘:=’ 声明一个新变量,同时为之赋值。等式的右值通常就可以定义类型,因此没必要编写 ‘var x typeOfX = expression‘。另一方面讲,如果你 import 一个不被使用的包或者定义了一个未用变量,这被认为是个bug。这些在编译阶段就会被检测为代码错误,还是非常智能的(虽然有时候挺闹心,我会 import 一个晚点用到的包,但是在我引用这个包之前,每当我保存代码时, IntelliJ 就会自动帮我删掉这个包)。

线程和队列是 Go 的内建功能。它们被称为 go协程(goroutines) 和 管道(channels)。只要你编写 go functioncall(),这个函数就会以不同的线程运行。 虽然在 Go 库中有对 “对象” 加锁的方法/函数,但是 Go 原生的多线程编程是利用 channels 实现的。channel 是 Go 的内建类型—— 适用于任何类型的固定大小先进先出(FIFO)管道。你可以向 channel 中 push 一个新值,goroutine 则从中 pull 出此值。如果 channel 已满,push 操作阻塞;如果 channel 已空,则 pull 操作阻塞。

Go 有异常处理机制,但是与 Java 中的用法不同。异常被称为 ‘panic’ ,当代码中出现问题的时候会被调用。在 Java 中异常实现以抛出类似 ‘…Error’ 之类的信息实现。当出现可被处理的异常情况或者错误时,错误状态由系统调用返回,然后程序中的函数以如下模式处理。

Java 通过 try/catch/finally 特性实现了紧密耦合的异常处理机制。在 Java 中你可以有一段绝对会在最后执行的代码。Go 通过 ‘defer’ 关键字实现了这个特性,它允许你指定一个函数调用,该函数会在当前方法返回前调用,即使在出现 panic 的情况下也是。这在解决问题的同时,几乎不会给你滥用的机会。你不能在函数里随便写点代码,然后延迟调用该函数。在 Java 中你甚至可以让 finally 代码块返回状态码,或者为了处理 finally 代码块中可能出现的异常,把一切搞得一团混乱。

公共函数和变量是首字母大写的,Go 没有类似 ‘public’, ‘private’ 的关键字。
库的源代码会被导入到工程代码中(我不是很确定我真的明白这个特性)。
不支持泛型
代码生成特性的支持是语言内建的,以注释指令方式实现。(简直 Bee 了狗)
总而言之,Go 是个有意思的语言。即便在语言层面,Go 也不是 Java 的替代品。Java 和 Go 本不是服务于相同任务的 —— Java 是企业开发语言, Go 则是系统开发语言。Go 和 Java 一样,都在不断的开发中,相信在未来我们会看到更多变化。
武汉网站设计www.zhandodo.com

免责声明:文章内容不代表本站立场,本站不对其内容的真实性、完整性、准确性给予任何担保、暗示和承诺,仅供读者参考;文章版权归原作者所有!本站作为信息内容发布平台,页面展示内容的目的在于传播更多信息;本站不提供任何相关服务,阁下应知本站所提供的内容不能做为操作依据。市场有风险,投资需谨慎!如本文内容影响到您的合法权益(含文章中内容、图片等),请及时联系本站,我们会及时删除处理。


为您推荐