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































