Go 語言到底適合幹什麼?

Go語言開發團隊花了很長時間來解決當今軟件開發人員面對的問題。開發人員在為項目選擇語言時,不得不在快速開發和性能之間做出選擇。C和C++這類語言提供了很快的執行速度,而Ruby和Python這類語言則擅長快速開發。Go語言在這兩者間架起了橋樑,不僅提供了高性能的語言,同時也讓開發更快速。

在探索Go語言的過程中,讀者會看到精心設計的特性以及簡潔的語法。作為一門語言,Go不僅定義了能做什麼,還定義了不能做什麼。Go語言的語法簡潔到只有幾個關鍵字,便於記憶。Go語言的編譯器速度非常快,有時甚至會讓人感覺不到在編譯。所以,Go開發者能顯著減少等待項目構建的時間。因為Go語言內置併發機制,所以不用被迫使用特定的線程庫,就能讓軟件擴展,使用更多的資源。Go語言的類型系統簡單且高效,不需要為面向對象開發付出額外的心智,讓開發者能專注於代碼複用。Go語言還自帶垃圾回收器,不需要用戶自己管理內存。讓我們快速瀏覽一下這些關鍵特性。

1.1.1 開發速度

編譯一個大型的C或者C++項目所花費的時間甚至比去喝杯咖啡的時間還長。圖1-1是XKCD中的一幅漫畫,描述了在辦公室裡開小差的經典借口。

Go 語言到底適合幹什麼?

圖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)。這讓用戶的應用程序執行效率更高,而開發工作量顯著減少。

Go 語言到底適合幹什麼?

圖1-2 在單一系統線程上執行多個goroutine

如果想在執行一段代碼的同時,並行去做另外一些事情,goroutine是很好的選擇。下面是一個簡單的例子:

<code>

func

log

(msg

string

)

{ ...這裡是一些記錄日誌的代碼 }

go

log(

"發生了可怕的事情"

)/<code>

關鍵字go是唯一需要去編寫的代碼,調度log函數作為獨立的goroutine去運行,以便與其他goroutine並行執行。這意味著應用程序的其餘部分會與記錄日誌並行執行,通常這種並行能讓最終用戶覺得性能更好。就像之前說的,goroutine佔用的資源更少,所以常常能啟動成千上萬個goroutine。我們會在第6章更加深入地探討goroutine和併發。

2.通道

通道是一種數據結構,可以讓goroutine之間進行安全的數據通信。通道可以幫用戶避免其他語言裡常見的共享內存訪問的問題。

併發的最難的部分就是要確保其他併發運行的進程、線程或goroutine不會意外修改用戶的數據。當不同的線程在沒有同步保護的情況下修改同一個數據時,總會發生災難。在其他語言中,如果使用全局變量或者共享內存,必須使用複雜的鎖規則來防止對同一個變量的不同步修改。

為了解決這個問題,通道提供了一種新模式,從而保證併發修改時的數據安全。通道a

Go 語言到底適合幹什麼?

圖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展示了繼承和組合之間的不同。

Go 語言到底適合幹什麼?

圖1-4 繼承和組合的對比

2.Go接口對一組行為建模

接口用於描述類型的行為。如果一個類型的實例實現了一個接口,意味著這個實例可以執行一組特定的行為。你甚至不需要去聲明這個實例實現某個接口,只需要實現這組行為就好。其他的語言把這個特性叫作鴨子類型——如果它叫起來像鴨子,那它就可能是隻鴨子。Go語言的接口也是這麼做的。在Go語言中,如果一個類型實現了一個接口的所有方法,那麼這個類型的實例就可以存儲在這個接口類型的實例中,不需要額外聲明。

在類似Java這種嚴格的面嚮對象語言中,所有的設計都圍繞接口展開。在編碼前,用戶經常不得不思考一個龐大的繼承鏈。下面是一個Java接口的例子:

<code>

interface

User

{

public

void

login

(

)

;

public

void

logout

(

)

; }/<code>

在Java中要實現這個接口,要求用戶的類必須滿足User接口裡的所有約束,並且顯式聲明這個類實現了這個接口。而Go語言的接口一般只會描述一個單一的動作。在Go語言中,最常使用的接口之一是io.Reader。這個接口提供了一個簡單的方法,用來聲明一個類型有數據可以讀取。標準庫內的其他函數都能理解這個接口。這個接口的定義如下:

<code>type Reader 

interface

{ Read(p []

byte

) (n

int

, err error) }/<code>

為了實現io.Reader這個接口,你只需要實現一個Read方法,這個方法接受一個byte切片,返回一個整數和可能出現的錯誤。

這和傳統的面向對象編程語言的接口系統有本質的區別。Go語言的接口更小,只傾向於定義一個單一的動作。實際使用中,這更有利於使用組合來複用代碼。用戶幾乎可以給所有包含數據的類型實現io.Reader接口,然後把這個類型的實例傳給任意一個知道如何讀取io.Reader的Go函數。

Go語言的整個網絡庫都使用了io.Reader接口,這樣可以將程序的功能和不同網絡的實現分離。這樣的接口用起來有趣、優雅且自由。文件、緩衝區、套接字以及其他的數據源都實現了io.Reader接口。使用同一個接口,可以高效地操作數據,而不用考慮到底數據來自哪裡。

1.1.4 內存管理

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

本文摘自《Go語言實戰》

Go 語言到底適合幹什麼?

Go 語言結合了底層系統語言的能力以及現代語言的高級特性,旨在降低構建簡單、可靠、高效軟件的門檻。本書向讀者提供一個專注、全面且符合語言習慣的視角。本書同時關注語言的規範和實現,涉及的內容包括語法、類型系統、併發、管道、測試,以及其他一些主題。

本書是寫給有其他編程語言基礎且有一定開發經驗的、想學Go語言的中級開發者的。對於剛開始要學習Go語言和想要深入瞭解Go語言內部實現的人來說,本書都是最佳的選擇。


分享到:


相關文章: