這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
一個線上的Go編譯器
如果還沒來得及安裝Go環境,想體驗一下Go語言,可以在Go線上編譯器 上運行Go程式。
格式化
讓所有人都遵循一樣的編碼風格是一種理想,現在Go語言通過gofmt程式,讓機器來處理大部分的格式化問題。gofmt程式是go標準庫提供的一段程式,可以嘗試運行它,它會按照標準風格縮排,對齊,保留注釋,它預設使用定位字元進行縮排。Go標準庫的所有代碼都經過gofmt程式格式化的。
注釋
Go注釋支援C風格的塊注釋/* */和C++風格的行注釋//。塊注釋主要用作包的注釋。Go官方提倡每個包都應包含一段包注釋,即放置在包子句前的一個塊注釋。對於有多個檔案的包,包注釋只需要出現在其中一個檔案即可。
godoc 既是一個程式,又是一個 Web 服務器,它對 Go 的源碼進行處理,並提取包中的文檔內容。 出現在頂級聲明之前,且與該聲明之間沒有空行的注釋,將與該聲明一起被提取出來,作為該條目的說明文檔。
命名
- Go語言的命名會影響語義:某個名稱在包外是否可見,取決於其首個字元是否為大寫字母。
- 包:應當以小寫單個單詞來命名,且不應使用底線或駝峰記法。
- 包名:應為其源碼目錄的基本名稱,例如在 src/pkg/encoding/base64 中的包應作為"encoding/base64" 匯入,其包名應為 base64
- 擷取器:若有個名為 owner (小寫,未匯出) 的欄位,其擷取器應當名為 Owner(大寫,可匯出) 而非 GetOwner。若要提供設定器方法,可以選擇SetOwner。
- 介面:只包含一個方法的介面應當以該方法的名稱加上 - er 尾碼來命名
- 駝峰記法:Go 中約定使用駝峰記法 MixedCaps 或 mixedCaps
分號
- Go的詞法分析器會用簡單的規則來自動插入分號
- 如果在一行中寫多個語句,需要用分號隔開
- 控制結構的左大括弧不能放在下一行,因為根據詞法分析器的規則,會在大括弧前加入一個分號,造成錯誤
初始化
常量必須在定義的時候就進行初始化。常量只能是數字、字元、字串、布爾值等基本類型,定義它們的運算式必須是在編譯期就可以求值的類型。使用const來定義一個常量:
const LENGTH int = 10 const WIDTH int = 5
在Go中,枚舉常量使用iota來建立,iota是一個自增長的值:
type AudioOutput intconst ( OutMute AudioOutput = iota // 0 OutMono // 1 OutStereo // 2 _ _ OutSurround // 5)
iota總是用於increment,但它也可以用於運算式,在《effective Go》展示了一個定義數量級的表示:
type ByteSize float64const ( _ = iota // 使用_來忽略iota=0 KB ByteSize = 1 << (10 * iota) // 1 << (10*1) MB // 1 << (10*2) GB // 1 << (10*3) TB // 1 << (10*4) PB // 1 << (10*5) EB // 1 << (10*6) ZB // 1 << (10*7) YB // 1 << (10*8))
源檔案可以定義無參數init函數,該函數在真正執行函數邏輯之前被自動調用,下面的程式簡單說明這一點:
package mainimport "fmt"func init() { fmt.Print("執行init函數0\n")}func init() { fmt.Print("執行init函數1\n")}func init() { fmt.Print("執行init函數2\n")}func main() { fmt.Print("執行main函數\n")}//output :執行init函數0執行init函數1執行init函數2執行main函數
可以看到,在執行main函數中的邏輯前,init函數會先被調用,而且同一個源檔案中可以定義多個init函數。init函數通常被用在程式真正執行之前對變數、程式狀態進行校正。它的執行機制是這樣的:
- 該包中所有的變數都被初始化器求值後,init才會被調用
- 之後在所有已匯入的包都被初始化之後,init才會被調用
控制結構
Go使用更加通用的for來代替do與while迴圈,for的三種形式為:
// Like a C forfor init ; condition;post { }//Like a C while for condition{ }//Like a C for(;;)for {}
對於數組、切片、字串、map,或者從通道讀取訊息,可以使用range子句:
for key ,value := range oldMap { newMap[key] = value}
Go的switch要更加靈活通用,當switch後面沒有運算式的時候,它將匹配ture,這也意味著if-else-if-else鏈可以使用switch來實現:
func unhex(c byte) byte { switch { //switch將匹配true case '0' <= c && c <= '9': return c - '0' case 'a' <= c && c <= 'f': return c - 'a' + 10 case 'A' <= c && c <= 'F': return c - 'A' + 10 } return 0}
函數
Go的函數可以進行多值返回。在C語言中經常有這種笨拙的用法:函數通過傳回值來告知函數的執行情況,例如返回0代表無異常,返回-1表示EOF等,而通過指標實參來傳遞資料給外部。現在使用Go函數的多值返回可以解決解決這個問題。下面是Go標準庫中開啟檔案的File.Write的簽名:
func (file *File) Write(b []byte) (n int, err error)
Write函數返回寫入的位元組數以及一個錯誤。如果正確寫入了,則err為nil,否則,err為一個非nil的error錯誤值,這在Go中是一種常見的編碼風格。
Go函數的傳回值可以被命名。Go的傳回值在函數體內可以作為常規的變數來使用,稱為結果“形參”,結果“形參”在函數開始執行時被初始化與其類型相應的零值。如果函數執行了不帶參數的return,則把結果形參的當前值返回:
func abs(i int) (result int){ if i < 0{ result = -i //傳回值result可以直接當成常規變數使用 } return }
這樣做的好處是函數的簽名即為文檔,傳回值的含義也寫到了函數簽名中,提高了代碼的可讀性。
Go提供defer語句用於順延強制函數。defer語句修飾的函數,在外層函數結束之前被調用。可以這樣來使用defer語句:
func printStr (a string){ fmt.Print(a);}func main() { defer printStr("one\n") defer printStr("two\n") defer printStr("three\n") fmt.Print("main()\n")}//output :main()threetwoone
關於defer語句:
- 適用於關閉開啟的檔案,避免多個返迴路徑都需要去關閉檔案。
- 被延遲執行的函數的實參,才延遲執行時就會求值,而不是在調用執行時才求值。
- 被延遲的函數按照後進先出(LIFO)的順序執行。
- defer語句是在函數層級的,即使把它寫在大括弧(塊)中,也只會在調用函數結束時才調用被延遲執行的函數。
使用defer語句時還有一些細節需要注意。下面這段代碼:
func main() { fmt.Print(test())}func test() (r int) { defer func() { r = 1 return }() r = 2 return 3}//output:1
輸出並不是3,而是1.原因是return的操作實際包括了:
r = 0 //結果“形參”在函數開始執行時被初始化為零值r = 2 r = 1 //defer語句執行return r
記憶體配置
Go提供了兩種分配原語new與make:
func new(Type) *Typefunc make(t Type, size ...IntegerType) Type
new(T)用於分配記憶體,它返回一個指標,指向新分配的,類型為T的零值,通過new來申請的記憶體都會被置零。這意味著如果設計了某種資料結構,那麼每種類型的零值就不必進一步初始化了。
make(T,args)的目的不同於new(T),它只用於建立切片(slice)、映射(map)、通道(channel),這三種類型本質上與引用資料類型,它們在使用前必須初始化。make傳回型別為一個類型為T的已初始化的值,而非*T。
下面是new與make的對比:
var p *[]int = new([]int) // 分配切片結構;*p == nil;基本沒用var v []int = make([]int, 100) // 切片 v 現在引用了一個具有 100 個 int 元素的新數組// 沒必要的複雜:var p *[]int = new([]int)*p = make([]int, 100, 100)// 習慣用法:v := make([]int, 100)
數組
Go的數組與C語言的數組有很大的區別:
- 數組是值,把數組傳遞給函數,函數會得到該數組的一個副本,而不是指標。
- 數組的大小是類型的一部分。
[10]int與[20]int是兩種類型。
如果想要像C語言那樣傳遞數組指標,需要這樣做:
func Sum(a *[3]float64) (sum float64) {for _, v := range *a {sum += v} return} array := [...]float64{7.0, 8.5, 9.1}x := Sum(&array) // 注意顯式的取址操作
但在Go中通常不會這樣做,而是通過切片來實現引用的傳遞。切片儲存了對底層數組的引用,若你將某個切片賦予另一個切片,它們會引用同一個數組。
切片
切片是一個很小的對象,它對底層數組進行了抽象,並提供相應的操作方法,切片包含3個欄位,其的內部實現為:
可以通過一些方式來定義切片:
var slice0 []type //通過聲明一個未指定大小的數組來定義切片var slice1 []type = make([]type, len) //通過make來建立切片,長度與容量都是5個元素make([]T, length, capacity) //可以通過make來指定它的容量
聲明的時候,只要在[]運算子裡指定了一個值,那麼建立的就是數組而不是切片,只有不指定值的時候,才會建立切片。
切片之所以稱為切片,是因為建立一個新的切片就是把底層數組切出一部分,例如代碼:
slice := [] int {10,20,30,40,50} //建立一個切片,長度與容量都是5newSlice := slice[1:3] //建立一個新切片,其長度為5,容量為4
對底層數組容量是k的新切片slice[i,j]來說,長度是j-i,容量是k-i,建立的新切片內部實現為:
由於兩個切片共用一部分的底層數組,所以修改newSlice的第2個元素,也將同樣修改了slice的第三個元素。
可以使用append來增長切片的長度,這有兩種情況:
- 當切片的可用容量足夠時,append函數會增加切片的長度,而不會改變容量
- 當切片的可用容量不足時,append函數會增加切片的容量,增加的策略是:切片容量小於1000時,總是成倍地增加容量;一旦元素個數超過1000個,容量增加因子為1.25,也就是每次會增加25%。
當append函數造成切片容量拓展時,該切片將擁有一個全新的底層數組。
映射
映射與切片一樣,也是參考型別。如果通過一個不存在的key來擷取value,將返回與該映射中項的類型對應的零值:
var map1 map[string] int map1 = make(map[string]int ,10) map1["one"]=1 map1["two"]=2 fmt.Print(map1["three"])//output: 0
如果map1["three"]的value剛好是0,該怎麼區分呢?可以採用多重賦值的形式來分辨這種情況:
i, ret := map1["three"] if ret == true{ fmt.Print("map1[\"three\"]存在,值為:", i) } else { fmt.Print("map1[\"three\"] 不存在\n") }
或者這樣寫更好一些,《effective Go》稱為the “comma ok” idiom ,逗號OK慣用法
if i, ret := map1["three"] ;ret { fmt.Print("map1[\"three\"]存在,值為:", i) } else { fmt.Print("map1[\"three\"] 不存在\n") }
如果僅是需要判斷某個key是否存在,可以用空白標識符_來代替value:
if _, ret := map1["three"] ;ret { fmt.Print("map1[\"three\"]存在\n") } else { fmt.Print("map1[\"three\"]不存在,值為:") }
使用內建函數delete函數來刪除索引值對,即使對應的鍵不在該映射中,delete操作也是安全的
方法
在函數的一節中,我們已經看到了write函數的聲明為:
func (file *File) Write(b []byte) (n int, err error)
我們可以抽象出Go中函數的結構為:
func [(p mytype)] funcname([pram type]) [(parm type)] {//body}
其中,函數的(p mytype)為可選部分,具備此部分的函數稱為方法(method),這部分稱為接收者(receiver)。我們可以為任何已命名的類型,包括自己定義的結構體類型,定義方法。通過receiver,把方法綁定到類型上。下面是一個樣本:
package mainimport "fmt"//定義一個矩形類型type rect struct { width ,height int}//這個方法擴大矩形邊長為multiple倍//這個方法的reciever為*rect//表示這是定義在rect結構體上的方法func (r *rect) bigger(multiple int){ r.height *=multiple r.height *=multiple}//方法的reciever可以為結構體類型//也可以為結構體指標類型//區別在於當reciever為類型指標時//可以在該方法內部修改結構體成員func (r rect) area() int{ return r.width*r.height}func main(){ r := rect{width:10,height:5} fmt.Print("r 's area:",r.area(),"\n") r.bigger(10) fmt.Print("r's area:",r.area())}//output:r 's area:50r's area:5000
以指標或值作為reciever的區別在於::
- 指標可以修改接收者
- 值方法可通過指標和值調用,而指標方法只能通過指標來調用
值方法可以通過指標和值調用,所以下面語句是合法的:
func main(){ r := rect{width:10,height:5} //通過指標調用 fmt.Print("r 's area:",(&r).area(),"\n") //通過值調用 fmt.Print("r 's area:",r.area(),"\n")}//output:r 's area:50r 's area:50
而對於指標方法只能通過指標來調用,你可能會感到疑惑,因為下面的語句也是合法的:
func main(){ r := rect{width:10,height:5} fmt.Print("r 's area:",r.area(),"\n") //通過值來調用指標方法(為什麼合法?) r.bigger(10) fmt.Print("r's area:",r.area())}//output:
其實是這樣的:如果值是可以定址的,那麼Go會自動插入取址操作符來對付一般的通過值調用的指標方法。在這個例子中,r是可定址的,因此r.Bigger(10)將被編譯器改寫為(&r).Bigger。
另外,方法也可以"轉換"為函數,這一點便不在這裡詳談。
介面
通過方法與介面,Go語言定義了一種與java/C++等OOP語言截然不同的“繼承”的形態。通過實現介面定義的方法,便可將reciever的類型變數賦值給介面類型變數,通過介面類型變數來調用到reciever類型的方法,用C++來類比,就是通過父類指標來調用到了衍生類別的成員函數(不過Go沒有這些概念)。下面是一個樣本:
package mainimport ( "fmt" "math")//定義了一個介面geometry表示幾何類型type geometry interface { area() float64 bigger(float64)}//矩形和圓形要實現這介面的兩個方法type rect struct { width, height float64}type circle struct { radius float64}//在Go中,實現介面,只需要實現該介面定義的所有方法即可//矩形的介面方法實現func (r *rect) bigger(multiple float64) { r.height *= multiple r.height *= multiple}func (r *rect) area() float64 { return r.width * r.height}//圓形的介面方法實現func (c *circle) bigger(multiple float64){ c.radius *= multiple}func (c *circle) area() float64 { return math.Pi * c.radius * c.radius}//可以把rect和circle類型的變數作為實參//傳遞給geometry介面類型的變數func measure (g geometry){ fmt.Print("geometry 's area:",g.area(),"\n") g.bigger(2) fmt.Print("after bigger 2 multiple, area :",g.area(),"\n")}func main() { r := rect{width: 10, height: 5} c := circle{radius:3} measure(&r) measure(&c)}//output:geometry 's area:50after bigger 2 multiple, area :200geometry 's area:28.274333882308138after bigger 2 multiple, area :113.09733552923255
類型轉換
字面量的值,Go編譯器會進行隱式轉換:
func main() {var myInt int32 =5var myFloat float64 = 6fmt.Print(myInt,"\n")fmt.Print(myFloat)}
這裡的6為整數型別的字面值常量 Integer literals.。它賦值給了float64類型變數,編譯器進行了隱式類型轉換。
底層類型不同的變數,需要顯式類型轉換:
func main() { var myInt int32 =5 //var myFloat float64 = myInt //error var myFloat float64 = float64(myInt) //需要顯式轉換 fmt.Print(myInt,"\n") fmt.Print(myFloat)}
這裡還要區分靜態類型與底層類型:
type IntA int32type IntB int32 func main() { var a IntA =1 //var b IntB = a //error var b IntB = IntB(a) fmt.Print(a,"\n") fmt.Print(b)}
這裡IntA為變數a的靜態類型,而int32為變數a的底層類型。即使兩個類型的底層類型相同,在相互賦值時還是需要強制類型轉換的。
介面類型變數的類型轉換,有兩種情況:
- 普通類型向介面類型的轉換:隱式進行
- 介面類型向普通類型的轉換:需要類型斷言
根據Go 官方文檔 所說,所有的類型,都實現了空介面interface{},所以普通類型都可以向interface{}進行類型轉換:
func main() { var x interface{} = "hello" // 字串常量->interface{} var y interface{} = []byte{'w','o','r','l','d'} //[]byte ->interface{} fmt.Print(x," ") fmt.Printf("%s",y)}
而介面類型向普通類型的轉換,則需要由Comma-ok斷言或switch測試來進行了。
Comma-ok斷言
文法: value,ok := element.(T)
element必須為ingerface類型,宣告失敗,ok為false,否則為true,下面是常式:
func main() { var vars []interface{} = make([]interface{},5) vars[0] = "one" vars[1] = "two" vars[2] = "three" vars[3] = 10 vars[4] = []byte{'a', 'b', 'c'} for index, element := range vars { if value, ok := element.(int); ok { fmt.Printf("vars[%d] type is int,value is %d \n",index,value) }else if value,ok := element.(string);ok{ fmt.Printf("vars[%d] type is string,value is %s \n",index,value) }else if value,ok := element.([]byte);ok{ fmt.Printf("vars[%d] type is []byte,value is %s \n",index,value) } }}//output:vars[0] type is string,value is one vars[1] type is string,value is two vars[2] type is string,value is three vars[3] type is int,value is 10 vars[4] type is []byte,value is abc
Comma-ok斷言也可以這樣使用:
value := element.(T)
但一旦宣告失敗將產生執行階段錯誤,不推薦使用。
switch測試
switch測試只能在switch語句中使用。將上面的常式改為switch測試:
func main() { var vars []interface{} = make([]interface{}, 5) vars[0] = "one" vars[1] = "two" vars[2] = "three" vars[3] = 10 vars[4] = []byte{'a', 'b', 'c'} for index, element := range vars { switch value := element.(type) { case int: fmt.Printf("vars[%d] type is int,value is %d \n", index, value) case string: fmt.Printf("vars[%d] type is string,value is %s \n", index, value) case []byte: fmt.Printf("vars[%d] type is []byte,value is %s \n", index, value) } }}
(完)