一:命名html
1:Go語言中的函數名、變量名、常量名、類型名、語句標號和包名等全部的命名,都遵循一個簡單的標識符命名規則:一個名字必須以一個字母(Unicode字母)或下劃線開頭,後面能夠跟任意數量的字母、數字或下劃線。注意,這裏的字母是指Unicode編碼字母,所以Go語言開發者能夠在代碼中自由地使用他們的母語。程序員
Go中的標識符區分大小寫。golang
2:Go定義了若干關鍵字,標識符不能是這些關鍵字之一。Go定義的關鍵字以下:數組
break安全 |
defaultapp |
funcide |
interface模塊化 |
select函數 |
case工具 |
defer |
go |
map |
struct |
chan |
else |
goto |
package |
switch |
const |
fallthrough |
if |
range |
type |
continue |
for |
import |
return |
var |
Go還預約義了一些標識符,雖然定義與這些預約義標識符同樣的標識符也能編譯經過,但最好不這麼作。預約義的標識符以下:
Types |
bool byte complex64 complex128 error float32 float64 int int8 int16 int32 int64 rune string uint uint8 uint16 uint32 uint64 uintptr |
Constants |
true false iota |
Zero value |
nil |
Functions |
append cap close complex copy delete imag len make new panic print println real recover |
3:空標識符」_」是一個佔位符,在賦值操做的時候將某個值賦值給空標識符,從而達到丟棄該值的目的。
空標識符不是一個新的變量,所以將它用於」:=」操做符的時候,必須同時爲至少另外一個值賦值。
4:若是一個名字是在函數內部定義,那麼它就只在函數內部有效。若是是在函數外部定義,那麼將在當前包的全部文件中均可以訪問。
名字的開頭字母的大小寫決定了名字在包外的可見性。若是一個名字是大寫字母開頭的(譯註:必須是在函數外部定義的包級名字;包級函數名自己也是包級名字),那麼它將是導出的,也就是說能夠被外部的包訪問,例如fmt包的Printf函數就是導出的,能夠在fmt包外部訪問。包自己的名字通常老是用小寫字母。
5:Go語言的風格是儘可能使用短小的名字。一般來講,若是一個名字的做用域比較大,生命週期也比較長,那麼用長的名字將會更有意義。
6:在習慣上,Go語言程序員推薦使用 駝峯式 命名,當名字有幾個單詞組成的時優先使用大小寫分隔,而不是優先用下劃線分隔。所以,在標準庫有QuoteRuneToASCII和parseRequestLine這樣的函數命名。
二:聲明
1:Go語言主要有四種類型的聲明語句:var、const、type和func,分別對應變量、常量、類型和函數實體對象的聲明。
2:一個Go語言編寫的程序對應一個或多個以.go爲文件後綴名的源文件中。每一個源文件以包的聲明語句開始,說明該源文件是屬於哪一個包。包聲明語句以後是import語句導入依賴的其它包,而後是包一級的類型、變量、常量、函數的聲明語句,包一級的各類類型的聲明語句的順序可有可無(譯註:函數內部的名字則必須先聲明以後才能使用)。好比:
func main() { str := "hehe" fmt.Println(str) fmt.Println(globalInt) printfun() } func printfun() { fmt.Println("hello,world") } var globalInt = 3
上面的例子中,printfun函數和變量globalInt 在定義以前就使用了,這是沒有問題的。可是在main中,若是在str定義以前就使用它,就會報語法錯誤。
三:變量
1:var聲明語句能夠建立一個特定類型的變量,而後給變量附加一個名字,而且設置變量的初始值。
變量聲明的通常語法以下:「 var 變量名字 類型 = 表達式 」。其中「類型」或「= 表達式」兩個部分能夠省略其中的一個。若是省略的是類型信息,那麼將根據初始化表達式來推導變量的類型信息。若是初始化表達式被省略,那麼將用零值初始化該變量。
數值類型變量對應的零值是0,布爾類型變量對應的零值是false,字符串類型對應的零值是空字符串,接口或引用類型(包括slice、map、chan和函數)變量對應的零值是nil。數組或結構體等聚合類型對應的零值是每一個元素或字段都是對應該類型的零值。
零值初始化機制能夠確保每一個聲明的變量老是有一個良好定義的值,所以在Go語言中不存在未初始化的變量。Go語言程序員應該讓一些聚合類型的零值也具備意義,這樣能夠保證無論任何類型的變量老是有一個合理有效的零值狀態。
2:能夠在一個聲明語句中同時聲明一組變量,或用一組初始化表達式聲明並初始化一組變量。若是省略每一個變量的類型,將能夠聲明多個類型不一樣的變量(類型由初始化表達式推導):
var i, j, k int // int, int, int var b, f, s = true, 2.3, "four" // bool, float64, string
初始化表達式能夠是字面量或任意的表達式,也能夠是函數的返回值:
var f, err = os.Open(name) // os.Open returns a file and an error
3:在包級別聲明的變量會在main入口函數執行前完成初始化,局部變量將在聲明語句被執行到的時候完成初始化:
var globalInt = getvalue() func getvalue() int { fmt.Println("this is getvalue") return 3 } func main() { fmt.Printf("globalInt is %d\n", globalInt) }
結果以下:
this is getvalue globalInt is 3
4:在函數內部,能夠以「簡短變量聲明」的形式聲明和初始化局部變量,也就是「名字 := 表達式」的形式。變量的類型根據表達式來自動推導。
注意,這種形式只能用於局部變量,不能用於包級別變量;
同var形式聲明同樣,在「簡短變量聲明」中,可使用字面量、表達式和函數返回值來初始化變量:也能夠初始化一組變量:
//i := 3 func main() { i := getValue() j := 1 k := rand.Float64() * 3.0 m, n := 3.4, "abc" }
上面的代碼中,若是把註釋去掉,就會報編譯錯誤:」syntax error: non-declaration statement outside function body」。
簡短變量聲明左邊的變量不必定都是新聲明的變量。若是有一些變量已經在相同的詞法域聲明過了,那麼簡短變量聲明語句對這些已經聲明過的變量就只有賦值行爲了。須要注意的是,至少要有一個變量是新的,不然回報編譯錯誤:
func getValue() (int,string) { return 3, "abc" } func main() { i := 2 //j := "hehe" i,j := getValue() }
若是去掉註釋的話,回報編譯錯誤:」no new variables on left side of :=」。
注意:簡短變量聲明語句只有對已經在同級詞法域聲明過的變量才和賦值操做語句等價,若是變量是在外部詞法域聲明的,那麼簡短變量聲明語句將會在當前詞法域從新聲明一個新的變量。
5:普通變量在聲明語句建立時被綁定到一個變量名,經過指針,咱們能夠直接讀或更新對應變量的值,而不須要知道該變量的名字(若是變量有名字的話)。
類型*int表示指向int類型的指針。
任何類型的指針的零值都是nil。指針之間能夠進行相等測試,只有當它們指向同一個變量或所有是nil時才相等。
在Go語言中,返回函數中局部變量的地址也是安全的。例以下面的代碼,調用f函數時建立局部變量v,在局部變量地址被返回以後依然有效,由於指針p依然引用這個變量:
func f() *int { v := 1 return &v } func main() { var p = f() fmt.Println(p) fmt.Println(*p) *p = 3 fmt.Println(p) fmt.Println(*p) p = f() fmt.Println(p) fmt.Println(*p) }
每次調用f函數都將返回不一樣的結果,上述代碼結果以下:
0xc420012098 1 0xc420012098 3 0xc4200120d0 1
每次對一個變量取地址,或者複製指針,咱們都是爲原變量建立了新的別名。指針特別有價值的地方在於咱們能夠不用名字而訪問一個變量,可是這是一把雙刃劍:要找到一個變量的全部訪問者並不容易,咱們必須知道變量所有的別名(譯註:這是Go語言的垃圾回收器所作的工做)。不只僅是指針會建立別名,不少其餘引用類型也會建立別名,例如slice、map和chan,甚至結構體、數組和接口都會建立所引用變量的別名。
6:另外一個建立變量的方法是調用用內建的new函數。表達式new(T)將建立一個T類型的匿名變量,初始化爲T類型的零值,而後返回變量地址,返回的指針類型爲*T。
p := new(int) // p, *int 類型, 指向匿名的 int 變量 fmt.Println(*p) // "0" *p = 2 // 設置 int 匿名變量的值爲 2 fmt.Println(*p) // "2"
7:變量的生命週期指的是在程序運行期間變量有效存在的時間間隔。對於包一級聲明的變量,它們的生命週期和整個程序的運行週期是一致的。而局部變量的生命週期則是動態的:從每次建立一個新變量的聲明語句開始,直到該變量再也不被引用爲止,而後變量的存儲空間可能被回收。函數的參數變量和返回值變量都是局部變量。它們在函數每次被調用的時候建立。
那麼Go語言的自動垃圾收集器是如何知道一個變量是什麼時候能夠被回收的呢?基本的實現思路是,從每一個包級的變量和每一個當前運行函數的每個局部變量開始,經過指針或引用的訪問路徑遍歷,是否能夠找到該變量。若是不存在這樣的訪問路徑,那麼說明該變量是不可達的,也就是說它是否存在並不會影響程序後續的計算結果。
由於一個變量的有效週期只取決因而否可達,所以一個循環迭代內部的局部變量的生命週期可能超出其局部做用域。同時,局部變量可能在函數返回以後依然存在。
編譯器會自動選擇在棧上仍是在堆上分配局部變量的存儲空間,但可能使人驚訝的是,這個選擇並非由用var仍是new聲明變量的方式決定的:
var global *int func f() { var x int x = 1 global = &x } func g() { y := new(int) *y = 1 }
f函數裏的x變量必須在堆上分配,由於它在函數退出後依然能夠經過包一級的global變量找到,用Go語言的術語說,這個x局部變量從函數f中逃逸了;相反,當g函數返回時,變量*y將是不可達的,也就是說能夠立刻被回收的。所以,*y並無從函數g中逃逸,編譯器能夠選擇在棧上分配*y的存儲空間,雖然這裏用的是new方式。
其實在任什麼時候候,你並不需爲了編寫正確的代碼而要考慮變量的逃逸行爲,要記住的是,逃逸的變量須要額外分配內存,同時對性能的優化可能會產生細微的影響。
Go語言的自動垃圾收集器對編寫正確的代碼是一個巨大的幫助,但也並非說你徹底不用考慮內存了。你雖然不須要顯式地分配和釋放內存,可是要編寫高效的程序你依然須要瞭解變量的生命週期。例如,若是將指向短生命週期對象的指針保存到具備長生命週期的對象中,特別是保存到全局變量時,會阻止對短生命週期對象的垃圾回收(從而可能影響程序的性能)。
四:賦值
1:Go支持複合賦值語句,好比:a += 3
2:數值變量支持++遞增和--遞減語句。
注意:自增和自減是語句,而不是表達式,所以x = i++之類的表達式是錯誤的;自增和自減只支持後綴形式,不支持前綴。因此,下面的三條語句都是錯誤的:
b = a++ fmt.Println(a++) ++a
3:元組賦值是另外一種形式的賦值語句,它容許同時更新多個變量的值。在賦值以前,賦值語句右邊的全部表達式將會先進行求值,而後再統一更新左邊對應變量的值。因此能夠這樣交換兩個變量的值:x, y = y, x
4:賦值語句是顯式的賦值形式。程序中還有不少地方會發生隱式的賦值行爲:函數調用會隱式地將調用參數的值賦值給函數的參數變量,一個返回語句將隱式地將返回操做的值賦值給結果變量,一個複合類型的字面量也會產生賦值行爲。例以下面的語句:
medals := []string{"gold", "silver", "bronze"}
隱式地對slice的每一個元素進行賦值操做,相似這樣寫的行爲:
medals[0] = "gold" medals[1] = "silver" medals[2] = "bronze"
無論是隱式仍是顯式地賦值,只有右邊的值對於左邊的變量是可賦值的,賦值纔是容許的。
可賦值性的規則對於不一樣類型有着不一樣要求,目前而言,它的規則是簡單的:類型必須徹底匹配,nil能夠賦值給任何指針或引用類型的變量。常量則有更靈活的賦值規則,由於這樣能夠避免沒必要要的顯式的類型轉換。
對於兩個值是否能夠用==或!=進行相等比較的能力也和可賦值能力有關係:對於任何類型的值的相等比較,第二個值必須是對第一個值類型對應的變量是可賦值的,反之依然。
五:類型
1:變量或表達式的類型定義了對應存儲值的屬性特徵,例如數值在內存的存儲大小(或者是元素的bit個數),它們在內部是如何表達的,是否支持一些操做符,以及它們本身關聯的方法集等。
2:類型聲明語句建立了一個新的類型名稱,和現有類型具備相同的底層結構。新命名的類型提供了一個方法,用來分隔不一樣概念的類型,這樣即便它們底層類型相同也是不兼容的。
類型聲明的格式是:type 類型名字 底層類型
類型聲明語句通常出如今包一級,所以若是新建立的類型名字的首字符大寫,則在外部包也可使用。
3:下面的語句:
type Celsius float64 // 攝氏溫度 type Fahrenheit float64 // 華氏溫度
聲明瞭兩種類型:Celsius和Fahrenheit分別對應不一樣的溫度單位。它們雖然有着相同的底層類型float64,可是它們是不一樣的數據類型,所以它們不能夠被相互比較或混在一個表達式運算。刻意區分類型,能夠避免一些像無心中使用不一樣單位的溫度混合計算致使的錯誤。
4:對於每個類型T,都有一個對應的類型轉換操做T(x),用於將x轉爲T類型(譯註:若是T是指針類型,可能會須要用小括弧包裝T,好比(*int)(0))。只有當兩個類型的底層基礎類型相同時,才容許這種轉型操做,或者是二者都是指向相同底層結構的指針類型。這些轉換隻改變類型而不會影響值自己。
數值類型之間的轉型也是容許的,字符串和一些特定類型的slice之間也是能夠轉換的,這類轉換可能改變值的表現。例如,將一個浮點數轉爲整數將丟棄小數部分,將一個字符串轉爲[]byte類型的slice將拷貝一個字符串數據的副本。在任何狀況下,運行時不會發生轉換失敗的錯誤(譯註: 錯誤只會發生在編譯階段)。
下面的轉換都是合法的;
var a int = 1 x := Celsius(1) y := Celsius(a) z := float64(a)
5:比較運算符如==或<能夠用來比較一個命名類型的變量和另外一個有相同類型的變量,或有着相同底層類型的未命名類型的值之間作比較。可是若是兩個值有着不一樣的類型,則不能直接進行比較:
var c Celsius var f Fahrenheit fmt.Println(c == 0) // "true" fmt.Println(f >= 0) // "true" fmt.Println(c == f) // compile error: type mismatch fmt.Println(f >= float64(3)) // compile error: type mismatch fmt.Println(c == Celsius(f)) // "true"!
6:命名類型還能夠爲該類型的值定義新的行爲。這些行爲表示爲一組關聯到該類型的函數集合,稱爲類型的方法集。後續會詳細介紹,這裏僅說寫簡單用法:
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }
Celsius類型的參數c出如今了函數名的前面,表示聲明的是Celsius類型的一個叫名叫String的方法,該方法返回該類型對象的字符串表示。
許多類型都會定義一個String方法,由於當使用fmt包的打印方法時,將會優先使用該類型對應的String方法返回的結果打印:
x := Celsius(1.04) fmt.Println(x) //1.04°C
六:包和文件
1:Go語言中的包和其餘語言的庫或模塊的概念相似,目的都是爲了支持模塊化、封裝、單獨編譯和代碼重用。
一個包的源代碼保存在一個或多個以.go爲文件後綴名的源文件中,同一個包中的多個源文件中,不能定義同名的變量、函數和類型等。
一般一個包所在目錄路徑的後綴是包的導入路徑;例如包gopl.io/ch1/helloworld對應的目錄路徑是$GOPATH/src/gopl.io/ch1/helloworld。
2:每一個包都對應一個獨立的名字空間。例如,在image包中的Decode函數和在unicode/utf16包中的 Decode函數是不一樣的。要在外部引用該函數,必須顯式使用image.Decode或utf16.Decode形式訪問。
3:包級別的名字,例如在一個文件聲明的類型和常量,在同一個包的其餘源文件是能夠直接訪問的,就好像全部代碼都在一個文件同樣。
包可讓咱們經過控制哪些名字是外部可見的來隱藏內部實現信息。一個簡單的規則是:若是一個名字是大寫字母開頭的,那麼該名字是導出的(譯註:由於漢字不區分大小寫,所以漢字開頭的名字是沒有導出的)。
4:每一個源文件都是以包的聲明語句開始,用來指名包的名字。包聲明前能夠有包註釋。一個包一般只有一個源文件有包註釋。若是有多個包註釋,目前的文檔工具會根據源文件名的前後順序將它們連接爲一個包註釋。若是包註釋很大,一般會放到一個獨立的doc.go文件中。
5:在Go語言程序中,每一個包都是有一個全局惟一的導入路徑。導入語句中相似"gopl.io/ch2/tempconv"的字符串對應包的導入路徑。Go語言的規範並無定義這些字符串的具體含義或包來自哪裏,它們是由構建工具來解釋的。當使用Go語言自帶的go工具箱時,一個導入路徑表明一個目錄中的一個或多個Go源文件。
除了包的導入路徑,每一個包還有一個包名,包名通常是短小的名字(並不要求包名是惟一的),包名在包的聲明處指定。按照慣例,一個包的名字和包的導入路徑的最後一個字段相同,例如gopl.io/ch2/tempconv包的名字通常是tempconv。
導入語句將導入的包綁定到包名,而後經過該包名就能夠引用包中導出的所有內容。所以能夠tempconv.CToF的形式來訪問gopl.io/ch2/tempconv包中的內容。在默認狀況下,導入的包綁定到tempconv名字(譯註:這包聲明語句指定的名字),可是咱們也能夠綁定到另外一個名稱,以免名字衝突。
6:若是導入了一個包,可是又沒有使用該包將被看成一個編譯錯誤處理。
7:可使用golang.org/x/tools/cmd/goimports導入工具,它能夠根據須要自動添加或刪除導入的包。
8:包的初始化首先是解決包級變量的依賴順序,而後按照包級變量聲明出現的順序依次初始化:
var a = b + c // a 第三個初始化, 爲 3 var b = f() // b 第二個初始化, 爲 2, 經過調用 f (依賴c) var c = 1 // c 第一個初始化, 爲 1 func f() int { return c + 1 }
若是包中含有多個.go源文件,Go語言的構建工具首先會將.go文件根據文件名排序,而後依次調用編譯器編譯,若是同一個包中多個源文件中的變量有相互依賴的關係,則初始化順序也能夠是穿插的。
每一個文件均可以包含多個init初始化函數。這樣的init初始化函數除了不能被調用或引用外,其餘行爲和普通函數相似。在每一個文件中的init初始化函數,在程序開始執行時按照它們聲明的順序被自動調用。
包的初始化過程當中,首先初始化全部go文件的變量,而後纔是調用init函數。好比,若是包中包含兩個go文件:file1和file2,file1和file2的代碼分別以下:
//file1.go var a int = getvalue(3) var b int = c + getvalue(2) func init() { fmt.Println("this is p1 init func") } //file2.go func getvalue(arg int) int { fmt.Printf("this is getvalue(%d)\n", arg) return arg } func init() { fmt.Println("this is p2 init func") } func init() { fmt.Println("this is p22 init func") } var c int = getvalue(33) var d int = getvalue(11) + getvalue(111)
根據文件名的排序,首先初始化file1中定義的變量a,該變量須要調用file2中定義的getvalue函數;而後初始化變量b,該變量須要用file2中定義的變量c,所以先初始化file2中的變量c,以後再初始化b;file1中的變量初始化完成以後,而後是初始化file2中的變量d;全部變量初始化完成以後,開始調用文件中的init函數,首先是file1中的init,而後是file2中的兩個init,所以,運行結果以下:
this is getvalue(3) this is getvalue(33) this is getvalue(2) this is getvalue(11) this is getvalue(111) this is p1 init func this is p2 init func this is p22 init func
10:每一個包在解決依賴的前提下,以導入聲明的順序初始化,每一個包只會被初始化一次。所以,若是一個p包導入了q包,那麼在p包初始化的時候q包必然已經初始化過了。初始化工做是自下而上進行的,main包最後被初始化。以這種方式,能夠確保在main函數執行以前,全部依然的包都已經完成初始化工做了。
七:做用域
1:聲明語句的做用域是指源代碼中能夠有效使用這個名字的範圍。
不要將做用域和生命週期混爲一談。聲明語句的做用域對應的是一個源代碼的文本區域;它是一個編譯時的屬性。一個變量的生命週期是指程序運行時變量存在的有效時間段,在此時間區域內它能夠被程序的其餘部分引用;是一個運行時的概念。
2:語法塊是由花括弧所包含的一系列語句,語法塊內部聲明的名字是沒法被外部語法塊訪問的。
3:對於內置的類型、函數和常量,好比int、len和true等是在全局做用域的,所以能夠在整個程序中直接使用;任何在在函數外部(也就是包級語法域)聲明的名字能夠在同一個包的任何源文件中訪問的;對於導入的包,例如tempconv導入的fmt包,則是對應源文件級的做用域,所以只能在當前的文件中訪問導入的fmt包,當前包的其它源文件沒法訪問在當前源文件導入的包;還有許多聲明語句,則是局部做用域的,它只能在函數內部(甚至只能是局部的某些部分)訪問;控制流標號,就是break、continue或goto語句後面跟着的那種標號,則是函數級的做用域。
4:一個程序可能包含多個同名的聲明,只要它們在不一樣的詞法域就沒有關係。例如,能夠聲明一個局部變量,和包級的變量同名。
當編譯器遇到一個名字引用時,它首先從最內層的詞法域向全局的做用域查找。若是查找失敗,則報告「未聲明的名字」這樣的錯誤。若是該名字在內部和外部的塊分別聲明過,則內部塊的聲明首先被找到。在這種狀況下,內部聲明屏蔽了外部同名的聲明。
5:有許多語法塊是if或for等控制流語句構造的:
for i := 0; i < len(x); i++ { x := x[i] if x != '!' { x := x + 'A' - 'a' fmt.Printf("%c", x) } }
正如上面例子所示,並非全部的詞法域都顯式地對應到由花括弧包含的語句;還有一些隱含的規則。上面的for語句建立了兩個詞法域:花括弧包含的是顯式的for循環體部分詞法域,另一個隱式的部分則是循環的初始化部分,好比用於迭代變量i的初始化。隱式的詞法域部分的做用域包含條件測試部分和循環後的迭代部分( i++ ),固然也包含循環體詞法域。
和for循環相似,if和switch語句也會在條件部分建立隱式詞法域,還有它們對應的執行體詞法域。下面的if-else測試鏈演示了x和y的有效做用域範圍:
if x := f(); x == 0 { fmt.Println(x) } else if y := g(x); x == y { fmt.Println(x, y) } else { fmt.Println(x, y) } fmt.Println(x, y) // compile error: x and y are not visible here
第二個if語句嵌套在第一個內部,所以第一個if語句條件初始化詞法域聲明的變量在第二個if中也能夠訪問。switch語句的每一個分支也有相似的詞法域規則:條件部分爲一個隱式詞法域,而後是每一個分支的詞法域。
6:在包級別,聲明的順序並不會影響做用域範圍,所以一個先聲明的能夠引用它自身或者是引用後面的一個聲明,可是若是一個變量或常量遞歸引用了自身,則會產生編譯錯誤。
要特別注意短變量聲明語句的做用域範圍,考慮下面的程序:
var cwd string func init() { cwd, err := os.Getwd() // compile error: unused: cwd if err != nil { log.Fatalf("os.Getwd failed: %v", err) } }
雖然cwd在外部已經聲明過,可是 := 語句仍是將cwd和err從新聲明爲新的局部變量。由於內部聲明的cwd將屏蔽外部的聲明,所以上面的代碼並不會正確更新包級聲明的cwd變量。
有許多方式能夠避免出現相似潛在的問題。最直接的方法是經過單獨聲明err變量,來避免使用 := 的簡短聲明方式:
var cwd string func init() { var err error cwd, err = os.Getwd() if err != nil { log.Fatalf("os.Getwd failed: %v", err) } }
http://docs.ruanjiadeng.com/gopl-zh/ch2/ch2.html