
讓我們從 Go(或 Golang)的一個小介紹開始。 Go 由 Google 工程師 Robert Griesemer,Rob Pike 和 Ken Thompson 設計。 它是一種靜态類型的編譯語言。 第一個版本于 2012 年 3 月作為開源釋出。
“Go 是一種開源程式設計語言,可以輕松建構的簡單,可靠,高效的軟體”。 --- 關于 go |
在許多語言中,有許多方法可以解決某些給定的問題。是以 程式員可以花很多時間思考解決問題的最佳方法。
然而,Go 卻是隻有一種正确的方法來解決問題的語言。
這節省了開發人員的時間,并使大型代碼庫易于維護。 Go 中沒有地圖和過濾器等 “富有表現力” 的功能。
“如果你有增加表現力的功能,通常會增加費用” --- Rob Pike |
入門
Go 是由包組成的。 main 包告訴 Go 編譯器該程式可以被編譯成可執行檔案,而不是一個共享的庫。它是應用程式的入口。main 包被定義為如下格式:
package main
接下來,讓我們通過在 Go 工作區中建立一個檔案 main.go 來編寫一個簡單的 hello world 示例。
go的工作區
Go 中的工作空間由環境變量「GOPATH」定義。你寫的任何代碼都将寫在工作區内。Go 将搜尋 GOPATH 目錄中的任何包,或者在安裝 Go 時預設設定的 GOROOT 目錄。 GOROOT 是安裝 go 的路徑。
将 GOPATH 設定為你想要的目錄。 現在,讓我們将它添加到檔案夾〜/ workspace 中。
#寫入 env
export GOPATH=~/workspace
#cd 到工作區目錄\
cd ~/workspace
使用我們剛剛建立的工作空間檔案夾中的以下代碼建立檔案 main.go。
Hello World!
package main
import (
"fmt"
)
func main(){
fmt.Println("Hello World!")
}
在上面的 demo 中, fmt 是 Go 中的内置包,它實作了格式化 I / O 的功能。
在 Go 中我們導入一個包使用 import 關鍵字func main 是代碼執行的入口。Println 是 fmt 包中的一個函數,它為我們列印 “hello world”。
讓我們看一下運作這個檔案。 我們可以通過兩種方式運作 Go 指令。 我們知道,Go 是一種編譯語言,是以我們首先需要在執行之前編譯它。
> go build main.go
這會建立一個二進制可執行檔案 main,現在我們可以運作它:
> ./main
# Hello World!
還有另一種更簡單的方法來運作程式。 go run 指令有助于抽象編譯步驟。 您隻需運作以下指令即可執行該程式。
go run main.go
# Hello World!
您可以使用
https://play.golang.org來運作本文提到的代碼。
變量
變量在 Go 語言中是一個很明确的定義。 Go 是一種靜态類型的語言。這意味着在聲明變量時我們就需要明确變量的類型。一般一個變量的定義如下:
var a int
上面的執行個體中,我們定義了一個 int 類型的變量 a ,預設會被指派成 0 。使用以下文法可以初始化改變變量的值:
var a = 1
這裡我們沒有制定變量 a 的類型,在我們給它初始化為 1 時,它就自動被定義成了 int 類型的變量。
我們也可以使用一種更簡短的文法來定義它:
message := "hello world"
我們也可以在同一行聲明多個同類型變量:
var b, c int = 2, 3
資料類型
跟任何其他程式設計語言一樣,Go 語言支援各種不同的資料結構。 讓我們探讨一下:
Number, String, and Boolean
一些受支援的 number 存儲類型是:int, int8, int16, int32, int64,uint, uint8, uint16, uint32, uint64, uintptr...
string 類型存儲一些列的位元組。 它用關鍵字string 表示和聲明。
boolean 使用關鍵字 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)
數組,切片,以及 Maps
數組是相同資料類型的元素序列。 數組在聲明中定義要指定長度,是以不能進行擴充。 數組聲明為:
var a [5]int
數組也可以是多元的。 我們可以使用以下格式建立它們:
var multiD [2][3]int
當數組的值在運作時不能進行更改。 數組也不提供擷取子數組的能力。 為此,Go 有一個名為切片的資料類型。
切片存儲一系列元素,可以随時擴充。 切片聲明類似于數組聲明 --- 沒有定義容量:
var b []int
這将建立一個零容量和零長度的切片。 切片也可以定義容量和長度。 我們可以使用以下文法:
numbers := make([]int,5,10)
這裡,切片的初始長度為 5,容量為 10。
切片是數組的抽象。 切片使用數組作為底層結構。 切片包含三個元件:容量,長度和指向底層數組的指針,如下圖所示:
圖檔位址:
https://blog.golang.org/go-slices-usage-an...通過使用 append 或 copy 函數可以增加切片的容量。 append 函數可以為數組的末尾增加值,并在需要時增加容量。
numbers = append(numbers, 1, 2, 3, 4)
增加切片容量的另一種方法是使用複制功能。 隻需建立另一個具有更大容量的切片,并将原始切片複制到新建立的切片:
// 建立切片
number2 := make([]int, 15)
// 将原始切片複制到新切片
copy(number2, numbers)
我們可以建立切片的子切片。 這可以使用以下指令完成:
// 初始化長度為 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 的一種 Key-Value 類型的資料結構,我們可以通過下面的指令聲明一個 map :
m := make(map[string]int)
m 是 一個 Key 類型為 string、Value 類型為 int 的 map 類型的變量。我們可以很容易地添加鍵值對到 map 中:
// adding key/value
m["clearity"] = 2
m["simplicity"] = 3
// printing the values
fmt.Println(m["clearity"]) // -> 2
fmt.Println(m["simplicity"]) // -> 3
類型轉化
通過類型轉化,能将一種類型轉為另一種類型。讓我們來看一個簡單的例子:
a := 1.1
b := int(a)
fmt.Println(b)
//-> 1
并不是所有類型都可以轉為另一種類型。需要確定資料類型是可以轉化的。
流程控制
if else
對于流程控制,我們可以使用 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
Switch cases 有助于組織多個條件語句。 以下示例顯示了一個簡單的 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 循環。對于 for 循環,可以使用相同的 for 語句
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
Note: 當你在部落格中嘗試示例代碼時,不要忘記将其包含在 main 包中,并在需要時導入 fmt 或其他包,如上面第一個 main.go 示例所示。
函數
main 函數 定義在 main 包中,是程式執行的入口。可以定義和使用更多功能。 讓我們看一個簡單的例子:
func add(a int, b int) int {
c := a + b
return c
}
func main() {
fmt.Println(add(2, 1))
}
//=> 3
上面的例子中可以看到,使用 func 關鍵字後面跟函數名定義 Go 的函數
函數的傳回值也可以在函數中預先定義:
func add(a int, b int) (c int) {
c = a + b
return
}
func main() {
fmt.Println(add(2, 1))
}
//=> 3
這裡 c 被定義為傳回變量。 是以,定義的變量 c 将自動傳回,而無需在結尾的 return 語句中再次定義。
你還可以從單個函數傳回多個傳回值,将傳回值與逗号分隔開。
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 類型的資料進行分組,我們會定義一個 person 的屬性,其中可能包括姓名,年齡,性别。 可以使用以下文法定義結構:
type person struct {
name string
age int
gender string
}
在定義了 person 結構體的情況下,現在讓我們建立一個 person 執行個體 p:
//方式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
方法
方法是一個特殊類型的帶有傳回值的函數。傳回值既可以是值,也可以是指針。讓我們建立一個名為 describe 的方法,它具有我們在上面的例子中建立的 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.describe 這樣。請注意,傳回值是指針類型。使用指針,我們傳遞對值的引用,是以如果我們對方法進行任何更改,它将反映在傳回值 pp 中。指針類型的傳回值也不會建立對象的新副本,進而節省了記憶體。
請注意,在上面的示例中,age 的值已更改,而 name 的值不會改變。因為方法 setName 是傳回值是值類型,而 setAge 方法的傳回值是類型指針。
接口
Go 的接口是一系列方法的集合。接口有助于将類型的屬性組合在一起。下面,我們以接口 animal 為例:
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。我們為 animal 接口指定了 snake 和 cat 兩個執行個體對象,并使用 Println 方法列印 a.description 。
包
我們所有用 go 語言寫的代碼都是在包含在對應的包中。 main 包是程式執行的入口。Go 中有很多内置包。 我們使用的一個最常見的包是 fmt 包
「Go 的包主要是用來進行大規模程式設計,并且可以将大型項目分成更小的部分。」 --- Robert Griesemer |
包的安裝
go get <package-url-github>
// 例子
go get [github.com/satori/go.uuid](https://github.com/satori/go.uuid)
我們安裝的包儲存在環境變量 env 的 GOPATH 目錄下,這是我們的工作目錄。 你可以通過我們的工作目錄 cd $GOPATH/pkg 中的 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
現在,我們回到 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
至此,我們已經可以引入建立的 person 包了,并且使用包中的 Description 方法。注意,我們在包中建立的 secretName 方法是無法被通路的。在 Go 語言中,方法名稱為非大寫字母開頭的,即為私有方法。
封包檔
Go 擁有内建的封包檔支援功能。運作如下指令生成文檔。
godoc person Description
它将會為我們的 person 包内部的 Description 函數生成文檔。檢視文檔的話隻需要使用如下指令啟動一個 web 伺服器就可以:
godoc -http=":8080"
現在去通路這個URL
http://localhost:8080/pkg/然後你就可以看到我們剛建立的封包檔了。
Go 中部分常見的内置包
fmt
fmt 包實作了格式化 I/O 的功能。我們可以使用這個包來列印到标準輸出。
json
Go 中另外一個有用的常見的包就是 json 包,用來編碼和解碼 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 映射到的響應類型 struct 的位址。 請注意,json:"page" 将 page 的鍵映射到結構體中的 PageNumber 的鍵。
錯誤處理
錯誤是程式不希望出現的意外結果。假設我們正在對一個外部服務的 API 進行調用。 當然,API 調用可能會成功或者失敗。當出現錯的時候,Go 語言可以識别程式中的錯誤。 我們來看看這個例子:
resp, err := http.Get("http://example.com/")
這裡的 API 調用傳回的錯誤對象可能存在或者不存在。 我們可以檢查錯誤是否為 nil 值,并相應地處理響應:
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 {
// 傳回一個 error 對象
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)
}
}
在 Go 中内置的包或我們使用的外部的包都有一個錯誤處理機制。因為我們調用的任何函數都可能存在錯誤。而且這些錯誤永遠不應該被忽略,并且總是在我們稱之為函數的地方優雅地處理,就像我們在上面的例子中所做的那樣。
Panic
panic 是在程式執行期間突然遇到,未經處理的異常。 在 Go 中,panc 不是處理程式中異常的理想方式。 建議使用 error 對象。 發生 panic 時,程式執行停止。 panic 之後要繼續執行的程式就使用 defer。
Defer
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)
}
在上面的例子中,我們使用 panic()來執行程式。 正如你所注意到的一樣,有一個延遲語句,它将使程式在程式執行結束時執行該行。 當我們需要在函數結束時執行某些操作時,也可以使用 Defer,例如關閉檔案。
并發
Go 是建立在并發的基礎上的。Go 中的并發可以通過輕量級線程的 Go routine 來實作。
Go routine
Go routine 是可以與另一個函數并行或并發的函數。 建立 Go routine 非常簡單。 隻需在函數前面添加關鍵字 Go,我們就可以使它并行執行。 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,它與 Go 程式的主線程并行執行。 有時我們希望在多個線程之間共享資源。 Go 不是将一個線程的變量與另一個線程共享,因為這會增加死鎖和資源等待的可能性。 還有另一種在 Go routine 之間共享資源的方法:通過 Go 語言的通道。
通道
我們可以使用通道在兩個 Go routine 之間傳遞資料。 在建立通道時,必須指定通道接收的資料類型。 讓我們建立一個 string 類型的簡單通道,如下所示:
c := make(chan string)
有了這個通道,我們可以發送 string 類型資料。 我們都可以在此通道中發送和接收資料:
package main
import "fmt"
func main(){
c := make(chan string)
go func(){ c <- "hello" }()
msg := <-c
fmt.Println(msg)
}
//=>"hello"
接收方通道将會一直等待發送方向通道發送資料。
單向通道
在某些情況下,我們希望 Go routine 通過通道接收資料但不發送資料,反之亦然。 為此,我們還可以建立單向通道。 讓我們看一個簡單的例子:
package main
import (
"fmt"
)
func main() {
ch := make(chan string)
go sc(ch)
fmt.Println(<-ch)
}
func sc(ch chan<- string) {
ch <- "hello"
}
在上面的例子中,sc 是一個 Go routine,它隻能向通道發送消息但不能接收消息。
使用 select 為 Go routine 處理多個通道
一個程序裡面可能有多個通道正在等待。為此,我們可以使用 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"
}
在上面的示例中,main 方法正在等待讀取 c1 和 c2 通道的資料。 使用 select case 語句列印出結果,消息會通過通道發送過來,會列印出先發送過來的消息。
緩沖通道
有些情況下,我們需要像一個通道發送多個資料。 你可以為此建立一個緩沖通道。使用緩沖通道, 在緩沖區滿之前接受方不會收到任何消息。 讓我們看一下這個例子:
package main
import "fmt"
func main(){
ch := make(chan string, 2)
ch <- "hello"
ch <- "world"
fmt.Println(<-ch)
}
恭喜你!!! 你現在對 Go 有了不錯的認識。
不要止步于此,繼續前進, 考慮一個小的應用程式并開始建構它吧!!!