QQ群网友“四月份平民”编写的《Golang评估报告》,原文地址:https://docs.google.com/document/d/1NosYIbM6tfBqKh49BrHOngBfXuT1MfrvYXwc_ikwuMk/edit?pli=1
文章对Go语句的优缺点进行了详细地评估,很有参考价值,感兴趣的朋友可以看看。
1. Go简介
Go是由Google在2009年11月10日开源,2012年3月28日推出第一个正式版本的通用型编程语言,它为系统编程而设计。它是强类型化的语言,具有垃圾回收机制,并显式支持并发编程。程序由包构造,以此来提供高效的依赖管理功能。当前实现使用传统的 编译/链接 模型来生成可执行的二进制文件。
2. C/C++的缺陷
a.全局变量的初始化顺序
由于在C/C++中,全局变量的初始化顺序并不确定,因此依赖于全局变量初始化顺序的操作,可能会给程序带来不可预知的问题。
b.变量默认不会被初始化
由于变量默认不会被初始化,因此如果在程序中忘记初始化某个变量,就有可能造成一些奇怪的细节性错误,以至于在Coding Standard中都为之专门加以强调。
c.字符集的支持
C/C++最先支持的字符集是ANSI。虽然在C99/C++98之后提供了对Unicode的支持,但在日常的编码工作中却要在ANSI与Unicode之间来回转换,相当地繁琐。
d.复杂的继承模式
C++提供了单/多继承,而多继承则引入了大量的复杂性,比如“钻石型继承”等。细节请参阅《深度探索C++对象模型》。
e.对并发的支持
在C++中,并发更多的是通过创建线程来启用,而线程间的通信则是通过加锁共享变量来实现,很容易死锁。虽然在C++11中添加了并发处理机制,但这给本来就十分复杂的类型系统又添加了更重的负担。
关于这一点存在争议。内存泄漏是C/C++程序员经常遭遇到的问题,但随着垃圾回收算法的成熟,对于大多数开发者来说,自动回收带来的便利已经超过手工操作提高的效率。而在C++中虽然可使用智能指针来减少原生指针的使用,但不能杜绝它,因此这个问题仍然存在。
g.落后的包管理机制
C/C++中采用.h/.c(pp)文件来组织代码,这种方式使编译时间变得过于漫长;C++中的模板更让这个问题雪上加霜。
比如: 构造函数/析构函数/new/delete等。如果是动态库,当include不同版本的头文件时,容易生成版本不兼容的代码。
3.Go的优势
正如语言的设计者之一Rob Pike所说:
“我们——Ken,Robert和我自己曾经是C++程序员,我们设计新的语言是为了解决那些我们在编写软件时遇到的问题。”
这些问题中的大部分,就是在第2节中列举的内容。这一小节就是Go针对这些缺陷提出的解决方案。
a.Init
每个包都可以定义一个或多个init函数(原型为 func init()),init函数在包初次被导入时调用,同一个包内的多个init函数的执行的顺序是不定的,而如果这个包又导入了其他的包,则级连调用,所有包import完成,所有init函数执行完后,则开始main的执行。
而对于全局变量,以一个简单的例子来说明:
// package p
var gInt int
…
// package a
import “p”
…
// package b
import “p”
…
// package main
import (
“a”
“b” )
…
在package p中,我们定义了一个全局变量gInt,而p被package a,b所import,接着package main又按序import了a,b,即a在b前被import。a先import了p,所以此时gInt被初始化,这样就解决了C/C++中全局变量初始化顺序不一致的问题。
b.默认自动初始化
Go引入了零值的概念,即每个对象被创建的时候,默认初始化为它相应类型的零值。例如,string为””,指针为nil,int为0等等,这样就保证了变量在使用时,不会因为忘记初始化而出现一些莫名其妙的问题。此外,由于零值的引入,也方便了代码的编写。比如说sync包的mutex类型,在引入零值后,就能以如下方式使用:
var locker sync.Mutex
locker.Lock()
defer locker.Unlock()
…
而相应的C/C++代码,可能就要这样写了:
CRITICAL_SECTION locker
InitializeCriticalSection(&locker)
EnterCriticalSection(&locker)
…
LeaveCriticalSection(&locker)
DeleteCriticalSection(&locker)
忘记任何一步操作,都将造成dead lock或者其他的问题。
c.UTF-8
Go语言原生支持UTF-8编码格式。同时Go涉及到字符串的各种包,也直接为UTF-8提供了支持,比如:
str := "示例"
if str == "示例" {...}
OOP在Go中是通过组合而非继承来实现的,因为“继承”存在一些弊端,比如:“不适应变化”,“会继承到不适用的功能”。所以在编码实践中一般建议优先使用组合而非继承。在Go中则更进一步,直接去掉了继承,只支持组合。在定义struct时,采用匿名组合的方式,也更好地实现了C++中的“实现”继承,而在定义interface时,也可以实现接口继承。比如:
type A struct{}
func (a A) HelloA() {
…
}
type B struct{}
func (b B) HelloB() {
…
}
type C struct {
A
B
}
c := &C{}
c.HelloA()
c.HelloB()
此时c就拥有了HelloA、HelloB两个方法,即我们很容易地实现了“实现继承”
e.Go程(goroutine)与信道(channel)
Go对并发的支持,采用的是CSP模型,即在代码编写的时候遵循“通过通信来共享内存,而非通过共享内存来通信”的原则。为此,Go提供了一种名为“Go程”的抽象。由于Go程是一种高于线程的抽象,因此它使用起来也就更加轻量方便。而当多个Go程需要通信的时候,信道就成为了它们之间的桥梁。例如:
func goroutine(pass chan bool) {
println("hello i’m in the goroutine")
pass <- true
}
func main() {
pass:= make(chan bool)
go goroutine(pass)
<-pass
println("passed")
}
代码中通过关键字chan来声明一个信道,在函数前加上关键字go来开启一个新的Go程。此Go程在执行完成后,会自动销毁。而在通信过程中,可通过<-操作符向信道中放入或从中取出数据。
与C#、Java等语言类似,为了将程序员从内存泄漏的泥沼中解救出来,Go提供了自动垃圾回收机制,同时不再区分对象是来自于stack还是heap。
g.interface
除开Goroutine以外,Go语言的最大特色就是interface的设计,Go的interface与Java的interface,C++的虚基类是不同的,它是非侵入式的,即我们在定义一个struct的时候,不需要显式的说明它实现了哪一/几个interface,而只要某个struct定义了某个interface所声明的所有方法,则它就隐式的实现了那个interface,即所谓的duck-typing。
假设我要定义一个叫Shape的interface,它有Circle,Square,Triangle等实现类。
在java等语言中,我们是先在大脑中从多个实现中抽象出一个interface,即:
在定义Shape的时候,我们会先从实现类中得出共性,比如它们都可以计算面积,都可以被绘制出来,即Shape拥有Area与Show方法,在定义出了Shape过后,再定义Circle,Triangle等实现类,这些类都显式的从Shape派生,即我们先实现了接口再实现了“实现”,在实现“实现”的过程中,如果发现定义的接口不合适,因为“实现”是显式的指定了它派生自哪个基类,所以此时我们需要重构
public interface Shape
{
public float Area();
public void Show();
}
public class Circle : implements Shape
{
public float Area() { return ...}
public void Show() { ...}
}
(同理Square,Triangle)
而在Go中,因为interface是非侵入的,是隐式的,我们可以先实现Circle,Triangle等子类,在实现这些“实现类”的过程中,由于知识的增加,我们可以更好的了解哪些方法应该放到interface中,即在抽象的过程中完成了重构。
type Circle struct {}
func ( c Circle) Area() float32 {}
func ( c Circle) Show() {}
(同理Square,Triangle)
type Shape interface{
Area() float32
Show()
}
这样Circle,Square,Triangle就实现了Shape。
对于一个模块来说,只有模块的使用者才能最清楚的知道,它需要使用由 其他被使用模块提供的哪些方法,即interface应该由使用者定义,而被使用者在实现时,并不知道他会被哪些模块使用,所以它只需要实现自己就好了,不需要去关心接口的粒度是多细才合适这一类的琐碎问题,interface是由使用方按需定义,而不用事前规划。
Go 的interface与Java等的interface相比优势在于:
1.按需定义,最小化重构的代价
2.先实现后抽象,搭配结构嵌入,在编写大型软件的时候,我们的模块可以组织得耦合度更低。
h. Go命令
在unix/linux下为了编译程序的方便,都可能需要编写makefile.或者各种高级的自动构建工具(windows也存在类似的工具,只不过被各种强大的ide给隐藏在了背后),而Rob Pike等人当初发明Go的动机之一就是:”Google的大型的C++程序的编译时间过长”。所以为了达到:”编译Go程序时,作为程序员除开编写代码外,不需要编写任何配置文件或类似额外的东西。“这个目标,引入了Go命令族,通过Go命令族,你可以很容易实现的从在线repostory上获得开源代码,编译并执行代码,测试代码等功能,这与C/C++的处理方式相比,前进了一大步。
i. 自动类型推导
Go虽然是一门编译型语言,但是在编写代码的时候,却可以给你提供动态语言的灵活性,在定义一个变量的时候,你可以省略类型,而让编译器自动为之推导类型,这样减少了程序员的输入字数。
比如:
i := 0 ⇔ var i int
s := “hello world ” ⇔ var s string = “hello world”
j. 强制编码规范
在C/C++中,大家为大括号的位置采用K&R,还是ANSI,是使用tab还是whitespace,whitespace是2个字符,还是4个字符等琐碎的问题而争论不休,每个公司内部都定义了自己的Coding Standard.来强制约束,而随着互联网的蓬勃发展,开源项目的越发增多,这些小问题却影响了大家的工作效率,而有一条编程准则是”less is more”. 为了一致性,Go提供了专门的格式化命令 go fmt,用以统一大家的编码风格。
作为程序员,你在编写代码的时候,可以按你喜欢的风格编写,编写完成后 执行一下 go fmt命令,就可以将你的代码统一成Go的标准风格,这样你在接触到陌生的Go代码时,减少了因为编码风格差异带来的陌生感,强调了一致性。
C/C++虽未提供官方的单元测试与性能测试的工具,但有大量的第三方的相关工具,而由于可能每个人接触的,喜欢的工具不一样,造成在交流时负担,鉴于此Go提供了官方测试工具go test. 你可以很方便就可以编写出单元测试用例.
比如这样就完成了一个单元测试的编写:
// example.go
func Add( a,b int ) int {
return a+b
}
…
// example_test.go
func TestAdd( t *test.T) {
a,b,result = 1,2,3
if result != Add(a,b) {
t.Printf(“Failed”)
}
}
同理性能测试。
编写完成后执行 go test 就可完成测试
l. 云平台的支持
最近几年云计算发展得如火如荼,Go被称为“21世纪的C语言”,当然它也不能忽视这一块的需求,现在有大量的云计算平台支持Go语言开发,比如由官方维护的GAE,第三方的AWS, Heroku等,相关细节.
m. 简单的语法,入门快速,对于新成员很容易上手
Go本质上是一个C家族的语言,所以如果有C家族语言的经验,很容易上手。
4. Go的劣势
a. 调度器的不完善
b. 原生库太少/弱
c. 32bit上的内存泄漏
d. 无强大IDE支持
因为今年3.28日Go才推出Go1,所以目前Go还存在不足,a,c这两个缺陷2013年初的Go1.1可以解决,而b,d则需要等时间的积累才能完善。
5. Go的争议
a. 错误处理机制
在错误处理上,Go不像C++,Java提供了异常机制,而是采取检查返回值的方案来,这是目前Go最大争议之所在。
反对的理由:
1.每一步,都得做检查繁琐,原始。
2.返回的error类型可以通过 _ 给忽略掉
支持的理由:
1.在离错误发生最近的地方,可以最佳的处理错误2.异常在crash后抛出的stack的信息,对于别有用心者,会泄漏关键信息,而对于最终用户,他将看不明白究竟发生了什么情况,而错误将会让你有机将stack信息用更有意义的信息所替换,提高了安全性与用户友好性。
3. 异常也可以默认处理
b. new与变量初始化
在Go中new与delete与C++中的含义是不一样的,delete用以删除 一个map项,而new用以获得一个指向某种类型对象的指针,而因为Go支持类似如下的语法:
type T struct {…
}
obj := &T{} ⇔ obj = new(T)
同时Go提供另一个关键字make,用以创建内建的对象,所以 &T{}这种语法与make合起来,就可以替代new,即new冗余了,这与Go的简单原则相悖。
c. For…range不能用于自定义类型
为了遍历的方便,Go 提供了 for range语法,但是这种构造只能用于builtin的类型如slice和map,chan上,而对于builtin类型 ,即使官方包container中的相关数据结构也不行,这降低了for range的易用性,而目前在不支持泛型的前提下,要实现一个很友好的for range 看起来还是很不容易的。
目前Go只支持静态链接,这又是另一个引起争论的地方,争论双方 的论据就是动态链接的优、缺点,再此不再赘述。
e. 无泛型
现代的大多数编程语言都提供了泛型的支持,在Go1时中没有提供对 泛型的支持,按官方团队成员russ cox的说法,支持泛型要么降低编译器,要么降低程序员,要么降低运行效率,而这三个恰好与Go的快速,高效,易编写的目标是相冲突的,同时Go提供的interface{}可以降低对泛型期望。所以是否需要泛型也成了争论点
f. 首字母大写表示可见性
Go中只支持包级别的可见性,即无论变量,结构,方法,还是函数等,如果以大写字母开头,则他的可见性是公共的,在其他包中可以引用的,而以小写字母开头的则其可见性为其所在包,由于Go支持UTF8,而对于像中文这种没有大小写分别的字符在需要导出时,就会出现问题,关于这个问题,支持者的理由是:既然语言本身支持UTF8,那么在变量命名上就应该是一致的,不支持者的理由是,中国人用中文命名,日本人用日语命名.....,而且非要用类似中文这类符号编写的话,可以在中文符号前加一个英文符号.比如:
var 不可导出 int = 0
var E可导出 int = 0
6. 替代方案
a. Cgo
在前边的劣势部分有讲过,Go缺乏原生包,而现在世面上已经有大量的C实现的高质量的第三方库,比如openGL,openAL...,为了解决这个问题,Go引入一个叫做Cgo的命令,通过遵守简单的约定,就可以将一个C库wrapper成一个Go包,这也是为何在短短几年Go拥有了大量的高质量包的原因.cgo相关示例在此不再展示.
b. b/s
因为到目前为止Go尚未提供Gui相关的支持,同时在云计算时代,越发多的程序采用了b/s结构,而且Go对web编程提供了最完善的支持,所以如果程序需要提供界面,无论是本地程序,还是服务器程序,在当下建议使用b/s架构来替代。