兄弟連golang神技(1)-關於 Go 語言的介紹

尹成發表於2018-07-03
計算機一直在演化,但是程式語言並沒有以同樣的速度演化。現在的手機,內建的 CPU 核
數可能都多於我們使用的第一臺電腦。高效能伺服器擁有 64 核、128 核,甚至更多核。但是我
們依舊在使用為單核設計的技術在程式設計。
程式設計的技術同樣在演化。大部分程式不再由單個開發者來完成,而是由處於不同時區、不同
時間段工作的一組人來完成。大專案被分解為小專案,指派給不同的程式設計師,程式設計師開發完成後,
再以可以在各個應用程式中交叉使用的庫或者包的形式,提交給整個團隊。
如今的程式設計師和公司比以往更加信任開源軟體的力量。Go 語言是一種讓程式碼分享更容易的編
程語言。Go 語言自帶一些工具,讓使用別人寫的包更容易,並且 Go 語言也讓分享自己寫的包
更容易。
在本章中讀者會看到 Go 語言區別於其他程式語言的地方。Go 語言對傳統的物件導向開發
進行了重新思考,並且提供了更高效的複用程式碼的手段。Go 語言還讓使用者能更高效地利用昂貴
伺服器上的所有核心,而且它編譯大型專案的速度也很快。
在閱讀本章時,讀者會對影響 Go 語言形態的很多決定有一些認識,從它的併發模型到快如
閃電的編譯器。我們在前言中提到過,這裡再強調一次:這本書是寫給已經有一定其他程式語言
經驗、想學習 Go 語言的中級開發者的。本書會提供一個專注、全面且符合習慣的視角。我們同
時專注語言的規範和實現,涉及的內容包括語法、Go 語言的型別系統、併發、通道、測試以及
其他一些非常廣泛的主題。我們相信,對剛開始要學習 Go 語言和想要深入瞭解語言內部實現的
人來說,本書都是最佳選擇。
本書示例中的原始碼可以在 https://github.com/goinaction/code 下載。
我們希望讀者能認識到,Go 語言附帶的工具可以讓開發人員的生活變得更簡單。最後,讀
者會意識到為什麼那麼多開發人員用 Go 語言來構建自己的新專案。
1
第 1 章 關於 Go 語言的介紹
1.1 用 Go 解決現代程式設計難題
Go 語言開發團隊花了很長時間來解決當今軟體開發人員面對的問題。開發人員在為專案選
擇語言時,不得不在快速開發和效能之間做出選擇。C 和 C++這類語言提供了很快的執行速度,
而 Ruby 和 Python 這類語言則擅長快速開發。Go 語言在這兩者間架起了橋樑,不僅提供了高性

能的語言,同時也讓開發更快速。


在探索 Go 語言的過程中,讀者會看到精心設計的特性以及簡潔的語法。作為一門語言,Go
不僅定義了能做什麼,還定義了不能做什麼。Go 語言的語法簡潔到只有幾個關鍵字,便於記憶。
Go 語言的編譯器速度非常快,有時甚至會讓人感覺不到在編譯。所以,Go 開發者能顯著減少等
待專案構建的時間。因為 Go 語言內建併發機制,所以不用被迫使用特定的執行緒庫,就能讓軟體
擴充套件,使用更多的資源。Go 語言的型別系統簡單且高效,不需要為物件導向開發付出額外的心
智,讓開發者能專注於程式碼複用。Go 語言還自帶垃圾回收器,不需要使用者自己管理記憶體。讓我
們快速瀏覽一下這些關鍵特性。
1.1.1 開發速度
編譯一個大型的 C 或者 C++專案所花費的時間甚至比去喝杯咖啡的時間還長。圖 1-1 是 XKCD
中的一幅漫畫,描述了在辦公室裡開小差的經典借口。
圖 1-1 努力工作?(來自 XKCD)
Go 語言使用了更加智慧的編譯器,並簡化了解決依賴的演算法,最終提供了更快的編譯速度。
編譯 Go 程式時,編譯器只會關注那些直接被引用的庫,而不是像 Java、C 和 C++那樣,要遍歷
依賴鏈中所有依賴的庫。因此,很多 Go 程式可以在 1 秒內編譯完。在現代硬體上,編譯整個 Go
語言的原始碼樹只需要 20 秒。
因為沒有從編譯程式碼到執行程式碼的中間過程,用動態語言編寫應用程式可以快速看到輸出。
代價是,動態語言不提供靜態語言提供的型別安全特性,不得不經常用大量的測試套件來避免在
執行的時候出現型別錯誤這類 bug。
想象一下,使用類似 JavaScript 這種動態語言開發一個大型應用程式,有一個函式期望接收
一個叫作 ID 的欄位。這個引數應該是整數,是字串,還是一個 UUID?要想知道答案,只能
去看原始碼。可以嘗試使用一個數字或者字串來執行這個函式,看看會發生什麼。在 Go 語言
裡,完全不用為這件事情操心,因為編譯器就能幫使用者捕獲這種型別錯誤。
1.1.2 併發
作為程式設計師,要開發出能充分利用硬體資源的應用程式是一件很難的事情。現代計算機都擁
有多個核,但是大部分程式語言都沒有有效的工具讓程式可以輕易利用這些資源。這些語言需要
寫大量的執行緒同步程式碼來利用多個核,很容易導致錯誤。
Go 語言對併發的支援是這門語言最重要的特性之一。goroutine 很像執行緒,但是它佔用的
記憶體遠少於執行緒,使用它需要的程式碼更少。通道(channel)是一種內建的資料結構,可以讓
使用者在不同的 goroutine 之間同步傳送具有型別的訊息。這讓程式設計模型更傾向於在 goroutine
之間傳送訊息,而不是讓多個 goroutine 爭奪同一個資料的使用權。讓我們看看這些特性的

細節。


1.goroutine
goroutine 是可以與其他 goroutine 並行執行的函式,同時也會與主程式(程式的入口)並行
執行。在其他程式語言中,你需要用執行緒來完成同樣的事情,而在 Go 語言中會使用同一個執行緒
來執行多個 goroutine。例如,使用者在寫一個 Web 伺服器,希望同時處理不同的 Web 請求,如果
使用 C 或者 Java,不得不寫大量的額外程式碼來使用執行緒。在 Go 語言中,net/http 庫直接使用了
內建的 goroutine。每個接收到的請求都自動在其自己的 goroutine 裡處理。goroutine 使用的記憶體
比執行緒更少,Go 語言執行時會自動在配置的一組邏輯處理器上排程執行 goroutine。每個邏輯處
理器繫結到一個作業系統執行緒上(見圖 1-2)。這讓使用者的應用程式執行效率更高,而開發工作量
顯著減少。
如果想在執行一段程式碼的同時,並行去做另外一些事情,goroutine 是很好的選擇。下面是一
個簡單的例子:
func log(msg string) {
...這裡是一些記錄日誌的程式碼
}
// 程式碼裡有些地方檢測到了錯誤
go log("發生了可怕的事情")
圖 1-2 在單一系統執行緒上執行多個 goroutine
關鍵字 go 是唯一需要去編寫的程式碼,排程 log 函式作為獨立的 goroutine 去執行,以便與
其他 goroutine 並行執行。這意味著應用程式的其餘部分會與記錄日誌並行執行,通常這種並行
能讓終端使用者覺得效能更好。就像之前說的,goroutine 佔用的資源更少,所以常常能啟動成千上
萬個 goroutine。我們會在第 6 章更加深入地探討 goroutine 和併發。
2.通道
通道是一種資料結構,可以讓 goroutine 之間進行安全的資料通訊。通道可以幫使用者避免其
他語言裡常見的共享記憶體訪問的問題。
併發的最難的部分就是要確保其他併發執行的程式、執行緒或 goroutine 不會意外修改使用者的
資料。當不同的執行緒在沒有同步保護的情況下修改同一個資料時,總會發生災難。在其他語言中,
如果使用全域性變數或者共享記憶體,必須使用複雜的鎖規則來防止對同一個變數的不同步修改。
為了解決這個問題,通道提供了一種新模式,從而保證併發修改時的資料安全。通道這一模
式保證同一時刻只會有一個 goroutine 修改資料。通道用於在幾個執行的 goroutine 之間傳送資料。
在圖 1-3 中可以看到資料是如何流動的示例。想象一個應用程式,有多個程式需要順序讀取或者
修改某個資料,使用 goroutine 和通道,可以為這個過程建立安全的模型。
圖 1-3 使用通道在 goroutine 之間安全地傳送資料
圖 1-3 中有 3 個 goroutine,還有 2 個不帶快取的通道。第一個 goroutine 通過通道把數
據傳給已經在等待的第二個 goroutine。在兩個 goroutine 間傳輸資料是同步的,一旦傳輸完
成,兩個 goroutine 都會知道資料已經完成傳輸。當第二個 goroutine 利用這個資料完成其任
務後,將這個資料傳給第三個正在等待的 goroutine。這次傳輸依舊是同步的,兩個 goroutine
都會確認資料傳輸完成。這種在 goroutine 之間安全傳輸資料的方法不需要任何鎖或者同步
機制。
需要強調的是,通道並不提供跨 goroutine 的資料訪問保護機制。如果通過通道傳輸資料的
一份副本,那麼每個 goroutine 都持有一份副本,各自對自己的副本做修改是安全的。當傳輸的
是指向資料的指標時,如果讀和寫是由不同的 goroutine 完成的,每個 goroutine 依舊需要額外的
同步動作。
1.1.3 Go 語言的型別系統
Go 語言提供了靈活的、無繼承的型別系統,無需降低執行效能就能最大程度上覆用程式碼。
這個型別系統依然支援物件導向開發,但避免了傳統物件導向的問題。如果你曾經在複雜的 Java
和 C++程式上花數週時間考慮如何抽象類和介面,你就能意識到 Go 語言的型別系統有多麼簡單。
Go 開發者使用組合(composition)設計模式,只需簡單地將一個型別嵌入到另一個型別,就能
複用所有的功能。其他語言也能使用組合,但是不得不和繼承綁在一起使用,結果使整個用法非
常複雜,很難使用。在 Go 語言中,一個型別由其他更微小的型別組合而成,避免了傳統的基於
繼承的模型。
另外,Go 語言還具有獨特的介面實現機制,允許使用者對行為進行建模,而不是對型別進行
建模。在 Go 語言中,不需要宣告某個型別實現了某個介面,編譯器會判斷一個型別的例項是否
符合正在使用的介面。Go 標準庫裡的很多介面都非常簡單,只開放幾個函式。從實踐上講,尤
其對那些使用類似 Java 的面嚮物件語言的人來說,需要一些時間才能習慣這個特性。
1.型別簡單
Go 語言不僅有類似 int 和 string 這樣的內建型別,還支援使用者定義的型別。在 Go 語言
中,使用者定義的型別通常包含一組帶型別的欄位,用於儲存資料。Go 語言的使用者定義的型別看
起來和 C 語言的結構很像,用起來也很相似。不過 Go 語言的型別可以宣告操作該型別資料的方
法。傳統語言使用繼承來擴充套件結構——Client 繼承自 User,User 繼承自 Entity,Go 語言與此不同,
Go 開發者構建更小的型別——Customer 和 Admin,然後把這些小型別組合成更大的型別。圖 1-4
展示了繼承和組合之間的不同。
2.Go 介面對一組行為建模
介面用於描述型別的行為。如果一個型別的例項實現了一個介面,意味著這個例項可以執行
6
第 1 章 關於 Go 語言的介紹
一組特定的行為。你甚至不需要去宣告這個例項實現某個介面,只需要實現這組行為就好。其他
的語言把這個特性叫作鴨子型別——如果它叫起來像鴨子,那它就可能是隻鴨子。Go 語言的接
口也是這麼做的。在 Go 語言中,如果一個型別實現了一個介面的所有方法,那麼這個型別的實
例就可以儲存在這個介面型別的例項中,不需要額外宣告。
圖 1-4 繼承和組合的對比
在類似 Java 這種嚴格的面嚮物件語言中,所有的設計都圍繞介面展開。在編碼前,使用者經
常不得不思考一個龐大的繼承鏈。下面是一個 Java 介面的例子:
interface User {
public void login();
public void logout();
}


在 Java 中要實現這個介面,要求使用者的類必須滿足 User 介面裡的所有約束,並且顯式聲
明這個類實現了這個介面。而 Go 語言的介面一般只會描述一個單一的動作。在 Go 語言中,最
常使用的介面之一是 io.Reader。這個介面提供了一個簡單的方法,用來宣告一個型別有資料
可以讀取。標準庫內的其他函式都能理解這個介面。這個介面的定義如下:
type Reader interface {
Read(p []byte) (n int, err error)
}


為了實現 io.Reader 這個介面,你只需要實現一個 Read 方法,這個方法接受一個 byte
切片,返回一個整數和可能出現的錯誤。
這和傳統的物件導向程式語言的介面系統有本質的區別。Go 語言的介面更小,只傾向於
定義一個單一的動作。實際使用中,這更有利於使用組合來複用程式碼。使用者幾乎可以給所有包
含資料的型別實現 io.Reader 介面,然後把這個型別的例項傳給任意一個知道如何讀取
io.Reader 的 Go 函式。
Go 語言的整個網路庫都使用了 io.Reader 介面,這樣可以將程式的功能和不同網路的
實現分離。這樣的介面用起來有趣、優雅且自由。檔案、緩衝區、套接字以及其他的資料來源
都實現了 io.Reader 介面。使用同一個介面,可以高效地運算元據,而不用考慮到底資料

來自哪裡。



1.1.4 記憶體管理


不當的記憶體管理會導致程式崩潰或者記憶體洩漏,甚至讓整個作業系統崩潰。Go 語言擁有現
代化的垃圾回收機制,能幫你解決這個難題。在其他系統語言(如 C 或者 C++)中,使用記憶體
前要先分配這段記憶體,而且使用完畢後要將其釋放掉。哪怕只做錯了一件事,都可能導致程式崩
潰或者記憶體洩漏。可惜,追蹤記憶體是否還被使用本身就是十分艱難的事情,而要想支援多執行緒和
高併發,更是讓這件事難上加難。雖然 Go 語言的垃圾回收會有一些額外的開銷,但是程式設計時,
能顯著降低開發難度。Go 語言把無趣的記憶體管理交給專業的編譯器去做,而讓程式設計師專注於更

有趣的事情。



尹成老師

QQ77025077 

微信18510341407

所有視訊在尹成學院

www.yinchengxueyuan.com

尹成百度雲請聯絡QQ475318423



相關文章