Go 1.14更新:
Module support in the go command is now ready for production use.
We encourage all users to migrate to go modules for dependency management.
好消息呀!Go module終于被官方推薦使用在生產(chǎn)中了。官方也建議將項(xiàng)目遷移到Go module下管理。Go被詬病了多年的包管理問(wèn)題終于解決了(似乎?)。
但是泛型功能什么時(shí)候才能來(lái)呢??。。
Go語(yǔ)言已經(jīng)被很多大型企業(yè)使用在其基礎(chǔ)設(shè)施的開(kāi)發(fā)了。作為一個(gè)新生語(yǔ)言,為什么它有這么大的魅力呢?下面我整理了網(wǎng)上各位大佬的觀點(diǎn),配合我自己對(duì)Go的理解,來(lái)總體介紹一下這門(mén)語(yǔ)言的優(yōu)缺點(diǎn)(主要以C/C++、Java、Python作為比較對(duì)象)。
如果你是C/C++、Java、Python的開(kāi)發(fā)者,可以完全看懂這篇文章。快來(lái)和我一切速覽Go語(yǔ)言吧!
我這里是以go 1.13作為基準(zhǔn)的。
本文不是Go語(yǔ)言教程,不介紹語(yǔ)言細(xì)節(jié),只是粗略地展示一下Go語(yǔ)言。
本文將會(huì)持續(xù)更新~
下面的目錄供你先預(yù)覽:
Go優(yōu)點(diǎn)
開(kāi)發(fā)效率
在寫(xiě)Go的時(shí)候,你會(huì)很容易寫(xiě)出動(dòng)態(tài)類型語(yǔ)言的感覺(jué)。這是一個(gè)非常好的特性,得益于Go的自動(dòng)類型推斷(類似于C++11的auto),大多數(shù)情況下,你可以不用關(guān)心變量的具體類型。
如果你是從Python來(lái)的程序員,會(huì)很喜歡這一點(diǎn)。
雖然Go的寫(xiě)法很有動(dòng)態(tài)類型語(yǔ)言的感覺(jué),但是它實(shí)際上是一個(gè)靜態(tài)類型語(yǔ)言,這樣就不會(huì)有動(dòng)態(tài)語(yǔ)言的缺點(diǎn),能在編譯時(shí)檢查出很多問(wèn)題(動(dòng)態(tài)語(yǔ)言只能在運(yùn)行時(shí)檢查出來(lái))。
下面我們簡(jiǎn)單對(duì)比一下Go和Java聲明變量的不同:
java:
// 簡(jiǎn)單變量
Integer a = 23;
// 對(duì)象
Person person = new Person();
// 通過(guò)方法獲取
Something sth = createSomething();
go:
// 簡(jiǎn)單變量
a := 23
// 對(duì)象
person := Person{}
// 通過(guò)方法獲取
sth := CreateSomething()
Go是不是很有Python的感覺(jué)呢?
運(yùn)行效率
Go語(yǔ)言的運(yùn)行效率是很高的。目前Go的運(yùn)行效率和Java差不多,但是Go比優(yōu)化了多年的Java年輕的多,因此潛力也更多。
在高并發(fā)的情況下,Go的表現(xiàn)會(huì)更好。因此很多企業(yè)將Go作為服務(wù)器語(yǔ)言,用于替換原先C++的位置。
雖然Go比C++還是要慢的,但是它的開(kāi)發(fā)效率比C++實(shí)在是高上太多了,在硬件越來(lái)越便宜的今天,Go未來(lái)在服務(wù)器基礎(chǔ)設(shè)施領(lǐng)域必定會(huì)占據(jù)更多市場(chǎng)。
少即是多(缺點(diǎn)?)
Go語(yǔ)言遵循“少即是多”的設(shè)計(jì)理念,提供更少的語(yǔ)言特性。這會(huì)讓Go語(yǔ)言顯得不那么“臃腫”。特別是OOP,Go語(yǔ)言在OOP上更像C語(yǔ)言,將OOP神秘的面紗揭示得一干二凈。在Go中,僅提供了結(jié)構(gòu)體、組合等少數(shù)幾個(gè)功能,沒(méi)有直接提供繼承等功能,OOP只是一個(gè)語(yǔ)法糖。
甚至對(duì)于封裝,Go語(yǔ)言也只是一個(gè)命名的事而已(首字母大寫(xiě)即為public,小寫(xiě)為private)。
下面我們用Java和Go進(jìn)行一個(gè)簡(jiǎn)單的對(duì)比:
java:
public class Student {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public void show() {
System.out.println(this.name, this.age)
}
}
go:
type Student struct {
name string
age int
}
func NewStudent(name string, age int) Student {
return Student{name: name, age: age}
}
func (student Student) Show() {
fmt.Println(student.name, student.age)
}
這種一切從簡(jiǎn)的設(shè)計(jì)思路很受很多C語(yǔ)言程序員的喜愛(ài),但是也有一些從高級(jí)語(yǔ)言轉(zhuǎn)過(guò)來(lái)的人認(rèn)為,Go提供的語(yǔ)言特性太少了;當(dāng)然也有人覺(jué)得這種特性少的語(yǔ)言更能剖析編程的本質(zhì),寫(xiě)出來(lái)的代碼更加有美感。
當(dāng)然,給小白的好處就是,Go學(xué)習(xí)起來(lái)比其他語(yǔ)言簡(jiǎn)單,特別是對(duì)于C/C++程序員來(lái)說(shuō),轉(zhuǎn)型做Go是很簡(jiǎn)單的。
當(dāng)然,關(guān)于“少即是多”是好是壞,就仁者見(jiàn)仁智者見(jiàn)智了。
gofmt格式統(tǒng)一
go語(yǔ)言提供了很多小工具,其中最受歡迎的是gofmt。gofmt可以把任意格式的Go語(yǔ)言源代碼統(tǒng)一格式化為統(tǒng)一的格式。
這樣,有了官方指定的格式,我們終于不用為了代碼格式的統(tǒng)一吵得焦頭爛額了。直接gofmt一下即可。
并發(fā)
這是Go的一大賣點(diǎn),Go是原生支持并發(fā)的,并且Go的并發(fā)單位是協(xié)程(在Go中被叫做goroutine)。
關(guān)于協(xié)程的介紹,請(qǐng)見(jiàn):Go協(xié)程。
在go中,只需要一個(gè)go關(guān)鍵字就可以實(shí)現(xiàn)啟動(dòng)一個(gè)協(xié)程并運(yùn)行。這是一個(gè)非常棒的特性,對(duì)于其它大部分語(yǔ)言,都需要使用系統(tǒng)庫(kù)來(lái)實(shí)現(xiàn)并發(fā)。
下面再來(lái)對(duì)比Java和Go:
java:
package com.test.main;
import java.lang.Runnable;
import java.lang.Thread;
class Worker implements Runnable {
@Overried
public void run() {
System.out.println("run concurrent");
}
}
public class Main {
public static void main(String[] args) {
Worker worker = new Worker();
new Thread(worker).start()
}
}
go:
package main
import "fmt"
func main() {
go func() {
fmt.Println("run concurrent")
}()
}
怎么樣,是不是寫(xiě)起來(lái)比Java簡(jiǎn)單多了。
Java的juc包支持很多并發(fā)控制的工具,例如Executor、Lock、CountDownLatch、CyclicBarrier、Semaphore、Exchanger等。這些工具需要花一定時(shí)間去學(xué)習(xí)。
但是在Go中,goroutines之間的交互更多是用channel來(lái)實(shí)現(xiàn),當(dāng)然,Go也有提供Lock、WaitGroup(類似于Java的CountDownLatch)等功能。但是channel卻可以完成goroutine之間同步的大部分需求(Go開(kāi)發(fā)者也建議多使用channel)。這也符合Go的少即是多設(shè)計(jì)理念呢。
而且,channel也是Go內(nèi)置支持的呢。
部署簡(jiǎn)單
Go的build輸出的直接是可以運(yùn)行的二進(jìn)制文件,這就比Java簡(jiǎn)單多了。這意味我們的部署只需要簡(jiǎn)單地把一個(gè)二進(jìn)制文件丟到服務(wù)器上運(yùn)行即可(甚至服務(wù)器不需要安裝Go環(huán)境),而如果是Java,還需要在服務(wù)器上安裝一個(gè)jre。這對(duì)運(yùn)維部署人員來(lái)說(shuō)是個(gè)好事。
Go的部署也比C/C++簡(jiǎn)單,Go不需要什么繁瑣的靜態(tài)鏈接、動(dòng)態(tài)鏈接的過(guò)程。所有代碼倉(cāng)庫(kù)都會(huì)被編譯到一個(gè)可執(zhí)行文件中(當(dāng)然,這也有可能導(dǎo)致Go的可執(zhí)行文件比較大)
在大多數(shù)時(shí)候,只需要下面一行命令即可編譯Go項(xiàng)目為可執(zhí)行文件:
$ go build -o runnable main.go
go構(gòu)建器會(huì)自動(dòng)解決所有庫(kù)、連接等問(wèn)題。我們不用再去寫(xiě)冗長(zhǎng)的makefile了,也不需要專門(mén)去搞個(gè)maven這樣的第三方構(gòu)建工具。
庫(kù)
Go自帶的庫(kù)特別強(qiáng)大,特別是它的http庫(kù),可以滿足大多數(shù)Web開(kāi)發(fā)的需求了。更不用說(shuō)它的json、net、text、runtime等庫(kù)了。
自帶map
Go語(yǔ)言是原生支持map的。在Java中要使用map需要進(jìn)行導(dǎo)包,C++更不用說(shuō),光stl和boost的選擇就夠你頭痛的了。
下面我們還是拿Java和Go做個(gè)對(duì)比:
java:
Map<String, Object> man = new HashMap<>();
man.put("name", "Wang");
man.put("age", 18);
man.put("birthday", new Date());
System.out.println("name =", man.get("name"));
System.out.println("age =", man.get("age"));
System.out.println("birthday =", man.get("birthday"));
go:
man := make(map[string]interface{})
man["name"] = "Wang"
man["age"] = 18
man["birthday"] = time.Now()
fmt.Println("name =", man["name"])
fmt.Println("age =", man["age"])
fmt.Println("birthday =", man["birthday"])
這樣的寫(xiě)法更像Python。
生態(tài)圈
Go有一個(gè)殺手級(jí)別的項(xiàng)目:Docker。以及Kubernetes。這兩個(gè)東西的火熱程度不用說(shuō)大家心理已經(jīng)知道。
以容器技術(shù)在未來(lái)的趨勢(shì),只要Docker不倒,Go在容器領(lǐng)域就會(huì)一直火熱下去。
測(cè)試
Go自帶了一套很好用的測(cè)試組件,Go的測(cè)試不再使用傳統(tǒng)的assert。測(cè)試失敗需要手動(dòng)調(diào)用函數(shù)。
在測(cè)試失敗后,Go也不會(huì)馬上終止測(cè)試程序,而是會(huì)把測(cè)試程序堅(jiān)持運(yùn)行完畢。
Go的測(cè)試遠(yuǎn)不如此,Go支持并行測(cè)試、基準(zhǔn)測(cè)試等。對(duì)于服務(wù)器-客戶端程序的測(cè)試也有支持。
關(guān)于Go的測(cè)試,這里有一個(gè)很好的教程:Go testing教程。
Go缺點(diǎn)
吹完了Go,作為一個(gè)新生語(yǔ)言,Go還是有很多缺點(diǎn)的。許多地方甚至為人詬病。作為一個(gè)轉(zhuǎn)型做Go的,我當(dāng)然希望Go越來(lái)越好,所以我們需要直面這些問(wèn)題。
包管理(go?module大法好)
在Go 1.11以前,Go使用GOPATH對(duì)Go項(xiàng)目進(jìn)行管理。這需要把當(dāng)前項(xiàng)目的所有依賴放到一個(gè)vendor目錄下。這對(duì)開(kāi)發(fā)人員來(lái)說(shuō)是一個(gè)噩夢(mèng):
- 項(xiàng)目之間不能復(fù)用依賴
- 依賴沒(méi)有版本控制,涉及到依賴的版本更新、回退、多版本共存等問(wèn)題時(shí),你會(huì)感到絕望
- 如果一個(gè)依賴引用了其它依賴,你也會(huì)絕望的
還好社區(qū)有很多工具例如vgo可以在一定程度上解決這些問(wèn)題,但是還是很蛋疼。
終于,在Go 1.11以后,引入了go module,這個(gè)工具類似于Python的包管理,可以通過(guò)簡(jiǎn)單的命令來(lái)下載全局的依賴包。在項(xiàng)目中通過(guò)定義go.mod來(lái)引入依賴包,并且這個(gè)文件可以自動(dòng)生成。
有了go module,之前Go的包管理噩夢(mèng)就能在一定程度上得到解決,希望未來(lái)能得到大力推廣。
錯(cuò)誤處理
Go的錯(cuò)誤全部是通過(guò)返回來(lái)進(jìn)行的。Go并沒(méi)有傳統(tǒng)的try...catch。這意味著你在進(jìn)行錯(cuò)誤處理的時(shí)候需要寫(xiě)大量的這種代碼:
result, err := fun()
if err != nil {
// handle error
}
當(dāng)然,Go支持函數(shù)式編程,你可以通過(guò)函數(shù)式編程的方式,通過(guò)wrapper模式來(lái)省略很多這樣的代碼。但是在很多業(yè)務(wù)場(chǎng)景,還是會(huì)很蛋疼的。
所以Go并不非常適合寫(xiě)業(yè)務(wù)代碼,目前行業(yè)內(nèi)寫(xiě)業(yè)務(wù)還是以Java這種語(yǔ)言為主。
當(dāng)然,Go實(shí)際上也有實(shí)現(xiàn)try...catch的方式,但是比較蛋疼。我們可以對(duì)比一下:
java:
public class TestTryCatch {
public static void main(String[] args) {
try {
throw new Exception();
} catch (Exception e) {
System.out.Println("發(fā)生了錯(cuò)誤!");
}
}
}
go:
package main
import "fmt"
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println("產(chǎn)生了錯(cuò)誤!")
}
}()
panicFun()
}
func panicFun() {
panic("我是錯(cuò)誤")
}
通過(guò)defer+recover可以修復(fù)Go的panic,但是我還是覺(jué)得沒(méi)有try...catch理解和用起來(lái)簡(jiǎn)單。
缺乏泛型
這是我個(gè)人非常討厭的一點(diǎn)。作為一個(gè)靜態(tài)語(yǔ)言,Go居然沒(méi)有泛型。這就像回到了幾十年前的Java一樣。
如果我想做一個(gè)稍微“通用”一點(diǎn)的功能,就會(huì)涉及到大量的類型向下轉(zhuǎn)型。我們知道,從JDK1.5開(kāi)始,Java就強(qiáng)烈不建議使用向下轉(zhuǎn)型而使用泛型,因?yàn)樗_實(shí)很不安全,即使在你事先知道對(duì)象類型的時(shí)候。
然而在2020年,Go仍然在使用大量的類型向下轉(zhuǎn)型。
雖然Go的設(shè)計(jì)理念是“少即是多”,Go的設(shè)計(jì)者將泛型和繼承作為一個(gè)整體從Go中刪去了。但是作為類型安全的一個(gè)保證,我認(rèn)為只要有interface{}(類似Java中的Object)的存在,泛型就必不可少。
希望在Go未來(lái)的版本看到泛型。
缺少框架(優(yōu)點(diǎn)?)
不像Java Spring,Go沒(méi)有一個(gè)大一統(tǒng)的框架。光是在web框架領(lǐng)域,就有很多選擇:
- gin
- beego
- iris
- echo
當(dāng)然,這也和Go自帶的庫(kù)很強(qiáng)大有關(guān)系,很多人不喜歡框架,覺(jué)得框架限制了他們發(fā)揮的空間;也有人覺(jué)得框架能夠快速開(kāi)發(fā),符合現(xiàn)代開(kāi)發(fā)的要求。
這點(diǎn)就因人而異了。
缺少更多的數(shù)據(jù)結(jié)構(gòu)
Go語(yǔ)言只提供了數(shù)組、Slice和map。對(duì)于棧、堆、隊(duì)列等其它數(shù)據(jù)結(jié)構(gòu),以及并發(fā)安全的數(shù)據(jù)結(jié)構(gòu),并沒(méi)有直接的支持。
我覺(jué)得這很大是因?yàn)镚o沒(méi)有泛型,如果要做一個(gè)通用的其它數(shù)據(jù)結(jié)構(gòu),就不得不處理interface{},這對(duì)類型安全來(lái)說(shuō)是一個(gè)災(zāi)難。
所以如果我們要實(shí)現(xiàn)某個(gè)數(shù)據(jù)結(jié)構(gòu),只能針對(duì)自己的struct手?jǐn)]了。
GC
在Go GC經(jīng)過(guò)了以下的發(fā)展階段:
- Go 1.3之前:STW(Stop The World) 非常簡(jiǎn)陋的GC算法,在內(nèi)存超過(guò)閾值或定時(shí)的條件下,暫停所有任務(wù),執(zhí)行mark+sweep(標(biāo)記清除)。在高內(nèi)存場(chǎng)景下,這意味著任務(wù)的長(zhǎng)時(shí)間停頓,是一種災(zāi)難。
- Go 1.3:Mark STW + Sweep。將mark和sweep分開(kāi)。但是也需要暫停所有任務(wù),但是暫停過(guò)程只進(jìn)行mark,mark之后恢復(fù)其它任務(wù),sweep通過(guò)協(xié)程異步進(jìn)行。這在一定程度上減少了GC的開(kāi)銷,減少STW的時(shí)間。
- Go 1.5:三色標(biāo)記法。對(duì)mark進(jìn)行改進(jìn),使mark可以和用戶任務(wù)并發(fā)執(zhí)行。這種方法的mark操作是可以漸進(jìn)執(zhí)行的而不需每次都掃描整個(gè)內(nèi)存空間,可以進(jìn)一步減少STW的時(shí)間。
- Go 1.8:混合寫(xiě)屏障(hybrid write barrier)允許堆棧掃描永久地使堆棧變黑(沒(méi)有STW并且沒(méi)有寫(xiě)入堆棧的障礙),這完全消除了堆棧重新掃描的需要,從而消除了對(duì)堆棧屏障的需求。使用這種方法可以將STW的時(shí)間降低到1毫秒以下
如果你的Go程序突然出現(xiàn)卡頓,就可能是GC的原因,就需要花時(shí)間去優(yōu)化,減少Go內(nèi)存的壓力。
在1.8版本以后,GC的延遲性下降了很多,但是因?yàn)樾枰⑿刑幚鞧C,線程間同步和多余的數(shù)據(jù)生成復(fù)制都會(huì)占用實(shí)際邏輯業(yè)務(wù)代碼運(yùn)行的時(shí)間。因此程序的吞吐量會(huì)下降,STW可以提高程序的吞吐量。
Go GC和Java JVM相比,差距還是很大的。但是Go畢竟是一個(gè)比較年輕的語(yǔ)言,給它時(shí)間進(jìn)行發(fā)展,未來(lái)GC一定會(huì)越來(lái)越好。
字段標(biāo)記災(zāi)難
看下面一段代碼,這是來(lái)自Go官方文檔的一個(gè)例子:
type Test struct {
Label *string `protobuf:"bytes,1,req,name=label" json:"label,omitempty"`
Type *int32 `protobuf:"varint,2,opt,name=type,def=77" json:"type,omitempty"`
Reps []int64 `protobuf:"varint,3,rep,name=reps" json:"reps,omitempty"`
Optionalgroup *Test_OptionalGroup `protobuf:"group,4,opt,name=OptionalGroup" json:"optionalgroup,omitempty"`
}
字段標(biāo)簽是Go提供的一個(gè)功能,用于給Go字段進(jìn)行注釋。在使用json的過(guò)程中,字段標(biāo)簽非常重要。
因?yàn)镚o中公共屬性首字母是大寫(xiě)的,而在json標(biāo)準(zhǔn)中字段首字母一般是小寫(xiě)的。而Go的標(biāo)準(zhǔn)庫(kù)json沒(méi)有這方面的自動(dòng)轉(zhuǎn)換,因此需要我們手動(dòng)進(jìn)行標(biāo)注。
如果涉及到更多協(xié)議相關(guān)的內(nèi)容,則標(biāo)簽會(huì)更長(zhǎng)。冗長(zhǎng)的標(biāo)簽會(huì)讓結(jié)構(gòu)體代碼的可讀性下降。
不是總結(jié)的總結(jié)
總體來(lái)說(shuō),Go還是一門(mén)不錯(cuò)的語(yǔ)言的。它寫(xiě)起來(lái)很像Python,卻有著Python遠(yuǎn)不及的性能。Go也有著很低的入門(mén)門(mén)檻。
當(dāng)然,Go最吸引人的地方就是它的協(xié)程了。協(xié)程的概念讓我們?cè)诰幊讨锌梢詭缀蹼S心所欲地創(chuàng)建并發(fā)任務(wù)而不用太多考慮開(kāi)銷。也讓我們的并發(fā)編程變得更加簡(jiǎn)單。
在高并發(fā)領(lǐng)域,Go可以說(shuō)是越來(lái)越受歡迎,很多公司都使用Go來(lái)構(gòu)建他們的基礎(chǔ)服務(wù)器設(shè)施。Go讓C/C++程序員遠(yuǎn)離構(gòu)建的痛苦,專注于開(kāi)發(fā)。
Go當(dāng)然還有很多缺點(diǎn),在業(yè)務(wù)編寫(xiě)上面,為很多人詬病。和Java還有很大的差別,生態(tài)圈也不如Java豐富。Go還有很多不為人知的陷阱(可以詳見(jiàn)我的“Go基礎(chǔ)筆記”系列)。
希望Go在未來(lái)能夠越來(lái)越好。