這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
這一周,利用每天晚上下班回來後的一小時,學習了Google開發的Go語言,算是對其有了個基本的瞭解。確實是門漂亮的語言。
首先,從它的設計目標是設計一種高效的、靜態編譯的、易於編寫的語言。它涉足的是系統級的編程,試圖與C/C++抗衡。
詳細來說,它的設計目標有如下幾點(來自wikipedia和golang FAQ):
- 安全:型別安全與記憶體安全。沒有繼承,無需處理類型的依賴關係,弱化類型的使用;變數預設初始化,簡化設計負擔。
- 並發和通訊的支援。內建的並發機制使得多線程編程變得非常簡單;內建的chan(channel)類型簡化了線程間通訊。
- 完全的記憶體記憶體回收機制。
- 高速編譯。沒有標頭檔、Makefile等複雜的工程依賴關係,使得編譯速度更快,工程更容易組織。
而在我看來,通過一周的學習,給我留下最深印象的,是如下幾個方面:
- 更符合自然語言的文法。類型的聲明放在變數後面,實戰發現確實比放在前面更易讀。
- 方便的內建類型。string、map、數組等,這些複雜的類型都內建於語言中。
- 內建的並發機制。對於多線程程式的編寫支援非常好。
- 沒有類、只有結構和介面。只是很不習慣,目前還沒有發現這樣做的好處。
- 文檔。從基本的文法、包的文檔,到教程、設計建議等。對於理解Go,寫好Go非常有協助。
下面我將以我在教程中寫的一些代碼為例,說說我對上面幾點的理解。
自然的文法
go語言的文法,非常符合英語文法的習慣(英語文法較漢語更具有邏輯性,更能清楚的解釋問題)。
比如
定義一個變數:x int,用英語來讀就是x of type int;
定義一個函數:func add(x, y int) int,讀出來就是:a function named add, with parameter x & y of type int, that returns int。非常自然。
當然,要顯示出這種定義的自然性,我們可以看一個複雜的例子,函數指標:
首先看看C語言的定義方式:
int (*fp)(int (*ff)(int x, int y), int b)
它定義了一個函數指標fp,指向一個以函數指標ff和b為參數,並返回int的函數。其中ff指向一個以x,y為參數,並返回int的函數。x!真複雜
如果用Go呢?
fp func(ff func(x, y int) int, b int) int
是不是剛好與上面的描述符合?
這種符合自然語言文法的定義方式,簡化了代碼理解的步驟,也不容易出錯。
關於此部分更詳細的內容,可以參考Go's Declaration Syntax。
下面給出一個hello world程式,一睹為快:
// 類似Java,用包名來組織代碼package mainimport "fmt"// 程式的“入口”,main函數。func main() {fmt.Println("Hello, 世界")// 沒有return語句}
方便的內建類型
這裡我以tour.golang.org中的一個練習為例子,介紹go語言中的map和string。
這個練習要求:
實現WordCount函數。此函數輸入一句英文語句,並返回一個map類型,儲存每個單詞對應的重複次數。主函數以及寫好,包含一個wc.Test函數,用於測試WordCount函數的正確性。
提示:strings.Fields可能會很有協助。
下面是我的實現:
package mainimport ("tour/wc""strings")func WordCount(s string) map[string]int {// 建立一個鍵為string,值為int的map// make可以用來建立任何類型的變數。// 比如make([]int, 3)是建立3個元素的int數組m := make(map[string]int)// 變數的使用可以不用顯示的指明類型// 這裡,words的類型即Fields的傳回值類型,是個字串數組words := strings.Fields(s)// Go語言沒有while、do-while// for 條件 { 執行體 } 即相當於while// for { 執行體 } 即無限迴圈// 這裡,使用for的range特性,取words的索引和值// 分別給_和word,底線_相當於一個預留位置,不賦值給具體的變數// 同樣,還可以使用:i, _ := range words,表示只需要其索引// 甚至可以使用:_, _ := range words,表示只需要迴圈相應次數即可for _, word := range words {// 根據鍵取map中的值,並修改// Go是記憶體安全的語言,如果m中不存在word鍵// 將會自動建立一個word,並初始化其值為0m[word]++;}return m}func main() {wc.Test(WordCount)}
由這個例子,我們可以瞭解到Go語言的很多特性,比如_, word := range words這樣的多個賦值同時進行(也可用於函數傳回值),比如內建的string、map類型,比如簡化的迴圈體(沒有括弧,去掉while,do-while,支援多種迴圈條件的定義),還有代碼的組織方式等。
結構體和介面
Go語言沒有類的概念,沒有構造、解構函式,更沒有繼承。只有結構體和介面。
下面以Exercise: Images為例,介紹Go語言的結構體和介面的使用:
package mainimport ("image""image/color""tour/pic")// 定義Image類型// 類似的定義還可以這樣:type MyInt int// 相當於typedeftype Image struct{content [][]uint32// 二維數組,儲存圖片內容// 包含每個像素點的RGBA值。width, height int// 圖片寬度和高度}// 自訂的像素點函數,返回給定點的RGBA值func valueOfPointer(x, y int) uint32 {return uint32(0xfffff*x^(0xfffff*y + 0xff))}// 自訂的圖片產生函數,用於使用給定的像素點函數產生一幅圖片func makePic(w, h int, f func(int,int) uint32) *Image {img := new(Image)img.width = wimg.height = h// 此處先申請一個長度為w,類型為[]uint32的數組img.content = make([][]uint32, w)for x := 0; x < w; x++ {// 再為每個數組的元素申請h長度的uint32型數組// 由此而建立出一塊 w x h 的二維數組img.content[x] = make([]uint32, h)// 使用f函數為每個像素賦值for y := 0; y < h; y++ {img.content[x][y] = f(x, y)}}return img}////////////////////////////////////////////// 接下來的幾個函數是介面image.Image的函數實現// Go語言中,無需顯示的申明實現介面// 只需要實現介面的所有函數,即實現了介面// Bounds 函數返回圖片的可用性區域域// 在 func 和函數名之間加上類型,表示此函數是該類型的成員函數// 注意,此處的類型不僅限於結構體,比如浮點數、整數都可以。func (img *Image) Bounds() image.Rectangle {return image.Rect(0, 0, img.width, img.height)}// ColorModel 函數指明圖片使用的顏色模式// 這裡,我們選用RGBA模式func (img *Image) ColorModel() color.Model {return color.RGBAModel}// At 函數,返回指定像素點的顏色屬性func (img *Image) At(x, y int) color.Color {// 根據練習的說明設定超出範圍的點的顏色if x >= img.width || y >= img.height {return color.RGBA{uint8(x), uint8(y), 0xff, 0xff}}// 根據儲存的二維數組,產生RGBA模式的顏色並返回var c = img.content[x][y]return color.RGBA{uint8((c >> 24) & 0xff),uint8((c >> 16) & 0xff),uint8((c >> 8) & 0xff),uint8(c & 0xff) }}func main() {m := makePic(200, 100, valueOfPointer)// 調用pic類的ShowImage來顯示產生的圖片pic.ShowImage(m)}
可能是我還未理解Go語言的精髓,暫時沒有發現這種沒有類、甚至沒有顯示繼承關係的設計有怎樣的優勢。如果有知道的朋友一定要告訴我,非常感謝!
內建的並發機制
Go語言內建了並發機制,無需第三方庫的支援就可以方便的建立線程。並且,Go語言套件含一個chan類型用於線程間的變數傳遞,降低了使用共用記憶體傳遞的風險,有些類似於unix裡的管道。
下面先以一個Equivalent Binary Trees的例子,來介紹並發以及chan的使用,並進一步熟悉Go語言:
package mainimport ("fmt""tour/tree""sort")// Walk 遍曆t,將其所有的內容由ch發送出去// 我使用遞迴的方式實現了它// 注意,Go語言的channel是有類型的func Walk(t *tree.Tree, ch chan int) {if t.Left != nil {Walk(t.Left, ch)}if t.Right != nil {Walk(t.Right, ch)}// 使用 <- 符號將變數值發送到chanch <- t.Value}// Same 決定t1、t2是否是具有相同內容的兩棵樹func Same(t1, t2 *tree.Tree) bool {// 建立兩個具有緩衝的管道// 管理髮送的線程將不停的發送,直至緩衝溢出,// 等到管理接收的線程取出值以後,才能繼續發送// 這相當於一個固定大小的隊列。ch1 := make(chan int, 10)ch2 := make(chan int, 10)// 使用兩個數組來儲存樹的內容a1 := make([]int, 10)a2 := make([]int, 10)// 建立兩個線程,同時開始遍曆go Walk(t1, ch1)go Walk(t2, ch2)// 主線程將不停的接收另外兩個線程傳來的資料for i := 0; i < 10; i++ {a1[i] = <- ch1a2[i] = <- ch2}// 排序以便於檢查其內容是否一致sort.Ints(a1)sort.Ints(a2)for i := 0; i < 10; i++ {if a1[i] != a2[i] {return false}}return true}func main() {// tree.New(k)可以建立內容包含k, 2k, 3k, ..., nk的樹t1 := tree.New(1)t2 := tree.New(2)ch := make(chan int, 10)// 列印t1樹fmt.Print("t1: ")go Walk(t1, ch)for i := 0; i < 10; i++ {fmt.Printf("%2d, ", <- ch)}fmt.Println()// 列印t2樹fmt.Print("t2: ")go Walk(t2, ch)for i := 0; i < 10; i++ {fmt.Printf("%2d, ", <- ch)}fmt.Println()// 測試Same函數fmt.Printf("Is %v that t1 equal t1.\n", Same(t1, t1))fmt.Printf("Is %v that t1 equal t2.\n", Same(t1, t2))}
由例子可以看到管道的使用方式: <- 。發送可以用 channel <- value,接收可以用 variable := <- channel。非常形象。
Go語言中的並發是基於函數的。使用 go function() 即可使此函數在新的線程中運行,父線程將繼續運行,不會等待函數結束。
並發更複雜更靈活的運用,請看練習:Web Crawler。
OK,關於Go的簡單介紹就到這裡,更詳細的文檔請參閱Go語言官方網站:http://golang.org/。
本專題(Go語言學習)所涉及的代碼已經同步到GitHub上,便於分享,下面是連結:
https://github.com/tankery/study-go
註:從今天起,我將以每星期至少一篇的頻率寫這份部落格。
至於每周一篇的頻率,是由我常年加班的工作性質決定的。每周一篇,法定節日休息。。。