為什么要用Go語言?
前言
Go 是一個(gè)開源的編程語言,它能讓構(gòu)造簡單、可靠且高效的軟件變得容易[1]。
Go 語言被設(shè)計(jì)成一門應(yīng)用于搭載 Web 服務(wù)器,存儲(chǔ)集群或類似用途的巨型中央服務(wù)器的系統(tǒng)編程語言。對(duì)于高性能分布式系統(tǒng)領(lǐng)域而言,Go語言無疑比大多數(shù)其它語言有著更高的開發(fā)效率。它提供了海量并行的支持,這對(duì)于游戲服務(wù)端的開發(fā)而言是再好不過了[1]。
其實(shí)早在2018年前,我就已經(jīng)有在國內(nèi)的程序員環(huán)境中斷斷續(xù)續(xù)地聽到Go語言的消息,Go語言提供的方便的并發(fā)編程方式,十分適合我當(dāng)時(shí)選擇的畢業(yè)設(shè)計(jì)選題,但是受限于導(dǎo)師的語言選擇、項(xiàng)目的進(jìn)度追趕、考研的時(shí)間壓榨,一直沒有機(jī)會(huì)來好好地學(xué)習(xí)這門語言。
在進(jìn)入研究生階段后,盡管研究的方向和算法相關(guān),但未來的職業(yè)方向還是選擇了以后端為主,主要是因?yàn)橄胱龈嗪蜆I(yè)務(wù)相關(guān)的工作。為了能在有限的時(shí)間里給予自己足夠深的知識(shí)底蘊(yùn),選擇了一些讓自己去深入了解的方向,Go語言自然也在其中,今天終于有機(jī)會(huì)來開始研究這門語言。
為什么要用Go語言?
撰寫此文的初衷,是本文的標(biāo)題,也是我作為初學(xué)者一直以來的疑問:
“我為什么要用Go語言?”
為了回答這個(gè)問題,我翻閱了很多Go語言相關(guān)的文檔、書籍和教程,我發(fā)現(xiàn)我很難在它們之中找到非常明顯直接的答案,書上和教程只會(huì)說,“是的,Go語言好用”。
對(duì)于部分人來說,這個(gè)問題的答案或許很“明顯”,比如選擇Go語言是因?yàn)镚oogle設(shè)計(jì)的語言、Go開發(fā)賺的錢多、XX公司使用Go語言等等,如果想要了解這門語言更加本質(zhì)的東西,僅僅這些答案我認(rèn)為是還不夠的。
部分Go的教徒可能會(huì)說,他們選擇的理由是和語言本身相關(guān)的,比如:
- Go編譯快
- Go執(zhí)行快
- Go并發(fā)編程方便
- Go有垃圾回收(Garbage Collection, GC)
的確,Go是有這些特點(diǎn),但這并非都是Go獨(dú)有的:
- 運(yùn)行時(shí)解釋的腳本語言(比如Python)幾乎不需要時(shí)間編譯
- C、C++甚至是匯編,基本上能夠榨干一臺(tái)機(jī)器的大部分性能
- 大部分語言都有并發(fā)編程的支持庫
- 大部分語言都不需要程序員主動(dòng)關(guān)注內(nèi)存情況
一些Go的忠實(shí)粉絲把這種All in One的特性作為評(píng)價(jià)語言的標(biāo)準(zhǔn),他們認(rèn)為至少在這些方面,Go是可以完美的代替其他語言的。
那么,Go真的能優(yōu)秀到完全替代另一個(gè)語言么?
其實(shí)未必,我始終認(rèn)為銀彈是不存在的[2],無論是在這次調(diào)查前,還是在這次調(diào)查后。
本文從Go語言被設(shè)計(jì)的初衷出發(fā),深入互聯(lián)網(wǎng)各種角落,調(diào)查Go所具有的那些特性是否足夠優(yōu)秀,同時(shí)和其他語言進(jìn)行適當(dāng)?shù)谋容^,你可以選擇性的閱讀、接受或者反對(duì)我的內(nèi)容,畢竟有交流才能傳播知識(shí)。
我的最終目的是讓更多的初學(xué)者看到Go沒有輕易暴露出的缺點(diǎn),同時(shí)也能看到Go真正優(yōu)秀的地方。
設(shè)計(jì)Go的初衷
Go語言的主要目標(biāo)是將靜態(tài)語言的安全性和高效性與動(dòng)態(tài)語言的易開發(fā)性進(jìn)行有機(jī)結(jié)合,達(dá)到完美平衡,從而使編程變得更加有樂趣,而不是在艱難抉擇中痛苦前行[3]。
Google公司不可能無緣無故地設(shè)計(jì)一個(gè)新語言(一些特性相比于其他語言也沒有新到哪里去),這一切肯定是有原因的。
設(shè)計(jì)Go語言是為了解決當(dāng)時(shí)Google開發(fā)遇到的一些問題[4]:
- C++編譯慢、沒有現(xiàn)代化(入門級(jí)友好的)的內(nèi)存管理
- 數(shù)以萬計(jì)行的代碼,難以維護(hù)
- 部署的平臺(tái)各式各樣,交叉編譯困難
- ......
找不到什么合適的語言,想著反正都是弄來自己用,Google選擇造個(gè)輪子試試。
Go 語言起源 2007 年,并于 2009 年正式對(duì)外發(fā)布。它從 2009 年 9 月 21 日開始作為谷歌公司 20%兼職項(xiàng)目,即相關(guān)員工利用 20% 的空余時(shí)間來參與 Go 語言的研發(fā)工作。該項(xiàng)目的三位領(lǐng)導(dǎo)者均是著名的 IT 工程師:Robert Griesemer,參與開發(fā) Java HotSpot 虛擬機(jī);Rob Pike,Go 語言項(xiàng)目總負(fù)責(zé)人,貝爾實(shí)驗(yàn)室 Unix 團(tuán)隊(duì)成員,參與的項(xiàng)目包括 Plan 9,Inferno 操作系統(tǒng)和 Limbo 編程語言;Ken Thompson,貝爾實(shí)驗(yàn)室 Unix 團(tuán)隊(duì)成員,C 語言、Unix 和 Plan 9 的創(chuàng)始人之一,與 Rob Pike 共同開發(fā)了 UTF-8 字符集規(guī)范。自 2008 年 1 月起,Ken Thompson 就開始研發(fā)一款以 C 語言為目標(biāo)結(jié)果的編譯器來拓展 Go 語言的設(shè)計(jì)思想[3]。
當(dāng)時(shí)Google的很多工程師是用的都是C/C++,所以語法的設(shè)計(jì)上接近于C,Go的設(shè)計(jì)師們想要解決其他語言使用中的缺點(diǎn),但是仍保留他們的優(yōu)點(diǎn)[5]:
- 靜態(tài)類型和運(yùn)行時(shí)效率
- 可讀性和易用性
- 高性能的網(wǎng)絡(luò)和多進(jìn)程
- ...
emmm,這些聽起來還是比較玄乎,畢竟設(shè)計(jì)歸設(shè)計(jì),實(shí)現(xiàn)歸實(shí)現(xiàn),我們回顧一下現(xiàn)在Go的幾個(gè)主要特點(diǎn),編譯速度、執(zhí)行速度、內(nèi)存管理以及并發(fā)編程。
Go的編譯為什么快
當(dāng)然,設(shè)計(jì)Go語言也不是完全從零開始,最初Go的團(tuán)隊(duì)嘗試設(shè)計(jì)實(shí)現(xiàn)一個(gè)Go語言的編譯前端,由基于C的gcc編譯器來編譯成機(jī)器代碼,這個(gè)面向gcc的前端編譯器也就是目前的Go編譯器之一的gccgo。
與其說Go的編譯為什么快,不如先說說C++的編譯為什么慢,C++也可以用gcc編譯,編譯速度的大部分差異很有可能來源于語言設(shè)計(jì)本身。
在討論問題之前,其中需要先說明的一點(diǎn)是:這里比較的編譯速度都是在靜態(tài)編譯下的。
靜態(tài)編譯和動(dòng)態(tài)編譯的區(qū)別:
- 靜態(tài)編譯:編譯器在編譯可執(zhí)行文件時(shí),要把使用到的鏈接庫提取出來,鏈接打包進(jìn)可執(zhí)行文件中,編譯結(jié)果只有一個(gè)可執(zhí)行文件。
- 動(dòng)態(tài)編譯:可執(zhí)行文件需要附帶獨(dú)立的庫文件,不打包庫到可執(zhí)行文件中,減少可執(zhí)行文件體積,在執(zhí)行的時(shí)候再調(diào)用庫即可。
兩種方式有各自的優(yōu)點(diǎn)和缺點(diǎn),前者不需要去管理不同版本庫的兼容性問題,后者可以減少內(nèi)存和存儲(chǔ)的占用(因?yàn)榭梢宰尣煌绦蚬蚕硗粋€(gè)庫),兩種方式孰優(yōu)孰弱,要對(duì)應(yīng)到具體的工程問題上,Go默認(rèn)的編譯方式是靜態(tài)編譯。
回到我們要討論的問題:C++的編譯為什么慢?
C++編譯慢的主要兩個(gè)大頭原因[6]:
- 頭文件的include方式
- 模板的編譯
C++使用include方式引用頭文件,會(huì)讓需要編譯的代碼有乘數(shù)級(jí)的增加,例如當(dāng)同一個(gè)頭文件被同一個(gè)項(xiàng)目下的N個(gè)文件include時(shí),編譯器會(huì)將頭文件引入到每一份代碼中,所以同一個(gè)頭文件會(huì)被編譯N次(這在大多數(shù)時(shí)候都是不必要的);C++使用的模板是為了支持泛型編程,在編寫對(duì)不同類型的泛型函數(shù)時(shí),可以提供很大的便利,但是這對(duì)于編譯器來說,會(huì)增加非常多不必要的編譯負(fù)擔(dān)。
當(dāng)然C++對(duì)這兩個(gè)問題有很多后續(xù)的優(yōu)化方法,但是這對(duì)于很多開發(fā)者來說,他們不想在這上面有過多時(shí)間和精力開銷。
大部分后來的編程語言在引入文件的方式上,使用了import module來代替include 頭文件的方式,import解決了重復(fù)編譯的問題,當(dāng)然Go也是使用的import方式;在模板的編譯問題上,由于Go在設(shè)計(jì)理念上遵循從簡入手,所以沒有將泛函編程納入到設(shè)計(jì)框架中,所以天生的沒有模版編譯帶來的時(shí)間開銷(沒有泛型支持也是很多人不滿Go語言的理由)。
在Go 的1.5 版本中,Go團(tuán)隊(duì)使用Go語言來編寫Go語言的編譯器(也叫自舉),相比于gccgo來說:
- 提高了編譯速度,但執(zhí)行速度略有下降(性能細(xì)節(jié)優(yōu)化還不如gcc)
- 增加了可編譯的平臺(tái)類型(以往受限于gcc)
在此之外,Go語言語法中的關(guān)鍵字也是非常少的(Go1.11版本里只有25個(gè))[7],這也可以減少編譯器花費(fèi)在語法解析上的時(shí)間開銷。
所以在我看來,Go編譯速度快,主要出于四個(gè)原因:
- 使用了import的引用管理方式;
- 沒有模板的編譯負(fù)擔(dān);
- 1.5版本后的自舉編譯器優(yōu)化;
- 更少的關(guān)鍵字。
所以為了加快編譯速度、放棄C++而轉(zhuǎn)入Go的同時(shí),也要考慮一下是否要放棄泛型編程的優(yōu)點(diǎn)。
注:泛型可能在Go 2版本獲得支持。
Go的實(shí)際性能如何
Go的執(zhí)行速度,可以參考一個(gè)語言性能測(cè)試數(shù)據(jù)網(wǎng)站 —— The Computer Language Benchmarks Game[8]。
這個(gè)網(wǎng)站在不同的算法上對(duì)每個(gè)語言進(jìn)行測(cè)試,然后給出時(shí)間和內(nèi)存上的開銷數(shù)據(jù)比對(duì)。
比較的語言有C++、Java、Python。
首先是時(shí)間開銷:
注意:時(shí)間開銷的單位是s,并且Y軸為了方便進(jìn)行不同跨度上的比較,所以選取的是對(duì)數(shù)軸(即非線性軸,為1-10-100-1000的比較跨度)。
然后是內(nèi)存開銷:
注意:Y軸為了方便進(jìn)行不同跨度上的比較,所以選取的是對(duì)數(shù)軸(即非線性軸,為1000-10000-100000-1000000的比較跨度)。
需要注意的是,語言本身的性能只決定了一個(gè)程序的最高理論性能,程序具體的性能還要取決于這個(gè)程序的實(shí)現(xiàn)方法,所以當(dāng)各個(gè)語言的性能并沒有太大的差異時(shí),性能往往只取決于程序?qū)崿F(xiàn)的方式。
通過兩個(gè)圖的數(shù)據(jù)可以分析:
- Go雖然還無法達(dá)到C++那樣的極致性能,但是在大部分情況下已經(jīng)很接近了;
- Go和Java在算法的時(shí)間開銷上難分伯仲,但在內(nèi)存的開銷上Java就要高得多了;
- Go在上述的絕大部分情況下,至少時(shí)間和內(nèi)存開銷都比Python要優(yōu)秀得多;
Go的并發(fā)編程
Go的并發(fā)之所以比較受歡迎,網(wǎng)絡(luò)上的很多內(nèi)容集中在幾個(gè)方面:
- 天生并發(fā)的設(shè)計(jì)
- 輕量化的并發(fā)編程方式
- 較高的并發(fā)性能
- 輕量級(jí)線程Goroutines、并發(fā)通信Channels以及其他便捷的并發(fā)同步控制工具
由于Go在設(shè)計(jì)的時(shí)候就考慮到了并發(fā)的支持,或者說很多特性都是為了并發(fā)而設(shè)計(jì),這和一些后期庫支持并發(fā)和第三方庫支持并發(fā)的語言不同。
所以Go的并發(fā)到底有多方便?在Go中使用并發(fā),只需要在普通的函數(shù)執(zhí)行前加上一個(gè)go關(guān)鍵字,就可以新建一個(gè)線程讓函數(shù)在其中執(zhí)行:
- func main() {
- go loop() // 啟動(dòng)一個(gè)goroutine
- loop()
- }
這樣帶來的好處不僅僅是讓并發(fā)編程更方便了,在一些特定情況下,比如Go引用一些使用了并發(fā)的庫時(shí),這些庫所使用的并發(fā)也是基于Go本身的并發(fā)設(shè)計(jì),不會(huì)存在庫使用另一套并發(fā)實(shí)現(xiàn)的情況,這樣Go調(diào)度器在處理程序中的各種并發(fā)線程時(shí),可以有更加統(tǒng)一化的管理方式。
不過Go的并發(fā)對(duì)于程序的實(shí)現(xiàn)要求還是比較高的,在使用一些通信Channel的場(chǎng)合,稍有疏忽就可能出現(xiàn)死鎖的問題,比如:
- fatal error: all goroutines are asleep - deadlock!
Go的并發(fā)量可以比大部分語言里普通的線程實(shí)現(xiàn)要高,這受益于輕量級(jí)的Goroutine,輕量化主要是它所占用的空間要小得多,例如64位環(huán)境下的JVM,它會(huì)默認(rèn)固定為每個(gè)線程分配1MB的線程??臻g,而Goroutines大概只有4-8KB,之后再按需分配。足夠輕量化的線程在相同的內(nèi)存下也就可以有更高并發(fā)量(服務(wù)器CPU還沒有飽和的情況下),同時(shí)也可以減少很多上下文切換的時(shí)間開銷[9]。但是如果你的每個(gè)線程占用空間都非常大時(shí)(比如10MB,當(dāng)然這是非常規(guī)需求的情況下),Go的輕量化優(yōu)勢(shì)就沒有那么明顯了。
Go在并發(fā)上的優(yōu)點(diǎn)很明顯,也是Go的功能目標(biāo),從語言設(shè)計(jì)上支持了并發(fā),提供了統(tǒng)一便捷的工具,復(fù)雜的并發(fā)業(yè)務(wù)也需要在Go的一整套并發(fā)規(guī)范體系下進(jìn)行編程,當(dāng)然這肯定會(huì)犧牲部分實(shí)現(xiàn)自由度,但可以獲得性能的提高和維護(hù)成本的下降。
PS:關(guān)于Go調(diào)度器的內(nèi)容在這里并沒有被提及,因?yàn)楹茈y用簡單的文字向讀者說明該調(diào)度方式和其他調(diào)度方式的優(yōu)劣,將在未來的某一篇中會(huì)細(xì)致地介紹Go調(diào)度器的內(nèi)容。
Go的垃圾回收
垃圾回收(英語:Garbage Collection,縮寫為GC),在計(jì)算機(jī)科學(xué)中是一種自動(dòng)的存儲(chǔ)器管理機(jī)制。當(dāng)一個(gè)計(jì)算機(jī)上的動(dòng)態(tài)存儲(chǔ)器不再需要時(shí),就應(yīng)該予以釋放,以讓出存儲(chǔ)器,這種存儲(chǔ)器資源管理,稱為垃圾回收。垃圾回收器可以讓程序員減輕許多負(fù)擔(dān),也減少程序員犯錯(cuò)的機(jī)會(huì)[10]。
在使用Go或者其他支持GC的語言時(shí),不用再像C++一樣,手動(dòng)地去釋放不需要的變量占用的內(nèi)容空間(free/delete)。
的確,這很方便(對(duì)于懶人和容易忘記主動(dòng)釋放的人),但是也多了一些限制(暗箱操作的不透明性以及在GC處理上的性能開銷)。GC也不是萬能的,當(dāng)遇到一些對(duì)性能要求較高的場(chǎng)景,還是需要記得進(jìn)行一些主動(dòng)釋放或優(yōu)化操作(比如說自定義內(nèi)存池)。
PS:將在未來的某一篇中會(huì)細(xì)致地介紹Go垃圾回收的細(xì)節(jié)(如果你們也覺得有必要的話)。
什么時(shí)候可以選擇Go?
Go有很多優(yōu)點(diǎn),編譯快、性能好、天生并發(fā)以及垃圾回收,很多比較有特色的內(nèi)容也還沒有說到(比如gofmt)。
Go語言也有很多缺點(diǎn),比如第三方庫支持還不夠多(相比于Python來說就少的太多了)、支持編譯的平臺(tái)還不夠廣、還有被稱為噩夢(mèng)的依賴版本管理(已經(jīng)在改善了,但是還沒有達(dá)到完全可靠的程度)。
所以到底Go適合做什么,不適合做什么?
分析了這么多后,這個(gè)問題其實(shí)很難回答,但我們可以選擇先從不適合的領(lǐng)域把Go剔除掉,看看我們會(huì)剩下什么。
Go不適合做什么
- 極致高性能優(yōu)化的場(chǎng)景,你可能需要使用C/C++,甚至是匯編;
- 簡單流程的腳本工具、數(shù)值分析、深度學(xué)習(xí),可能Python更適合(至少目前是);
- 搭一個(gè)博客或網(wǎng)站,PHP何嘗不是天下第一的語言呢;
- 如果你想比較方便找到一份的后端工作,絕大部分公司的Java崗一直缺人(在實(shí)際生產(chǎn)過程中,目前Go仍沒有比Java表現(xiàn)得好太多,至少?zèng)]有好到讓一個(gè)部門/公司將核心業(yè)務(wù)重新轉(zhuǎn)向Go來進(jìn)行重構(gòu));
- ...
你可以找到類似上面那樣的很多場(chǎng)景,你可能會(huì)發(fā)現(xiàn)Go并不能那么完美地替代掉誰。
Go適合做什么
最后,到了我們的終極問題,Go到底適合做什么?
讀到這里你可能會(huì)覺得,好像是我把Go的特性吹了一遍,然后突然告訴你可能Go不適合你。
Go天生并發(fā),面向并發(fā),所以Go的定位一直很清楚,從最淺顯的視角來看,至少Go作為一個(gè)有較高性能的并發(fā)后端來說,是具有非常大的誘惑力的。
尤其對(duì)于后端相關(guān)的程序員而言,在某些業(yè)務(wù)功能的初步實(shí)現(xiàn)上,簡潔的語法、內(nèi)置的并發(fā)、快速的編譯,都可以讓你更加高效快速地完成任務(wù)(前提是Go的內(nèi)容足以完成你的任務(wù)),不用再去擔(dān)憂編譯優(yōu)化和內(nèi)存回收、不用擔(dān)心過多的時(shí)間和內(nèi)存開銷、不用擔(dān)心不同版本庫之間的沖突(靜態(tài)編譯)以及不用擔(dān)心交叉編譯平臺(tái)適配問題。
大部分情況下,編寫一個(gè)服務(wù),你只需要:實(shí)現(xiàn)、編譯、部署、運(yùn)行。
高效快速,足夠敏捷,這在企業(yè)的絕大部分項(xiàng)目的初期都是適用的,這也是大部分項(xiàng)目對(duì)開發(fā)初期的要求。當(dāng)一個(gè)項(xiàng)目或者服務(wù)真的可以發(fā)展下去,需求的確觸碰到Go的天花板時(shí),再考慮使用更加好的語言或方法去優(yōu)化也為時(shí)不晚。
簡而言之,盡管Go的過于簡潔帶來了很多問題(有些人說的難聽點(diǎn)叫過于簡單),Go所具有的優(yōu)點(diǎn),可以讓大部分人用編程語言這種工具,來解決對(duì)他們而言更加重要的問題。
Go語言不是銀彈,但它的確能有效地解決這些問題。