這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
Go語言簡介
Go語言(golang)是Google推出的一種全新的程式設計語言,可以在不損失應用程式效能的情況下降低代碼的複雜性。Google首席軟體工程師羅布派克(Rob Pike)說:我們之所以開發Go,是因為過去10多年間軟體開發的難度令人沮喪。
Go語言的主要特性:
- 編譯型語言,執行效率接近c/c++
- 自動記憶體回收
- 更豐富的內建類型和自動類型推導(類似c++11的auto)
- 函數可以返回多個值
- 擁有更好的錯誤處理
- 匿名函數和閉包
- 支援類型和介面
- 並發編程
- 支援反射機制
- 語言互動性強
- 工程依賴自動推導
- 打包和部署容易
- ……
就我個人而言,感覺Go語言是對像是對c語言的修補和延伸。這個由一群工程師們設計出的語言實在太符合我的口味了,所有的特性都是為瞭解決實際問題而存在的。但是新增加的特性並沒有給語言帶來額外的負擔,相反,Go的文法反而很簡單。大道至簡,這是我喜歡Go的原因。
Go語言開發環境部署
這個相信大家很容易在網上就能擷取到下載和安裝的教程,就不贅述了。
Go語言基本文法簡介
先來看一個Go語言版的”Hello world”程式:
package main // 聲明本檔案的包名是mainimport "fmt" // 匯入fmt包 func main() { // 入口函數 fmt.Println("Hello, Go語言!") // 列印文字}
使用 go run hello.go 編譯運行後輸出了結果。Go語言裡預設的編碼是UTF-8,所以很自然的支援中文。
第一感覺看上去Go好像是Java或者python和c語言的雜交吧?這裡package和import還真的類似Java或者python中的含義。Go語言沒有繼續背負c語言的標頭檔這個沉重的包袱,而是採用了更好的機制。
main函數的原型就是這樣,沒有參數和傳回值,至於命令列參數使用其他的機制傳遞。有意思的是main()後面的左括弧 { 是不可以另起一行的。這在c語言裡只是個人習慣的問題,而在Go語言裡卻是硬性規定,寫在下面的話直接就是一個編譯器錯誤。
列印語句的這一行結尾是可以沒有分號的,編譯器會自動加上的。正因為支援可以不寫分號的做法,所以Go語言不允許將左括弧另起一行,否則編譯器就不知道這一行結尾是否應該加上分號了。
變數的聲明和初始化
Go語言引入了 var 關鍵字來定義變數,和c語言最大的不同就是變數的類型寫在變數後面。下面是幾個例子:
var i int = 5 // 定義了一個整型變數i=5var str string = "Go語言" // 對,string是內建類型var pInt *int = &i // 沒錯,指標var array [10]int // 有10個元素的數組var ArrayPart []int // 數組切片,很python吧var Info struct { // 結構體,和c語言的沒啥太大區別 name string age int}var NumToName map[int] string // map是內建類型,這裡int是key,string是value
甚至Go語言的發明者們認為多寫幾個var都是不好的:
var ( a int = 1 b int = 2 c string = "簡化的變數定義")
還是略麻煩嗎?那麼你也可以這樣:
i := 10str := "hello"
i會被建立並初始化為10,類型自動推導為int;同理,str會推導為string類型並初始化,這個文法糖不錯吧。
還有更厲害的,c語言中的交換變數是需要一個中間變數的,但是Go語言不需要!只需要像下面這樣:
array[i], array[j] = array[j], array[i]
這樣兩個變數就交換了,簡單吧?
Go語言的內建類型還有很多,下面簡單的列出來一些:
- 布爾類型 bool
- 整型 int8 byte int16 int uint int32 int64 uintptr…
- 浮點類型 float32 float64
- 字元類型 rune
- 字串 string
- 複數類型 complex64 complex128
- 錯誤類型 error
還有一些複合類型:
- 指標 pointer
- 數組 array
- 數組切片 slice
- 字典 map
- 結構體 struct (貌似沒有c語言裡的union了)
- 通道 chan
- 介面 interface
類型不再贅述,請參閱Go語言文檔或者其它資料(Go的文檔中範例程式碼確實太少了…)。
常量定義
和c語言一樣使用 const 關鍵字來定義常量:
const PI float64 = 3.141592653 // 這裡的float64和c語言的double差不多
不過Go語言的常量可以不寫類型,即支援無類型常量。
Go語言預定義了true、false等常量,其含義相信大家懂的。
####流程式控制制
Go語言的流程式控制制和c語言的很相似,其實無非就是選擇和迴圈結構的寫法。
先來看選擇,和c語言的差不多:
if condition { // ...} else { // ...}
唯一需要注意的是左括弧的位置,左括弧另起一行可是編譯不過的哦~Go語言在文法的層面上限制了代碼風格,我表示喜歡這種做法…
和c語言唯一不一樣的恐怕就是if後的條件不用括弧括起來。
然後是switch語句,這個和c語言的區別有點大了:
i := 2switch i { case 1: // ... // 不需要break,Go語言的case不會下穿的。 case 2: // ...} // case還可以是字串:str := "hello"switch str { case "hello": // ... case "world": // ... default: // ...} // 甚至switch都可以取代if-else if 結構了:i := 5switch { case i > 0 && i < 10: fmt.Println("> 0 && < 10") case i > 10: fmt.Println("> 10") default: fmt.Println("= 10")}
那如果想要c語言中的case下穿的功能怎麼辦呢?很簡單,在case後語句裡寫fallthrough就可以了(會跳過下一個case的測試直接執行其代碼)。
接下來是迴圈了,Go語言的迴圈只有for一個關鍵字,沒有c語言的while和do-while了。不過Go語言的for很靈活,有以下幾種寫法:
// 接近c的寫法:for i := 0; i < 5; i++ { fmt.Println("i =" , i) // Go語言列印函數的各種寫法都很好玩哦} // 類似c語言while的寫法:i := 0for i < 5 { fmt.Println("i =" , i) i++} // 死迴圈的寫法,是不是很清爽呢?i := 0for { if i < 5 { fmt.Println("i =" , i) i++ } else { break }}
這是最基本的用法了,本文的定位是簡明教程,range的用法請參考其他資料。
函數定義
因為Go語言的類型要寫在後面,所以函數的定義差異比較大。
定義函數使用func關鍵字:
func Add(a int, b int) int { // 注意傳回值類型寫的位置 return a + b} // 還可以寫成這樣func Add(a, b int) int { // Go的編譯器知道a,b都是int類型 return a + b} // 有意思的是Go語言的函數可以返回多個值(傳回值可以有名字,error是內建的錯誤類型)func Add(a, b int) (ret int, err error) { if a + b > 10000 { // 假設一個錯誤條件,不要在意這個傻逼條件... err = error.New("Too big number...") } return a + b, nil // nil 表示空,有些類似c的NULL...}
函數如何調用呢?因為有包的概念,所以本包內部的函數直接調用就行了(函數定義位置無所謂的,不再是c語言裡必須先聲明再調用了)。
sum, err := Add(10, 20)if err == nil { // ok, sum is right...} // 如果只是想知道有沒有錯,不在乎結果,可以用底線佔位而忽略掉..._, err := Add(a, b)
不在一個包裡的話,先得import包,然後調用函數的時候前面要加包名和小數點,就像調用fmt包裡的Println函數似的。這裡需要注意的是,如果一個函數或者變數對包外可見,那麼其首字母必須大寫(Go語言又在用文法來限制代碼格式了…)。
不定參數、匿名函數和閉包就不介紹了。
錯誤處理
上面已經簡單的示範過錯誤處理的方法了,因為Go語言的多傳回值的支援,錯誤處理代碼可以寫的很漂亮。更進階的錯誤處理超出了本文科普範圍,請自行查閱資料。雖然話是這麼說,但是我還是忍不住想簡單介紹一下defer機制,樣本如下(這個例子來自文檔):
func CopyFile(dstName, srcName string) (written int64, err error) { src, err := os.Open(srcName) if err != nil { return } defer src.Close() dst, err := os.Create(dstName) if err != nil { return } defer dst.Close() return io.Copy(dst, src)}
defer做了什麼呢?簡單的說當離開這個函數的時候,defer後面的操作會被執行。這樣的話在諸如臨界地區加鎖的代碼時,通過defer語句,就不用時時刻刻勞心臨界區返回的時候有沒有忘記寫解鎖操作了,只需要在函數內使用defer寫上解鎖操作就可以了。Note:可以有多個defer語句,feder後的函數以後進先出(LIFO)的順行執行。
物件導向機制
基本的物件導向特性
以c++作為對比,首先在Go語言中沒有隱藏的this指標,沒有c++那種建構函式和虛構函數。
這裡所謂的沒有隱藏this指標意思是”this指標”顯式的進行聲明和傳遞。同時Go語言的結構體和其它語言的類具有同等地位,但是Go語言的物件導向機制放棄了其它所謂的物件導向的語言基本具有的大量特徵,只保留了組合(composition)這個基礎特性。
我不想在這裡就這個問題來討論Go語言物件導向特徵的優劣,因為很容易引發口水仗。不過我還是表明下態度,我支援Go語言目前的做法,物件導向的核心是訊息傳遞,而不是”構造類繼承樹”。
下面簡單的以一個例子來說明Go的結構體以及如何在結構體上定義方法(c++程式員可能更喜歡成員函數這個術語,不過要記得Go中沒有類,只有struct類型):
type Circle struct { r float64} func (c *Circle) Area() float64 { return c.r * c.r * 3.14}
type有些類似於C語言裡typedef的意思。關鍵是下面的函數,相信結合函數名前邊的聲明,你已經理解了Go語言定義結構體的成員函數的文法。這裡我傳入的是Circle的指標類型,參數名叫c(當然,這裡參數名稱隨意)。至於傳入的類型並不一定得是指標類型,也可以傳入實值型別(編譯器自行識別)。
Go語言裡也沒有public和private等關鍵字,而直接由這個函數的首字母決定。大寫字母開始的函數是可以在包外被訪問的(注意是包外不是結構體外,小寫開始的函數在包內的其它函數也可以調用!)。這又是一個Go語言直接用文法格式來表明語義的例子,不知道這麼做是好是壞(起碼項目內部代碼風格統一了…)。
Go語言沒有c++那種建構函式,但也提供了另外一種初始化結構體類型的方法:一個取名為New後面加類型名的全域建立函數。比如上面的Circle這個結構體的”建構函式”如下:
1func NewCircle(r float64) *Circle { return &Circle{r}}
如何定義對象並且調用呢?定義的方法如下:
c1 := new(Circle)c2 := &Circle{}c3 := &Circle{2}c4 := &Circle{r:2}
一直沒有強調過,Go語言中所有的變數都是預設初始化為0的,所以如果對c1、c2、c3、c4調用GetArea函數的話,得到的結果就是0,0,12.56,12.56
上面談到Go語言不支援繼承其實有點問題,Go語言實際上提供了”繼承”,不過是以”組合”的文法來提供的:
type Father struct { Name string} func (fath *Father) PrintClassName() { fmt.Println("Father")} type Child struct { Father Age int}
此時如果Child結構體沒有重寫/覆寫PrintClassName函數的話,定義Child結構體的對象child後執行child.PrintClassName()輸出的就是”Father”,如果添加以下代碼輸出的就是”Chlild”:
func (ch *Child) PrintClassName() { fmt.Println("Child")}
也可以這樣進行組合:
type Child struct { *Father Age int}
這樣做的話,初始化Child結構體的對象時就需要一個外部的Father結構體的對象指標了。
介面
介面這個概念對於熟悉Java的人來說並不陌生,c++中的虛基類和這個概念也類似。不過Go語言這裡的介面是非侵入性的介面,並不需要從這個介面來繼承(別忘了,Go沒有通常意義上的”繼承”)
在Go語言中,只要一個結構體上實現了介面所要求的所有函數,就認為實現了該介面。所以在Go語言中,不存在繼承樹。至於Go採用的非侵入式介面的原因,這超出了本文的科普範圍,請參考Go設計者發表的博文。
下面是一個介面定義(來自):
type IFile interface { Open(filename string) (pfile *File, err error) Close() error Read(buf []byte) (n int, err error) Write(buf []byte) (n int, err error)} type IReader interface { Read(buf []byte) (n int, err error)} type IWriter interface { Write(buf []byte) (n int, err error)}
此時若有一個結構體TextFile實現了Open,Close,Read,Write這四個函數。不需要從這些介面繼承,便可以進行賦值,例如:
var f1 IFile = new(File)var f2 IReader = new(File)var f3 IWriter = new(File)
空介面interface{}即Any類型,可以指向任意的類型。
介面查詢,類型查詢等特性進一步的學習請參閱Go語言文檔。