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