最近寫了很多Go代碼,可是寫着寫着,仍是容易忘,尤爲是再寫點Python代碼後。因此找了一篇不錯的Golang基礎教程,翻譯一下,時常看看。git
原文連接: 「Learning Go — from zero to hero」 by Milap Neupanegithub
Go是由各類 包 組成的。main包是程序的入口,由它告訴編譯器,這是一個可執行程序,而不是共享包。main包定義以下:golang
package main
Go的工做區是由環境變量GOPATH
決定的。
你能夠在工做區裏爲所欲爲地寫代碼,Go會在GOPATH
或者GOROOT
目錄下搜索包。注:GOROOT
是Go的安裝路徑。json
設置GOPATH
爲你想要的目錄:數組
# export 環境變量 export GOPATH=~/workspace # 進入工做區目錄 cd ~/workspace
在工做區目錄裏建立mian.go
文件。bash
package main import ( "fmt" ) func main(){ fmt.Println("Hello World!") }
咱們使用import
關鍵字來引入一個包。func main
是執行代碼的入口,fmt是Go的內置包,主要用來格式化輸入/輸出。而Println是fnt中的一個打印函數。服務器
想要運行Go程序,有兩種方法。併發
你們都知道,Go是一門編譯型語言,因此在執行以前,咱們須要先編譯它。app
> go build main.go
這個命令會生成二進制可執行文件 main,而後咱們再運行它。ide
> ./main # Hello World!
一個go run
命令就能夠搞定。
go run main.go # Hello World!
注意:你能夠在這個網站執行本文中的代碼。
Go中的變量都是顯式聲明的。Go是靜態語言,所以聲明變量時,就會去檢查變量的類型。
變量聲明有如下三種方式。
# 1) a的默認值爲0 var a int # 2) 聲明並初始化a,a自動賦值爲int var a = 1 # 3) 簡寫聲明 message := "hello world"
還能夠在一行聲明多個變量
var b, c int = 2, 3
Go 支持的數字存儲類型有不少,好比 int
, int8
, int16
, int32
, int64
,uint
, uint8
, uint16
, uint32
, uint64
, uintptr
等等。
字符串類型存儲一個字節序列。使用string
關鍵字來聲明。
布爾型使用bool
聲明。
Go還支持複數類型數據類型,可使用complex64
和complex128
進行聲明。
var a bool = true var b int = 1 var c string = 'hello world' var d float32 = 1.222 var x complex128 = cmplx.Sqrt(-5 + 12i)
數組是包含同一數據類型的元素序列,在聲明時肯定數組長度,所以不能隨意擴展。
數組的聲明方式以下:
var a [5]int
多維數組的聲明方式以下:
var multiD [2][3]int
Go中的數組有必定限制,好比不能修改數組長度、不能添加元素、不能獲取子數組。這時候,更適合使用slice[分片]
這一類型。
分片用於存儲一組元素,容許隨時擴展其長度。分片的聲明相似數組,只是去掉了長度聲明。
var b []int
這行代碼會建立一個 0容量、0長度的分片。也可使用如下代碼 設置分片的容量和長度。
// 初始化一個長度爲5,容量爲10的分片 numbers := make([]int,5,10)
實際上,分片是對數組的抽象。分片使用數組做爲底層結構。一個分片由三部分組成:容量、長度和指向底層數組的指針。
使用append
或者copy
方法能夠擴大分片的容量。append
方法在分片的末尾追加元素,必要時會擴大分片容量。
numbers = append(numbers, 1, 2, 3, 4)
還可使用copy
方法來擴大容量。
// 建立一個更大容量的分片 number2 := make([]int, 15) // 把原分片複製到新分片 copy(number2, number)
如何建立一個分片的子分片呢?參考如下代碼。
// 建立一個長度爲4的分片 number2 = []int{1,2,3,4} fmt.Println(numbers) // -> [1 2 3 4] // 建立子分片 slice1 := number2[2:] fmt.Println(slice1) // -> [3 4] slice2 := number2[:3] fmt.Println(slice2) // -> [1 2 3] slice3 := number2[1:4] fmt.Println(slice3) // -> [2 3 4]
Map也是Go的一種數據類型,用於記錄鍵值間的映射關係。使用如下代碼建立一個map。
var m map[string]int // 新增 鍵/值 m['clearity'] = 2 m['simplicity'] = 3 // 打印值 fmt.Println(m['clearity']) // -> 2 fmt.Println(m['simplicity']) // -> 3
這裏,m是一個鍵爲string,值爲int的map變量。
接下來看一下如何進行簡單的類型轉換。
a := 1.1 b := int(a) fmt.Println(b) //-> 1
並不是全部的數據類型都能轉換成其餘類型。注意:確保數據類型與轉換類型相互兼容。
參考如下代碼中的if-else語句進行條件判斷。注意:花括號與條件語句要在同一行。
if num := 9; num < 0 { fmt.Println(num, "is negative") } else if num < 10 { fmt.Println(num, "has 1 digit") } else { fmt.Println(num, "has multiple digits") }
switch-case用於組織多個條件語句,詳看如下代碼
i := 2 switch i { case 1: fmt.Println("one") case 2: fmt.Println("two") default: fmt.Println("none") }
Go中用於循環的關鍵字只有一個for
。
i := 0 sum := 0 for i < 10 { sum += 1 i++ } fmt.Println(sum)
以上代碼相似於C語言中的while
循環。另外一種循環方式以下:
sum := 0 for i := 0; i < 10; i++ { sum += i } fmt.Println(sum)
Go中的死循環
for { }
Go提供了指針,用於存儲值的地址。指針使用*
來聲明。
var ap *int
這裏的ap變量即指向整型的指針。使用&
運算符獲取變量地址,*
運算符用來獲取指針所指向的值。
a := 12 ap = &a fmt.Println(*ap) // => 12
如下兩種狀況,一般優先選用指針。
好比:
func increment(i *int) { *i++ } func main() { i := 10 increment(&i) fmt.Println(i) } //=> 11
main
包中的main
函數是go程序執行的入口,除此之外,咱們還能夠定義其餘函數。
func add(a int, b int) int { c := a + b return c } func main() { fmt.Println(add(2, 1)) } //=> 3
如上所示,Go中使用func
關鍵字加上函數名來定義一個函數。函數的參數須要指明數據類型,最後是返回的數據類型。
函數的返回值也能夠在函數中提早定義:
func add(a int, b int) (c int) { c = a + b return } func main() { fmt.Println(add(2, 1)) } //=> 3
這裏c被定義爲返回值,所以調用return
語句時,c會被自動返回。
你也能夠一次返回多個變量:
func add(a int, b int) (int, string) { c := a + b return c, "successfully added" } func main() { sum, message := add(2, 1) fmt.Println(message) fmt.Println(sum) }
Go 不是徹底面向對象的語言,可是有了 方法、結構體和接口,它也能夠達到面向對象的效果。
結構體包含不一樣類型的字段,可用來對數據進行分組。例如,若是咱們要對Person類型的數據進行分組,那麼能夠定義一我的的各類屬性,包括姓名,年齡,性別等。
type person struct { name string age int gender string }
有了Person類型後,如今來建立一個 Person對象:
//方法 1: 指定參數和值 p = person{name: "Bob", age: 42, gender: "Male"} //方法 2: 僅指定值 person{"Bob", 42, "Male"}
可使用.
來獲取一個對象的參數。
p.name //=> Bob p.age //=> 42 p.gender //=> Male
也能夠經過結構體的指針對象來獲取參數。
pp = &person{name: "Bob", age: 42, gender: "Male"} pp.name //=> Bob
方法是一種帶有接收器的函數。接收器能夠是一個值或指針。咱們能夠把剛剛建立的Person類型做爲接收器來建立方法:
package main import "fmt" // 定義結構體 type person struct { name string age int gender string } // 定義方法 func (p *person) describe() { fmt.Printf("%v is %v years old.", p.name, p.age) } func (p *person) setAge(age int) { p.age = age } func (p person) setName(name string) { p.name = name } func main() { pp := &person{name: "Bob", age: 42, gender: "Male"} // 使用 . 來調用方法 pp.describe() // => Bob is 42 years old pp.setAge(45) fmt.Println(pp.age) //=> 45 pp.setName("Hari") fmt.Println(pp.name) //=> Bob }
注意,此處的接收器是一個指針,方法中對指針進行的任何修改,均可以反映在接收器pp
上。這樣能夠避免複製帶來的內存消耗。
注意:上面示例中,age
被修改了,而name
不變。由於只有setAge
傳入的是指針類型,能夠對接收器進行修改。
在Go中,接口是方法的集合。接口能夠對一個類型的屬性進行分組,好比:
type animal interface { description() string }
animal
是一個接口。經過實現animal
接口,咱們來建立兩種不一樣類型的動物。
package main import ( "fmt" ) type animal interface { description() string } type cat struct { Type string Sound string } type snake struct { Type string Poisonous bool } func (s snake) description() string { return fmt.Sprintf("Poisonous: %v", s.Poisonous) } func (c cat) description() string { return fmt.Sprintf("Sound: %v", c.Sound) } func main() { var a animal a = snake{Poisonous: true} fmt.Println(a.description()) a = cat{Sound: "Meow!!!"} fmt.Println(a.description()) } //=> Poisonous: true //=> Sound: Meow!!!
在main函數中,咱們建立了一個類型爲animal的變量a。而後,給動物指定蛇和貓的類型,並打印a.description
。
在Go中,全部的代碼都寫在包裏面。main
包是程序執行的入口,Go自帶了不少內置包,最有名的就是剛剛用過的fmt
包。
「Go packages in the main mechanism for programming in the large that go provides and they make possible to divvy up a large project into smaller pieces.」
— Robert Griesemer
go get <package-url-github> // 舉個栗子 go get github.com/satori/go.uuid
包默認安裝在GOPATH
環境變量設置的工做區中。可使用cd $GOPATH/pkg
命令進入目錄,查看已安裝的包。
首先建立一個custom_package
文件夾
> mkdir custom_package > cd custom_package
假設要建立一個person
包,首先在custom_package
目錄下建立一個person
文件夾。
> mkdir person > cd person
而後建立一個 person.go
文件
package person func Description(name string) string { return "The person name is: " + name } func secretName(name string) string { return "Do not share" }
如今須要安裝這個包,以便引入並使用它。
> go install
注意:若是以上命令報錯,確認一下
GO111MODULE
環境變量是否設置正確,參考連接。
而後回到custom_package
目錄下,建立一個main.go
文件。
package main import( "custom_package/person" "fmt" ) func main(){ p := person.Description("Milap") fmt.Println(p) } // => The person name is: Milap
如今,就能夠引入包,並調用Description
方法了。注意,secretName
方法是小寫字母開頭的私有方法,因此不能被外部調用。
Go內置了對包文檔的支持。運行如下命令生成文檔:
go doc person Description
這將爲person
包生成Description
函數的文檔。請使用如下命令運行Web服務器,查看文檔:
godoc -http=":8080"
打開這個連接http://localhost:8080/pkg/,就能看到文檔了。
fmt
包實現了格式化I/O功能。咱們已經使用過這個包打印內容到標準輸出流了。
另一個頗有用的包是json
,用來編碼/解碼Json
數據。
// 編碼 package main import ( "fmt" "encoding/json" ) func main(){ mapA := map[string]int{"apple": 5, "lettuce": 7} mapB, _ := json.Marshal(mapA) fmt.Println(string(mapB)) }
// 解碼 package main import ( "fmt" "encoding/json" ) type response struct { PageNumber int `json:"page"` Fruits []string `json:"fruits"` } func main(){ str := `{"page": 1, "fruits": ["apple", "peach"]}` res := response{} json.Unmarshal([]byte(str), &res) fmt.Println(res.PageNumber) } //=> 1
使用Unmarshal
解碼json字節時,第一個參數是json字節,第二個是指望解碼後的結構體指針。注意:json:"page"
負責把page
映射到結構體中的PageNumber
字段上。
報錯是程序中的意外產物。假如咱們正在使用API
調用一個外部服務。這個API
調用可能成功,也可能失敗。好比,可使用如下方法,處理報錯:
package main import ( "fmt" "net/http" ) func main(){ resp, err := http.Get("http://example.com/") if err != nil { fmt.Println(err) return } fmt.Println(resp) }
在寫函數時,咱們可能會遇到須要報錯的情景,這時能夠返回一個自定義的error
對象。
func Increment(n int) (int, error) { if n < 0 { // return error object return nil, errors.New("math: cannot process negative number") } return (n + 1), nil } func main() { num := 5 if inc, err := Increment(num); err != nil { fmt.Printf("Failed Number: %v, error message: %v", num, err) }else { fmt.Printf("Incremented Number: %v", inc) } }
大部分的內置包或者外部包,都有本身的報錯處理機制。所以咱們使用的任何函數可能報錯,這些報錯都不該該被忽略,應該像上面示例中,在調用函數的地方,優雅地處理報錯。
當程序在運行過程當中,忽然遇到了未處理的報錯,就會致使panic
。在Go中,更推薦使用error
對象,而不是panic
來處理異常。發生panic
後,程序會中止運行,但會運行defer
語句代碼。
//Go package main import "fmt" func main() { f() fmt.Println("Returned normally from f.") } func f() { defer func() { if r := recover(); r != nil { fmt.Println("Recovered in f", r) } }() fmt.Println("Calling g.") g(0) fmt.Println("Returned normally from g.") } func g(i int) { if i > 3 { fmt.Println("Panicking!") panic(fmt.Sprintf("%v", i)) } defer fmt.Println("Defer in g", i) fmt.Println("Printing in g", i) g(i + 1) }
Defer
語句老是在函數最後執行。
在上面的栗子中,咱們觸發了panic
,可是defer
語句依然會在最後執行。Defer
適用於 須要在函數最後執行某些操做的場景,好比關閉文件。
Go在設計時考慮了併發性。 Go中的併發能夠經過輕量級線程Go routines
來實現。
Go routine
是一個函數,它能夠與另外一個函數並行或併發執行。 建立Go routine
很是簡單,只需在函數前面添加關鍵字go
,就可使其並行執行。 同時,它很輕量級,所以能夠建立上千個routine
。
package main import ( "fmt" "time" ) func main() { go c() fmt.Println("I am main") time.Sleep(time.Second * 2) } func c() { time.Sleep(time.Second * 2) fmt.Println("I am concurrent") } //=> I am main //=> I am concurrent
上面的示例中,c函數是一個Go routine
,與main函數中的線程並行。有時咱們想在多個線程之間共享資源。 Go傾向於不與另外一個線程共享變量,由於這會增長死鎖和資源等待的可能。可是仙人自有妙招,就是接下來說到的go channel
。
咱們可使用channel
在兩個routine
之間傳遞數據。建立channel
時,須要指定其接收的數據類型。
c := make(chan string)
經過上面建立的channel
,咱們能夠發送/接收string
類型的數據。
package main import "fmt" func main(){ c := make(chan string) go func(){ c <- "hello" }() msg := <-c fmt.Println(msg) } //=>"hello"
接收方channel
會一直等待發送方發數據到channel
。
在某些場景下,咱們但願Go routine
只接收數據但不發送數據,反之亦然。 這時,咱們能夠建立一個單向channel
。
package main import ( "fmt" ) func main() { ch := make(chan string) go sc(ch) fmt.Println(<-ch) } // sc函數:只能發送數據給 channel,不能接收數據 func sc(ch chan<- string) { ch <- "hello" }
select
語句在Go routine
中處理多個channel
一個函數可能正在等待多個通道。這時,咱們可使用select
語句。
package main import ( "fmt" "time" ) func main() { c1 := make(chan string) c2 := make(chan string) go speed1(c1) go speed2(c2) fmt.Println("The first to arrive is:") select { case s1 := <-c1: fmt.Println(s1) case s2 := <-c2: fmt.Println(s2) } } func speed1(ch chan string) { time.Sleep(2 * time.Second) ch <- "speed 1" } func speed2(ch chan string) { time.Sleep(1 * time.Second) ch <- "speed 2" } // => The first to arrive is: // => speed 2
在Go中,你還可使用緩衝區channel
,若是緩衝區已滿,發送到該channel
的消息將被阻塞。
package main import "fmt" func main(){ ch := make(chan string, 2) ch <- "hello" ch <- "world" ch <- "!" // extra message in buffer fmt.Println(<-ch) } // => fatal error: all goroutines are asleep - deadlock!
爲何 Golang
可以成功呢?
Simplicity… — Rob-pike
由於簡單...
好了,本文終於結束了!你從菜鳥變成大佬了嗎?開個玩笑,但願看完能有所收穫。