the little go book學習筆記(1):簡介
1.Go簡介
Go語言是編譯型、靜態型別的類C的語言,並帶有GC(垃圾收集器,garbage collection)。這意味著什麼?
另外,Go是一種非常嚴格的語言,它幾乎總是要求我們"以標準答案去答題",在其它語言可以容忍的不規範編碼方式在Go語言中幾乎都會拋異常。例如匯入了包卻沒有使用這個包,Go不會去編譯它並報錯。再例如,定義了一個變數但從來沒用過,也會報錯。
初學Go的時候,這可能是件無比的苦惱事情,但習慣了之後,編寫出來的程式自然是無比規範的。這也正是Go和不少語言的區別:其它語言編碼、除錯階段可能很快,但維護和優化階段可能會非常長;而Go的編碼週期可能稍長,但編碼完成後幾乎都是足夠優化的,維護和優化週期足夠短。
編譯型
編譯表示的是將你所寫的原始碼轉換為低層次的語言,例如組合語言(go採用此底層語言),或者其它中間的語言(如Java、C#編譯成位元組碼)。
編譯型語言可能不太友好,因為編譯的過程速度很慢。如果一個程式的編譯過程就需要花幾分鐘甚至幾小時,那麼程式的版本迭代可能會很難進行下去。編譯速度是Go語言的一個主要設計目標,值得慶幸的是,Go的編譯速度很快,即便對於習慣於使用解釋型語言的人來說,它也還是快。
編譯型語言雖然編譯過程慢,但這類語言在執行階段可能會更快,而且執行時不再需要載入額外的依賴。
靜態型別
靜態語言意味著變數必須要指定資料型別(int,string,bool,[]byte等)。雖然必須指定資料型別,但除了在宣告變數的時候顯式指定資料型別,也可以讓Go自己去推斷資料型別(稍後有示例)。
對於習慣於使用動態型語言的人來說,可能會感覺靜態型語言很笨重,事實確實如此。但靜態有靜態的好處,特別是配合編譯操作的時候。
關於靜態和動態資料型別,要說的內容其實很多很多,畢竟對於一門語言來說,資料型別牽一髮而動全身,無論是靜態、還是動態型語言,都因此而衍生出無數的優、缺點。
類C型的語言
當我們說一門語言是類C型(C-like)的語言時,意味著這門語言裡有一些語法和特性和C語言是類似的。例如,&&
表示布林的AND,==
表示等值比較,陣列索引從0開始計算,{...}
表示一段程式碼塊,也表示它屬於一個作用域範圍,等等。
類C型語言也意味著每行的語句要使用分號";"結束,條件表示式要使用括號包圍。但Go語言不採用這兩種方式,儘管還是可以使用括號包圍條件表示式以改變優先順序。例如:
if name == "malongshuai" {
print("name rigth!")
}
一個更復雜一點的條件表示式,使用括號改變優先順序:
if (name == "longshuai" && age > 23) || (name == "xiaofang" && age < 22) {
print("yeyeye!!!")
}
GC
每當建立一個變數後,這個變數都會有其生命週期。例如,函式內部的本地變數將在函式退出的時候消逝。對於非函式內部的變數生命週期,無論是對程式設計師還是對編譯器來說,變數的生命週期都沒有那麼顯而易見。
沒有garbage collection,意味著要讓程式設計師自己來決定變數所佔用記憶體的釋放,這是很艱鉅的任務,而且很容易出錯導致程式崩潰。
帶有GC的語言可以對變數進行跟蹤,並且在它們不再被需要的時候自動釋放它們。雖然GC帶來了一點點的負載,會影響一點點的效能,但對於現在高效能的計算機來說,這點影響相比它帶來的優點而言,完全可以將其無視。
嘗試寫一個簡單的Go程式
按照國際管理,每一門語言總是以hello world開篇。這裡就算了,因為我有我的慣例。
先安裝Go,so easy...
目前還沒有必要涉及Go的工作空間,所以隨意找個地方建立一個test.go檔案,內容如下:
package main
func main() {
println("Let's Go")
}
然後執行:
go run test.go
顯然,它將輸出Let's Go
。但是Go的編譯過程呢?go run
命令同時進行了編譯和執行兩個過程:它將使用一個臨時目錄儲存構建的程式,然後執行它,最後自動清理構建出來的臨時程式。
可以使用go run --work
檢視下具體情況:
$ go run --work test.go
WORK=/tmp/go-build267589647
Let's Go
構建的臨時目錄位於/tmp/go-buildXXXX中(我這是Linux),在此目錄下會有一個二進位制程式(對於Windows則是.exe檔案):
$ tree /tmp/go-build267589647/
/tmp/go-build267589647/
├── command-line-arguments
│ └── _obj
│ └── exe
│ └── test # 這是可執行二進位制程式
└── command-line-arguments.a
那個test檔案就是編譯後得到的二進位制程式,可以直接用來執行:
$ /tmp/go-build267589647/command-line-arguments/_obj/exe/test
Let's Go
如果要顯式編譯,使用go build
命令:
go build test.go
它將在當前目錄下生成一個名為test的二進位制檔案,可以直接拿來執行,就像前面/tmp中的一樣。
$ ./tese
Let's Go
在開發階段,用go build還是用go run,隨意即可。但在部署的時候,一般先go build,再go run。
main包和main函式
在上面的程式碼中,聲明瞭這個包的名稱為main,然後建立一個函式,並在此函式中使用println
輸出了一個字串,但是go run
如何知道要去執行什麼?在Go中,程式的入口是main包中的main函式,這兩名稱都是固定的。
對於一個從沒程式設計過的人,可能不理解程式的入口。它表示程式從此處開始執行,函式main中可能會包含很多其它函式的呼叫,這些函式可能放在其它檔案(包)中。通過一次次、一層層的呼叫,從而將整個程式的所有程式碼、邏輯都連線在一起並執行。
如果你願意,可以試著修改一下package後面的main
關鍵字,然後go run
和go build
都執行一下。再試著修改一下func main
的main
關鍵字,go run
和go build
再執行一下。
關於包的內容,後面再做介紹。目前來說,需要理解的只是些基礎,對於基礎階段來說,我們將總是在main包中寫程式碼。
import
Go有一些內建的函式,例如上面的println
,內建函式無需額外的引用就可直接呼叫。但內建函式畢竟很少,所以得從已經寫好的Go標準庫和其它第三方庫中找出一些工具來使用。
在Go中,import
關鍵字用於定義要匯入到當前檔案的包名,匯入某個包後,這個包中的屬性就能在當前檔案中去訪問,例如呼叫屬於這個包的函式。
例如,將前面的程式碼改改:
package main
import (
"fmt"
"os"
)
func main (){
if len(os.Args) != 2{
os.Exit(1)
}
fmt.Println("Arg0: ",os.Args[0])
fmt.Println("Arg1: ",os.Args[1])
}
執行一下:
$ go run test.go
exit status 1
$ go run test.go a b
exit status 1
$ go run test.go a
Arg0: /tmp/go-build730099388/command-line-arguments/_obj/exe/test
Arg1: a
上面的import匯入了兩個標準包:fmt
和os
,還使用了另一個內建函式len()
。
len()
函式返回字串的長度、字典的元素個數以及陣列的元素個數。上面使用len()判斷了該Go程式的引數個數必須為2,否則就以狀態碼1退出該程式。看上面的執行結果,好像只有給一個引數的時候才是正確的,這是因為第一個引數(Args[0]
)代表的總是當前正在執行的程式名稱,正如上面結果所顯示的那樣。
你可能還注意到了fmt.Println
,字首fmt
正好是匯入的一個包名,這表示使用fmt包中的Println函式。
本文的開頭就說過了,Go是一門非常嚴格的語言,如果這裡匯入了fmt包,但卻沒有使用它,它將報錯。
# command-line-arguments
./test.go:4:5: imported and not used: "fmt"
關於Go文件
還可以使用go doc
命令去查詢各幫助文件。
例如,檢視fmt包的幫助文件:
go doc fmt
檢視fmt.Println
函式的用法:
go doc fmt.Println
完整用法:
go doc
go doc <pkg>
go doc <sym>[.<method>]
go doc [<pkg>].<sym>[.<method>]
go doc <pkg> <sym>[.<method>]
此外,還可以構建本地的網頁版官方手冊,在斷網的時候可以訪問:
godoc -http=:6060
然後就可以在瀏覽器中通過http://localhost:6060/
訪問官方手冊。
變數和變數宣告
很多語言中,要為變數賦值只需一個語句:
x=10
這個語句中實際上包含了兩個過程:變數的宣告和變數的賦值。宣告一般也被稱為"定義"。
在Go中,必須先宣告變數,再賦值或使用變數。最複雜的宣告+賦值操作為:
package main
import ( "fmt" )
func main(){
var x int
x=10
fmt.Println("x =",x)
}
此處聲明瞭一個變數x,其資料型別為int
。預設情況下,Go在變數的宣告期間會為其做初始化賦值:int型別初始化賦值為0,booleans初始化賦值為false,strings初始化賦值為"",等等。
可以將宣告和賦值操作合併:
var x int = 10
還有一種更方便的宣告+賦值方式:
x := 10
通過這種變數的定義方式,還可以將函式執行結果(返回值)賦值給變數。例如:
func main() {
x := getAdd(10)
}
func getAdd(x int) int {
return x+1
}
:=
在Go中屬於型別推斷操作,它包含了變數宣告和變數賦值兩個過程。
需要注意的是,變數宣告之後不能再次宣告(除非在不同的作用域),之後只能使用=
進行賦值。例如,執行下面的程式碼將報錯:
package main
import ("fmt")
func main(){
x:=10
fmt.Println("x =",x)
x:=11
fmt.Println("x =",x)
}
錯誤如下:
# command-line-arguments
.\test.go:8:3: no new variables on left side of :=
報錯資訊很明顯,:=
左邊沒有新變數。
如果仔細看上面的報錯資訊,會發現no new variables
是一個複數。實際上,Go允許我們使用:=
一次性宣告、賦值多個變數,而且只要左邊有任何一個新變數,語法就是正確的。
func main(){
name,age := "longshuai",23
fmt.Println("name:",name,"age:",age)
// name重新賦值,因為有一個新變數weight
weight,name := 90,"malongshuai"
fmt.Println("name:",name,"weight:",weight)
}
需要注意,name第二次被:=
賦值,Go第一次推斷出該變數的資料型別之後,就不允許:=
再改變它的資料型別,因為只有第一次:=
對name進行宣告,之後所有的:=
對name都只是簡單的賦值操作。
例如,下面將報錯:
weight,name := 90,80
錯誤資訊:
.\test.go:11:14: cannot use 80 (type int) as type string in assignment
另外,變數宣告之後必須使用,否則會報錯,因為Go對規範的要求非常嚴格。例如,下面定義了weight
但卻沒使用:
weight,name := 90,"malongshuai"
fmt.Println("name:",name)
錯誤資訊:
.\test.go:11:2: weight declared and not used
函式定義
Go的函式允許有多個返回值。例如:
// 該函式有一個引數,沒有返回值
func log(message string){
...CODE...
}
// 該函式有兩個引數,一個返回值,返回值的型別為int
func add(a int,b int) int {
...CODE...
}
// 該函式一個引數,兩個返回值,分別是int和bool
func power(name string) (int,bool) {
...CODE...
}
既然函式可以有返回值,就可以將其返回值賦值給變數:
value, exists := power("malongshuai")
if exists == false {
...CODE...
}
有些時候,我們可能並不需要所有的返回值。例如,我們只想要取得power()的第二個返回值。這時可以將不想要的返回值丟給特殊符號下劃線_
,它表示丟棄這部分結果。
_,exists := power("longshuai")
if exists == false {
...CODE...
}
如果函式的引數型別相同,則不同的引數可以共享資料型別。
func (a,b int) int {
...CODE...
}