GO-C-Java-student

2020-08-11 21:13:23

 

1. 介紹與安裝

Golang 是什麼

Go 亦稱爲 Golang(按照 Rob Pike 說法,語言叫做 Go,Golang 只是官方網站的網址),是由谷歌開發的一個開源的編譯型的靜態語言。

Golang 的主要關注點是使得高可用性和可延伸性的 Web 應用的開發變得簡便容易。(Go 的定位是系統程式語言,只是對 Web 開發支援較好)

爲何選擇 Golang

既然有很多其他程式語言可以做同樣的工作,如 Python,Ruby,Nodejs 等,爲什麼要選擇 Golang 作爲伺服器端程式語言?

以下是我使用 Go 語言時發現的一些優點:

  • 併發是語言的一部分(並非通過標準庫實現),所以編寫多執行緒程式會是一件很容易的事。後續教學將會討論到,併發是通過 Goroutines 和 channels 機制 機製實現的。
  • Golang 是一種編譯型語言。原始碼會編譯爲二進制機器碼。而在直譯語言中沒有這個過程,如 Nodejs 中的 JavaScript。
  • 語言規範十分簡潔。所有規範都在一個頁面展示,你甚至都可以用它來編寫你自己的編譯器呢。
  • Go 編譯器支援靜態鏈接。所有 Go 程式碼都可以靜態鏈接爲一個大的二進制檔案(相對現在的磁碟空間,其實根本不大),並可以輕鬆部署到雲伺服器,而不必擔心各種依賴性。

安裝

Golang 支援三個平臺:Mac,Windows 和 Linux(譯註:不只是這三個,也支援其他主流平臺)。你可以在 golang.org/dl/中下載相應平臺的二進制檔案。(因爲衆所周知的原因,如果下載不了,請到 studygolang.com/dl下載)

Mac OS

在 golang.org/dl/下載安裝程式。雙擊開始安裝並且遵循安裝提示,會將 Golang 安裝到 /usr/local/go目錄下,同時 /usr/local/go/bin資料夾也會被新增到 PATH環境變數中。

Windows

在 golang.org/dl/下載 MSI 安裝程式。雙擊開始安裝並且遵循安裝提示,會將 Golang 安裝到 C:\Go目錄下,同時 c:\Go\bin目錄也會被新增到你的 PATH環境變數中。

Linux

在 golang.org/dl/下載 tar 檔案,並解壓到 /usr/local

請新增 /usr/local/go/bin到 PATH環境變數中。Go 就已經成功安裝在 Linux上了。

2. Hello World

建立 Go 工作區

在編寫程式碼之前,我們首先應該建立 Go 的工作區(Workspace)。

在 Mac 或 Linux操作系統下,Go 工作區應該設定在 HOME/go**。所以我们要在 **HOME目錄下建立 go目錄。

而在 Windows下,工作區應該設定在 C:\Users\YourName\go。所以請將 go目錄放置在 C:\Users\YourName

其實也可以通過設定 GOPATH 環境變數,用其他目錄來作爲工作區。但爲了簡單起見,我們採用上面提到的放置方法。

所有 Go 原始檔都應該放置在工作區裡的 src目錄下。請在剛新增的 go目錄下面 下麪建立目錄 src

所有 Go 專案都應該依次在 src 裏面設定自己的子目錄。我們在 src 裏面建立一個目錄 hello來放置整個 hello world 專案。

建立上述目錄之後,其目錄結構如下:

go
  src
    hello
複製程式碼

在我們剛剛建立的 hello 目錄下,在 helloworld.go檔案裡儲存下面 下麪的程式。

package main

import "fmt"

func main() {  
    fmt.Println("Hello World")
}
複製程式碼

建立該程式之後,其目錄結構如下:

go
  src
    hello
      helloworld.go
複製程式碼

執行 Go 程式

執行 Go 程式有多種方式,我們下面 下麪依次介紹。

1.使用 go run命令 - 在命令提示字元旁,輸入 go run workspacepath/src/hello/helloworld.go

上述命令中的 workspacepath應該替換爲你自己的工作區路徑(Windows 下的 C:/Users/YourName/go,Linux 或 Mac 下的 $HOME/go)。

在控制檯上會看見 Hello World的輸出。

2.使用 go install命令 - 執行 go install hello,接着可以用 workspacepath/bin/hello來執行該程式。

上述命令中的 workspacepath應該替換爲你自己的工作區路徑(Windows 下的 C:/Users/YourName/go,Linux 或 Mac 下的 $HOME/go)。

當你輸入 go install hello時,go 工具會在工作區中搜尋 hello 包(hello 稱之爲包,我們後面會更加詳細地討論包)。接下來它會在工作區的 bin 目錄下,建立一個名爲 hello(Windows 下名爲 hello.exe)的二進制檔案。執行 go install hello後,其目錄結構如下所示:

go
  bin
    hello
  src
    hello
      helloworld.go
複製程式碼

3.第 3 種執行程式的好方法是使用 go playground。儘管它有自身的限制,但該方法對於執行簡單的程式非常方便。我已經在 playground 上建立了一個 hello world 程式。點選這裏線上執行程式。 你可以使用 go playground與其他人分享你的原始碼。

簡述 hello world 程式

下面 下麪就是我們剛寫下的 hello world 程式。

package main //1

import "fmt" //2

func main() { //3  
    fmt.Println("Hello World") //4
}
複製程式碼

現在簡單介紹每一行大概都做了些什麼,在以後的教學中還會深入探討每個部分。

package main - 每一個 Go 檔案都應該在開頭進行 package name 的宣告(譯註:只有可執行程式的包名應當爲 main)。包(Packages)用於程式碼的封裝與重用,這裏的包名稱是main

import "fmt"- 我們引入了 fmt 包,用於在 main 函數裏面列印文字到標準輸出。

func main()- main 是一個特殊的函數。整個程式就是從 main 函數開始執行的。main 函數必須放置在 main 包中{和 }分別表示 main 函數的開始和結束部分。

fmt.Println("Hello World")fmt包中的 Println函數用於把文字寫入標準輸出。

3. 變數

變數是什麼

變數指定了某儲存單元(Memory Location)的名稱,該儲存單元會儲存特定型別的值。在 Go 中,有多種語法用於宣告變數。

宣告單個變數

var name type是宣告單個變數的語法。

package main

import "fmt"

func main() {
    var age int // 變數宣告
    fmt.Println("my age is", age)
}
複製程式碼

語句 var age int宣告瞭一個 int 型別的變數,名字爲 age。我們還沒有給該變數賦值。如果變數未被賦值,Go 會自動地將其初始化,賦值該變數型別的零值(Zero Value)。本例中 age 就被賦值爲 0。如果你執行該程式,你會看到如下輸出:

my age is 0
複製程式碼

變數可以賦值爲本型別的任何值。上一程式中的 age 可以賦值爲任何整型值(Integer Value)。

package main

import "fmt"

func main() {
    var age int // 變數宣告
    fmt.Println("my age is", age)
    age = 29 // 賦值
    fmt.Println("my age is", age)
    age = 54 // 賦值
    fmt.Println("my new age is", age)
}
複製程式碼

上面的程式會有如下輸出:

my age is  0  
my age is 29  
my new age is 54
複製程式碼

宣告變數並初始化

宣告變數的同時可以給定初始值。 var name type = initialvalue的語法用於宣告變數並初始化。

package main

import "fmt"

func main() {
    var age int = 29 // 宣告變數並初始化

    fmt.Println("my age is", age)
}
複製程式碼

在上面的程式中,age 是具有初始值 29 的 int 型別變數。如果你執行上面的程式,你可以看見下面 下麪的輸出,證實 age 已經被初始化爲 29。

my age is 29
複製程式碼

型別推斷(Type Inference)

如果變數有初始值,那麼 Go 能夠自動推斷具有初始值的變數的型別。因此,如果變數有初始值,就可以在變數宣告中省略 type

如果變數宣告的語法是 var name = initialvalue,Go 能夠根據初始值自動推斷變數的型別。

在下面 下麪的例子中,你可以看到在第 6 行,我們省略了變數 age的 int型別,Go 依然推斷出了它是 int 型別。

package main

import "fmt"

func main() {
    var age = 29 // 可以推斷型別

    fmt.Println("my age is", age)
}
複製程式碼

宣告多個變數

Go 能夠通過一條語句宣告多個變數。

宣告多個變數的語法是 var name1, name2 type = initialvalue1, initialvalue2

package main

import "fmt"

func main() {
    var width, height int = 100, 50 // 宣告多個變數

    fmt.Println("width is", width, "height is", heigh)
}
複製程式碼

上述程式將在標準輸出列印 width is 100 height is 50

你可能已經想到,如果 width 和 height 省略了初始化,它們的初始值將賦值爲 0。

package main

import "fmt"

func main() {  
    var width, height int
    fmt.Println("width is", width, "height is", height)
    width = 100
    height = 50
    fmt.Println("new width is", width, "new height is ", height)
}
複製程式碼

上面的程式將會列印:

width is 0 height is 0  
new width is 100 new height is  50
複製程式碼

在有些情況下,我們可能會想要在一個語句中宣告不同類型的變數。其語法如下:

var (  
    name1 = initialvalue1,
    name2 = initialvalue2
)
複製程式碼

使用上述語法,下面 下麪的程式宣告不同類型的變數。

package main

import "fmt"

func main() {
    var (
        name   = "naveen"
        age    = 29
        height int
    )
    fmt.Println("my name is", name, ", age is", age, "and height is", height)
}
複製程式碼

這裏我們宣告瞭 string 型別的 name、int 型別的 age 和 height(我們將會在下一教學中討論 golang 所支援的變數型別)。執行上面的程式會產生輸出 my name is naveen , age is 29 and height is 0

簡短宣告

Go 也支援一種宣告變數的簡潔形式,稱爲簡短宣告(Short Hand Declaration),該宣告使用了 :=操作符。

宣告變數的簡短語法是 name := initialvalue

package main

import "fmt"

func main() {  
    name, age := "naveen", 29 // 簡短宣告

    fmt.Println("my name is", name, "age is", age)
}
複製程式碼

執行上面的程式,可以看到輸出爲 my name is naveen age is 29

簡短宣告要求 :=操作符左邊的所有變數都有初始值。下面 下麪程式將會拋出錯誤 cannot assign 1 values to 2 variables,這是因爲 age 沒有被賦值

package main

import "fmt"

func main() {  
    name, age := "naveen" //error

    fmt.Println("my name is", name, "age is", age)
}
複製程式碼

簡短宣告的語法要求 :=操作符的左邊至少有一個變數是尚未宣告的。考慮下面 下麪的程式:

package main

import "fmt"

func main() {
    a, b := 20, 30 // 宣告變數a和b
    fmt.Println("a is", a, "b is", b)
    b, c := 40, 50 // b已經宣告,但c尚未宣告
    fmt.Println("b is", b, "c is", c)
    b, c = 80, 90 // 給已經宣告的變數b和c賦新值
    fmt.Println("changed b is", b, "c is", c)
}
複製程式碼

在上面程式中的第 8 行,由於 b 已經被宣告,而 c 尚未宣告,因此執行成功並且輸出:

a is 20 b is 30  
b is 40 c is 50  
changed b is 80 c is 90
複製程式碼

但是如果我們執行下面 下麪的程式:

package main

import "fmt"

func main() {  
    a, b := 20, 30 // 宣告a和b
    fmt.Println("a is", a, "b is", b)
    a, b := 40, 50 // 錯誤,沒有尚未宣告的變數
}

複製程式碼

上面執行後會拋出 no new variables on left side of :=的錯誤,這是因爲 a 和 b 的變數已經宣告過了,:=的左邊並沒有尚未宣告的變數。

變數也可以在執行時進行賦值。考慮下面 下麪的程式:

package main

import (  
    "fmt"
    "math"
)

func main() {  
    a, b := 145.8, 543.8
    c := math.Min(a, b)
    fmt.Println("minimum value is ", c)
}

複製程式碼

在上面的程式中,c 的值是執行過程中計算得到的,即 a 和 b 的最小值。上述程式會列印:

minimum value is  145.8

複製程式碼

由於 Go 是強型別(Strongly Typed)語言,因此不允許某一型別的變數賦值爲其他型別的值。下面 下麪的程式會拋出錯誤 cannot use "naveen" (type string) as type int in assignment,這是因爲 age 本來宣告爲 int 型別,而我們卻嘗試給它賦字串型別的值。

package main

func main() {  
    age := 29      // age是int型別
    age = "naveen" // 錯誤,嘗試賦值一個字串給int型別變數
}

複製程式碼

4. 型別

下面 下麪是 Go 支援的基本型別:

  • bool
  • 數位型別
    • int8, int16, int32, int64, int
    • uint8, uint16, uint32, uint64, uint
    • float32, float64
    • complex64, complex128
    • byte
    • rune
  • string

bool

bool 型別表示一個布爾值,值爲 true 或者 false。

package main

import "fmt"

func main() {  
    a := true
    b := false
    fmt.Println("a:", a, "b:", b)
    c := a && b
    fmt.Println("c:", c)
    d := a || b
    fmt.Println("d:", d)
}
複製程式碼

在上面的程式中,a 賦值爲 true,b 賦值爲 false。

c 賦值爲 a && b。僅當 a 和 b 都爲 true 時,操作符 && 才返回 true。因此,在這裏 c 爲 false。

當 a 或者 b 爲 true 時,操作符 || 返回 true。在這裏,由於 a 爲 true,因此 d 也爲 true。我們將得到程式的輸出如下。

a: true b: false  
c: false  
d: true
複製程式碼

有符號整型

int8:表示 8 位有符號整型大小:8 位範圍:-128~127

int16:表示 16 位有符號整型大小:16 位範圍:-32768~32767

int32:表示 32 位有符號整型大小:32 位範圍:-2147483648~2147483647

int64:表示 64 位有符號整型大小:64 位範圍:-9223372036854775808~9223372036854775807

int:根據不同的底層平臺(Underlying Platform),表示 32 或 64 位整型。除非對整型的大小有特定的需求,否則你通常應該使用 int表示整型。大小:在 32 位系統下是 32 位,而在 64 位系統下是 64 位。範圍:在 32 位系統下是 -2147483648~2147483647,而在 64 位系統是 -9223372036854775808~9223372036854775807。

package main

import "fmt"

func main() {  
    var a int = 89
    b := 95
    fmt.Println("value of a is", a, "and b is", b)
}
複製程式碼

線上執行程式

上面程式會輸出 value of a is 89 and b is 95

在上述程式中,a 是 int 型別,而 b 的型別通過賦值(95)推斷得出。上面我們提到,int 型別的大小在 32 位系統下是 32 位,而在 64 位系統下是 64 位。接下來我們會證實這種說法。

在 Printf 方法中,使用 %T格式說明符(Format Specifier),可以列印出變數的型別。Go 的 unsafe包提供了一個 Sizeof函數,該函數接收變數並返回它的位元組大小。unsafe包應該小心使用,因爲使用 unsafe 包可能會帶來可移植性問題。不過出於本教學的目的,我們是可以使用的。

下面 下麪程式會輸出變數 a 和 b 的型別和大小。格式說明符 %T用於列印型別,而 %d用於列印位元組大小。

package main

import (  
    "fmt"
    "unsafe"
)

func main() {  
    var a int = 89
    b := 95
    fmt.Println("value of a is", a, "and b is", b)
    fmt.Printf("type of a is %T, size of a is %d", a, unsafe.Sizeof(a)) // a 的型別和大小
    fmt.Printf("\ntype of b is %T, size of b is %d", b, unsafe.Sizeof(b)) // b 的型別和大小
}
複製程式碼

線上執行程式

以上程式會輸出:

value of a is 89 and b is 95  
type of a is int, size of a is 4  
type of b is int, size of b is 4
複製程式碼

從上面的輸出,我們可以推斷出 a 和 b 爲 int型別,且大小都是 32 位(4 位元組)。如果你在 64 位系統上執行上面的程式碼,會有不同的輸出。在 64 位系統下,a 和 b 會佔用 64 位(8 位元組)的大小。

無符號整型

uint8:表示 8 位無符號整型大小:8 位範圍:0~255

uint16:表示 16 位無符號整型大小:16 位範圍:0~65535

uint32:表示 32 位無符號整型大小:32 位範圍:0~4294967295

uint64:表示 64 位無符號整型大小:64 位範圍:0~18446744073709551615

uint:根據不同的底層平臺,表示 32 或 64 位無符號整型。大小:在 32 位系統下是 32 位,而在 64 位系統下是 64 位。範圍:在 32 位系統下是 0~4294967295,而在 64 位系統是 0~18446744073709551615。

浮點型

float32:32 位浮點數float64:64 位浮點數

下面 下麪一個簡單程式演示了整型和浮點型的運用。

package main

import (  
    "fmt"
)

func main() {  
    a, b := 5.67, 8.97
    fmt.Printf("type of a %T b %T\n", a, b)
    sum := a + b
    diff := a - b
    fmt.Println("sum", sum, "diff", diff)

    no1, no2 := 56, 89
    fmt.Println("sum", no1+no2, "diff", no1-no2)
}
複製程式碼

a 和 b 的型別根據賦值推斷得出。在這裏,a 和 b 的型別爲 float64(float64 是浮點數的預設型別)。我們把 a 和 b 的和賦值給變數 sum,把 b 和 a 的差賦值給 diff,接下來列印 sum 和 diff。no1 和 no2 也進行了相同的計算。上述程式將會輸出:

type of a float64 b float64  
sum 14.64 diff -3.3000000000000007  
sum 145 diff -33
複製程式碼

複數型別

complex64:實部和虛部都是 float32 型別的的複數。complex128:實部和虛部都是 float64 型別的的複數。

內建函數 complex用於建立一個包含實部和虛部的複數。complex 函數的定義如下:

func complex(r, i FloatType) ComplexType
複製程式碼

該函數的參數分別是實部和虛部,並返回一個複數型別。實部和虛部應該是相同類型,也就是 float32 或 float64。如果實部和虛部都是 float32 型別,則函數會返回一個 complex64 型別的複數。如果實部和虛部都是 float64 型別,則函數會返回一個 complex128 型別的複數。

還可以使用簡短語法來建立複數:

c := 6 + 7i
複製程式碼

下面 下麪我們編寫一個簡單的程式來理解複數。

package main

import (  
    "fmt"
)

func main() {  
    c1 := complex(5, 7)
    c2 := 8 + 27i
    cadd := c1 + c2
    fmt.Println("sum:", cadd)
    cmul := c1 * c2
    fmt.Println("product:", cmul)
}
複製程式碼

在上面的程式裡,c1 和 c2 是兩個複數。c1的實部爲 5,虛部爲 7。c2 的實部爲8,虛部爲 27。c1 和 c2 的和賦值給 cadd,而 c1 和 c2 的乘積賦值給 cmul。該程式將輸出:

sum: (13+34i)  
product: (-149+191i)
複製程式碼

其他數位型別

byte是 uint8 的別名。rune是 int32 的別名。

在學習字串的時候,我們會詳細討論 byte 和 rune。

string 型別

在 Golang 中,字串是位元組的集合。如果你現在還不理解這個定義,也沒有關係。我們可以暫且認爲一個字串就是由很多字元組成的。我們後面會在一個教學中深入學習字串。 下面 下麪編寫一個使用字串的程式。

package main

import (  
    "fmt"
)

func main() {  
    first := "Naveen"
    last := "Ramanathan"
    name := first +" "+ last
    fmt.Println("My name is",name)
}
複製程式碼

上面程式中,first 賦值爲字串 "Naveen",last 賦值爲字串 "Ramanathan"。+ 操作符可以用於拼接字串。我們拼接了 first、空格和 last,並將其賦值給 name。上述程式將列印輸出 My name is Naveen Ramanathan

還有許多應用於字串上面的操作,我們將會在一個單獨的教學裡看見它們。

型別轉換

Go 有着非常嚴格的強型別特徵。Go 沒有自動型別提升或型別轉換。我們通過一個例子說明這意味着什麼。

package main

import (  
    "fmt"
)

func main() {  
    i := 55      //int
    j := 67.8    //float64
    sum := i + j //不允許 int + float64
    fmt.Println(sum)
}
複製程式碼

上面的程式碼在 C 語言中是完全合法的,然而在 Go 中,卻是行不通的。i 的型別是 int ,而 j 的型別是 float64 ,我們正試圖把兩個不同類型的數相加,Go 不允許這樣的操作。如果執行程式,你會得到 main.go:10: invalid operation: i + j (mismatched types int and float64)

要修復這個錯誤,i 和 j 應該是相同的型別。在這裏,我們把 j 轉換爲 int 型別。把 v 轉換爲 T 型別的語法是 T(v)。

package main

import (  
    "fmt"
)

func main() {  
    i := 55      //int
    j := 67.8    //float64
    sum := i + int(j) //j is converted to int
    fmt.Println(sum)
}
複製程式碼

現在,當你執行上面的程式時,會看見輸出 122

賦值的情況也是如此。把一個變數賦值給另一個不同類型的變數,需要顯式的型別轉換。下面 下麪程式說明了這一點。

package main

import (  
    "fmt"
)

func main() {  
    i := 10
    var j float64 = float64(i) // 若沒有顯式轉換,該語句會報錯
    fmt.Println("j", j)
}
複製程式碼

在第 9 行,i 轉換爲 float64 型別,接下來賦值給 j。如果不進行型別轉換,當你試圖把 i 賦值給 j 時,編譯器會拋出錯誤。

5. 常數

定義

在 Go 語言中,術語"常數"用於表示固定的值。比如 5-89、 I love Go67.89等等。

看看下面 下麪的程式碼:

var a int = 50  
var b string = "I love Go"
複製程式碼

在上面的程式碼中,變數 a 和 b 分別被賦值爲常數 50 和 I love GO。關鍵字 const被用於表示常數,比如 50和 I love Go。即使在上面的程式碼中我們沒有明確的使用關鍵字 const,但是在 Go 的內部,它們是常數。

顧名思義,常數不能再重新賦值爲其他的值。因此下面 下麪的程式將不能正常工作,它將出現一個編譯錯誤: cannot assign to a.

package main

func main() {  
    const a = 55 // 允許
    a = 89       // 不允許重新賦值
}
複製程式碼

常數的值會在編譯的時候確定。因爲函數呼叫發生在執行時,所以不能將函數的返回值賦值給常數。

package main

import (  
    "fmt"
    "math"
)

func main() {  
    fmt.Println("Hello, playground")
    var a = math.Sqrt(4)   // 允許
    const b = math.Sqrt(4) // 不允許
}
複製程式碼

在上面的程式中,因爲 a是變數,因此我們可以將函數 math.Sqrt(4)的返回值賦值給它(我們將在單獨的地方詳細討論函數)。

b是一個常數,它的值需要在編譯的時候就確定。函數 math.Sqrt(4)只會在執行的時候計算,因此 const b = math.Sqrt(4)將會拋出錯誤 error main.go:11: const initializer math.Sqrt(4) is not a constant)

字串常數

雙引號中的任何值都是 Go 中的字串常數。例如像 Hello World或 Sam等字串在 Go 中都是常數。

什麼型別的字串屬於常數?答案是他們是無型別的。

像 Hello World這樣的字串常數沒有任何型別。

const hello = "Hello World"
複製程式碼

上面的例子,我們把 Hello World分配給常數 hello。現在常數 hello有型別嗎?答案是沒有。常數仍然沒有型別。

Go 是一門強型別語言,所有的變數必須有明確的型別。那麼, 下面 下麪的程式是如何將無型別的常數 Sam賦值給變數 name的呢?

package main

import (  
    "fmt"
)

func main() {  
    var name = "Sam"
    fmt.Printf("type %T value %v", name, name)

}
複製程式碼

答案是無型別的常數有一個與它們相關聯的預設型別,並且當且僅當一行程式碼需要時才提供它。在宣告中 var name = "Sam" , name 需要一個型別,它從字串常數 Sam 的預設型別中獲取。

有沒有辦法建立一個帶型別的常數?答案是可以的。以下程式碼建立一個有型別常數。

const typedhello string = "Hello World"
複製程式碼

上面程式碼中, typedhello就是一個 string型別的常數。

Go 是一個強型別的語言,在分配過程中混合型別是不允許的。讓我們通過以下程式看看這句話是什麼意思。

package main

func main() {  
        var defaultName = "Sam" // 允許
        type myString string
        var customName myString = "Sam" // 允許
        customName = defaultName // 不允許

}
複製程式碼

在上面的程式碼中,我們首先建立一個變數 defaultName並分配一個常數 Sam常數 Sam 的預設型別是 string ,所以在賦值後 defaultName 是 string 型別的。

下一行,我們將建立一個新型別 myString,它是 string的別名。

然後我們建立一個 myString的變數 customName並且給他賦值一個常數 Sam。因爲常數 Sam是無型別的,它可以分配給任何字串變數。因此這個賦值是允許的,customName的型別是 myString

現在,我們有一個型別爲 string的變數 defaultName和另一個型別爲 myString的變數 customName。即使我們知道這個 myString是 string型別的別名。Go 的型別策略不允許將一種型別的變數賦值給另一種型別的變數。因此將 defaultName賦值給 customName是不允許的,編譯器會拋出一個錯誤 main.go:7:20: cannot use defaultName (type string) as type myString in assignmen

布爾常數

布爾常數和字串常數沒有什麼不同。他們是兩個無型別的常數 true和 false。字串常數的規則適用於布爾常數,所以在這裏我們不再重複。以下是解釋布爾常數的簡單程式。

package main

func main() {  
    const trueConst = true
    type myBool bool
    var defaultBool = trueConst // 允許
    var customBool myBool = trueConst // 允許
    defaultBool = customBool // 不允許
}
複製程式碼

上面的程式是自我解釋的。

數位常數

數位常數包含整數、浮點數和複數的常數。數位常數中有一些微妙之處。

讓我們看一些例子來說清楚。

package main

import (  
    "fmt"
)

func main() {  
    const a = 5
    var intVar int = a
    var int32Var int32 = a
    var float64Var float64 = a
    var complex64Var complex64 = a
    fmt.Println("intVar",intVar, "\nint32Var", int32Var, "\nfloat64Var", float64Var, "\ncomplex64Var",complex64Var)
}
複製程式碼

上面的程式,常數 a是沒有型別的,它的值是 5。您可能想知道 a的預設型別是什麼,如果它確實有一個的話, 那麼我們如何將它分配給不同類型的變數。答案在於 a的語法。下面 下麪的程式將使事情更加清晰。

package main

import (  
    "fmt"
)

func main() {  
    var i = 5
    var f = 5.6
    var c = 5 + 6i
    fmt.Printf("i's type %T, f's type %T, c's type %T", i, f, c)

}
複製程式碼

在上面的程式中,每個變數的型別由數位常數的語法決定。5在語法中是整數, 5.6是浮點數,5+6i的語法是複數。當我們執行上面的程式,它會列印出 i's type int, f's type float64, c's type complex128

現在我希望下面 下麪的程式能夠正確的工作。

package main

import (  
    "fmt"
)

func main() {  
    const a = 5
    var intVar int = a
    var int32Var int32 = a
    var float64Var float64 = a
    var complex64Var complex64 = a
    fmt.Println("intVar",intVar, "\nint32Var", int32Var, "\nfloat64Var", float64Var, "\ncomplex64Var",complex64Var)
}
複製程式碼

在這個程式中, a的值是 5a的語法是通用的(它可以代表一個浮點數、整數甚至是一個沒有虛部的複數),因此可以將其分配給任何相容的型別。這些常數的預設型別可以被認爲是根據上下文在執行中生成的。 var intVar int = a要求 a是 int,所以它變成一個 int常數。 var complex64Var complex64 = a要求 a是 complex64,因此它變成一個複數型別。很簡單的:)。

數位表達式

數位常數可以在表達式中自由混合和匹配,只有當它們被分配給變數或者在需要型別的程式碼中的任何地方使用時,才需要型別。

package main

import (  
    "fmt"
)

func main() {  
    var a = 5.9/8
    fmt.Printf("a's type %T value %v",a, a)
}
複製程式碼

在上面的程式中, 5.9在語法中是浮點型,8是整型,5.9/8是允許的,因爲兩個都是數位常數。除法的結果是 0.7375是一個浮點型,所以 a的型別是浮點型。這個程式的輸出結果是: a's type float64 value 0.7375

6. 函數(Function)

函數是什麼?

函數是一塊執行特定任務的程式碼。一個函數是在輸入源基礎上,通過執行一系列的演算法,生成預期的輸出。

函數的宣告

在 Go 語言中,函數宣告通用語法如下:

func functionname(parametername type) returntype {  
    // 函數體(具體實現的功能)
}
複製程式碼

函數的宣告以關鍵詞 func開始,後面緊跟自定義的函數名 functionname (函數名)。函數的參數列表定義在 (和 )之間,返回值的型別則定義在之後的 returntype (返回值型別)處。宣告一個參數的語法採用 參數名參數型別的方式,任意多個參數採用類似 (parameter1 type, parameter2 type) 即(參數1 參數1的型別,參數2 參數2的型別)的形式指定。之後包含在 {和 }之間的程式碼,就是函數體。

函數中的參數列表和返回值並非是必須的,所以下面 下麪這個函數的宣告也是有效的

func functionname() {  
    // 譯註: 表示這個函數不需要輸入參數,且沒有返回值
}
複製程式碼

範例函數

我們以寫一個計算商品價格的函數爲例,輸入參數是單件商品的價格和商品的個數,兩者的乘積爲商品總價,作爲函數的輸出值。

func calculateBill(price int, no int) int {  
    var totalPrice = price * no // 商品總價 = 商品單價 * 數量
    return totalPrice // 返回總價
}
複製程式碼

上述函數有兩個整型的輸入 price和 no,返回值 totalPrice爲 price和 no的乘積,也是整數型別。

如果有連續若幹個參數,它們的型別一致,那麼我們無須一一羅列,只需在最後一個參數後新增該型別。例如,price int, no int可以簡寫爲 price, no int,所以範例函數也可寫成

func calculateBill(price, no int) int {  
    var totalPrice = price * no
    return totalPrice
}
複製程式碼

現在我們已經定義了一個函數,我們要在程式碼中嘗試着呼叫它。呼叫函數的語法爲 functionname(parameters)。呼叫範例函數的方法如下:

calculateBill(10, 5)
複製程式碼

完成了範例函數宣告和呼叫後,我們就能寫出一個完整的程式,並把商品總價列印在控制檯上:

package main

import (  
    "fmt"
)

func calculateBill(price, no int) int {  
    var totalPrice = price * no
    return totalPrice
}
func main() {  
    price, no := 90, 6 // 定義 price 和 no,預設型別爲 int
    totalPrice := calculateBill(price, no)
    fmt.Println("Total price is", totalPrice) // 列印到控制檯上
}
複製程式碼

該程式在控制檯上列印的結果爲

Total price is 540
複製程式碼

多返回值

Go 語言支援一個函數可以有多個返回值。我們來寫個以矩形的長和寬爲輸入參數,計算並返回矩形面積和周長的函數 rectProps。矩形的面積是長度和寬度的乘積, 周長是長度和寬度之和的兩倍。即:

  • 面積 = 長 * 寬
  • 周長 = 2 * ( 長 + 寬 )
package main

import (  
    "fmt"
)

func rectProps(length, width float64)(float64, float64) {  
    var area = length * width
    var perimeter = (length + width) * 2
    return area, perimeter
}

func main() {  
    area, perimeter := rectProps(10.8, 5.6)
    fmt.Printf("Area %f Perimeter %f", area, perimeter) 
}
複製程式碼

如果一個函數有多個返回值,那麼這些返回值必須用 (和 )括起來。func rectProps(length, width float64)(float64, float64)範例函數有兩個 float64 型別的輸入參數 length和 width,並返回兩個 float64 型別的值。該程式在控制檯上列印結果爲

Area 60.480000 Perimeter 32.800000
複製程式碼

命名返回值

從函數中可以返回一個命名值。一旦命名了返回值,可以認爲這些值在函數第一行就被宣告爲變數了。

上面的 rectProps 函數也可用這個方式寫成:

func rectProps(length, width float64)(area, perimeter float64) {  
    area = length * width
    perimeter = (length + width) * 2
    return // 不需要明確指定返回值,預設返回 area, perimeter 的值
}
複製程式碼

請注意, 函數中的 return 語句沒有顯式返回任何值。由於 area和 perimeter在函數宣告中指定爲返回值, 因此當遇到 return 語句時, 它們將自動從函數返回。

空白符

_在 Go 中被用作空白符,可以用作表示任何型別的任何值。

我們繼續以 rectProps函數爲例,該函數計算的是面積和周長。假使我們只需要計算面積,而並不關心周長的計算結果,該怎麼呼叫這個函數呢?這時,空白符 _就上場了。

下面 下麪的程式我們只用到了函數 rectProps的一個返回值 area

package main

import (  
    "fmt"
)

func rectProps(length, width float64) (float64, float64) {  
    var area = length * width
    var perimeter = (length + width) * 2
    return area, perimeter
}
func main() {  
    area, _ := rectProps(10.8, 5.6) // 返回值周長被丟棄
    fmt.Printf("Area %f ", area)
}
複製程式碼

在程式的 area, _ := rectProps(10.8, 5.6)這一行,我們看到空白符 _用來跳過不要的計算結果。

7. 包

什麼是包,爲什麼使用包?

到目前爲止,我們看到的 Go 程式都只有一個檔案,檔案裡包含一個 main 函數和幾個其他的函數。在實際中,這種把所有原始碼編寫在一個檔案的方法並不好用。以這種方式編寫,程式碼的重用和維護都會很困難。而包(Package)解決了這樣的問題。

包用於組織 Go 原始碼,提供了更好的可重用性與可讀性。由於包提供了程式碼的封裝,因此使得 Go 應用程式易於維護。

例如,假如我們正在開發一個 Go 影象處理程式,它提供了影象的裁剪、銳化、模糊和彩色增強等功能。一種組織程式的方式就是根據不同的特性,把程式碼放到不同的包中。比如裁剪可以是一個單獨的包,而銳化是另一個包。這種方式的優點是,由於彩色增強可能需要一些銳化的功能,因此彩色增強的程式碼只需要簡單地匯入(我們會在隨後討論)銳化功能的包,就可以使用銳化的功能了。這樣的方式使得程式碼易於重用。

我們會逐步構建一個計算矩形的面積和對角線的應用程式。

通過這個程式,我們會更好地理解包。

main 函數和 main 包

所有可執行的 Go 程式都必須包含一個 main 函數。這個函數是程式執行的入口。main 函數應該放置於 main 包中。

package packagename 這行程式碼指定了某一原始檔屬於一個包。它應該放在每一個原始檔的第一行。

下面 下麪開始爲我們的程式建立一個 main 函數和 main 包。在 Go 工作區內的 src 資料夾中建立一個資料夾,命名爲 geometry。在 geometry資料夾中建立一個 geometry.go檔案。

在 geometry.go 中編寫下面 下麪程式碼。

// geometry.go
package main 

import "fmt"

func main() {  
    fmt.Println("Geometrical shape properties")
}
複製程式碼

package main這一行指定該檔案屬於 main 包。import "packagename"語句用於匯入一個已存在的包。在這裏我們匯入了 fmt包,包內含有 Println 方法。接下來是 main 函數,它會列印 Geometrical shape properties

鍵入 go install geometry,編譯上述程式。該命令會在 geometry資料夾內搜尋擁有 main 函數的檔案。在這裏,它找到了 geometry.go。接下來,它編譯併產生一個名爲 geometry(在 windows 下是 geometry.exe)的二進制檔案,該二進制檔案放置於工作區的 bin 資料夾。現在,工作區的目錄結構會是這樣:

src
    geometry
        gemometry.go
bin
    geometry
複製程式碼

鍵入 workspacepath/bin/geometry,執行該程式。請用你自己的 Go 工作區來替換 workspacepath。這個命令會執行 bin 資料夾裡的 geometry二進制檔案。你應該會輸出 Geometrical shape properties

建立自定義的包

我們將組織程式碼,使得所有與矩形有關的功能都放入 rectangle包中。

我們會建立一個自定義包 rectangle,它有一個計算矩形的面積和對角線的函數。

屬於某一個包的原始檔都應該放置於一個單獨命名的資料夾裡。按照 Go 的慣例,應該用包名命名該資料夾。

因此,我們在 geometry資料夾中,建立一個命名爲 rectangle的資料夾。在 rectangle資料夾中,所有檔案都會以 package rectangle作爲開頭,因爲它們都屬於 rectangle 包。

在我們之前建立的 rectangle 資料夾中,再建立一個名爲 rectprops.go的檔案,新增下列程式碼。

// rectprops.go
package rectangle

import "math"

func Area(len, wid float64) float64 {  
    area := len * wid
    return area
}

func Diagonal(len, wid float64) float64 {  
    diagonal := math.Sqrt((len * len) + (wid * wid))
    return diagonal
}
複製程式碼

在上面的程式碼中,我們建立了兩個函數用於計算 Area和 Diagonal。矩形的面積是長和寬的乘積。矩形的對角線是長與寬平方和的平方根。math包下面 下麪的 Sqrt函數用於計算平方根。

注意到函數 Area 和 Diagonal 都是以大寫字母開頭的。這是有必要的,我們將會很快解釋爲什麼需要這樣做。

匯入自定義包

爲了使用自定義包,我們必須要先匯入它。匯入自定義包的語法爲 import path。我們必須指定自定義包相對於工作區內 src資料夾的相對路徑。我們目前的資料夾結構是:

src
    geometry
        geometry.go
        rectangle
            rectprops.go
複製程式碼

import "geometry/rectangle"這一行會匯入 rectangle 包。

在 geometry.go裏面新增下面 下麪的程式碼:

// geometry.go
package main 

import (  
    "fmt"
    "geometry/rectangle" // 匯入自定義包
)

func main() {  
    var rectLen, rectWidth float64 = 6, 7
    fmt.Println("Geometrical shape properties")
    /*Area function of rectangle package used*/
    fmt.Printf("area of rectangle %.2f\n", rectangle.Area(rectLen, rectWidth))
    /*Diagonal function of rectangle package used*/
    fmt.Printf("diagonal of the rectangle %.2f ", rectangle.Diagonal(rectLen, rectWidth))
}
複製程式碼

上面的程式碼匯入了 rectangle包,並呼叫了裏面的 Area 和 Diagonal 函數,得到矩形的面積和對角線。Printf 內的格式說明符 %.2f會將浮點數截斷到小數點兩位。應用程式的輸出爲:

Geometrical shape properties  
area of rectangle 42.00  
diagonal of the rectangle 9.22
複製程式碼

導出名字(Exported Names)

我們將 rectangle 包中的函數 Area 和 Diagonal 首字母大寫。在 Go 中這具有特殊意義。在 Go 中,任何以大寫字母開頭的變數或者函數都是被導出的名字。其它包只能存取被導出的函數和變數。在這裏,我們需要在 main 包中存取 Area 和 Diagonal 函數,因此會將它們的首字母大寫。

在 rectprops.go中,如果函數名從 Area(len, wid float64)變爲 area(len, wid float64),並且在 geometry.go中, rectangle.Area(rectLen, rectWidth)變爲 rectangle.area(rectLen, rectWidth), 則該程式執行時,編譯器會拋出錯誤 geometry.go:11: cannot refer to unexported name rectangle.area。因爲如果想在包外存取一個函數,它應該首字母大寫。

init 函數

所有包都可以包含一個 init函數。init 函數不應該有任何返回值型別和參數,在我們的程式碼中也不能顯式地呼叫它。init 函數的形式如下:

func init() {  
}
複製程式碼

init 函數可用於執行初始化任務,也可用於在開始執行之前驗證程式的正確性。

包的初始化順序如下:

  1. 首先初始化包級別(Package Level)的變數
  2. 緊接着呼叫 init 函數。包可以有多個 init 函數(在一個檔案或分佈於多個檔案中),它們按照編譯器解析它們的順序進行呼叫。

如果一個包匯入了另一個包,會先初始化被匯入的包。

儘管一個包可能會被匯入多次,但是它只會被初始化一次。

爲了理解 init 函數,我們接下來對程式做了一些修改。

首先在 rectprops.go檔案中新增了一個 init 函數。

// rectprops.go
package rectangle

import "math"  
import "fmt"

/*
 * init function added
 */
func init() {  
    fmt.Println("rectangle package initialized")
}
func Area(len, wid float64) float64 {  
    area := len * wid
    return area
}

func Diagonal(len, wid float64) float64 {  
    diagonal := math.Sqrt((len * len) + (wid * wid))
    return diagonal
}
複製程式碼

我們新增了一個簡單的 init 函數,它僅列印 rectangle package initialized

現在我們來修改 main 包。我們知道矩形的長和寬都應該大於 0,我們將在 geometry.go中使用 init 函數和包級別的變數來檢查矩形的長和寬。

修改 geometry.go檔案如下所示:

// geometry.go
package main 

import (  
    "fmt"
    "geometry/rectangle" // 匯入自定義包
    "log"
)
/*
 * 1. 包級別變數
*/
var rectLen, rectWidth float64 = 6, 7 

/*
*2. init 函數會檢查長和寬是否大於0
*/
func init() {  
    println("main package initialized")
    if rectLen < 0 {
        log.Fatal("length is less than zero")
    }
    if rectWidth < 0 {
        log.Fatal("width is less than zero")
    }
}

func main() {  
    fmt.Println("Geometrical shape properties")
    fmt.Printf("area of rectangle %.2f\n", rectangle.Area(rectLen, rectWidth))
    fmt.Printf("diagonal of the rectangle %.2f ",rectangle.Diagonal(rectLen, rectWidth))
}
複製程式碼

我們對 geometry.go做瞭如下修改:

  1. 變數 rectLen和 rectWidth從 main 函數級別移到了包級別。
  2. 新增了 init 函數。當 rectLen 或 rectWidth 小於 0 時,init 函數使用 log.Fatal函數列印一條日誌,並終止了程式。

main 包的初始化順序爲:

  1. 首先初始化被匯入的包。因此,首先初始化了 rectangle 包。
  2. 接着初始化了包級別的變數 rectLen和 rectWidth
  3. 呼叫 init 函數。
  4. 最後呼叫 main 函數。

當執行該程式時,會有如下輸出。

rectangle package initialized  
main package initialized  
Geometrical shape properties  
area of rectangle 42.00  
diagonal of the rectangle 9.22
複製程式碼

果然,程式會首先呼叫 rectangle 包的 init 函數,然後,會初始化包級別的變數 rectLen和 rectWidth。接着呼叫 main 包裡的 init 函數,該函數檢查 rectLen 和 rectWidth 是否小於 0,如果條件爲真,則終止程式。我們會在單獨的教學裡深入學習 if 語句。現在你可以認爲 if rectLen < 0能夠檢查 rectLen是否小於 0,並且如果是,則終止程式。rectWidth條件的編寫也是類似的。在這裏兩個條件都爲假,因此程式繼續執行。最後呼叫了 main 函數。

讓我們接着稍微修改這個程式來學習使用 init 函數。

將 geometry.go中的 var rectLen, rectWidth float64 = 6, 7改爲 var rectLen, rectWidth float64 = -6, 7。我們把 rectLen初始化爲負數。

現在當執行程式時,會得到:

rectangle package initialized  
main package initialized  
2017/04/04 00:28:20 length is less than zero
複製程式碼

像往常一樣, 會首先初始化 rectangle 包,然後是 main 包中的包級別的變數 rectLen 和 rectWidth。rectLen 爲負數,因此當執行 init 函數時,程式在列印 length is less than zero後終止。

使用空白識別符號(Blank Identifier)

匯入了包,卻不在程式碼中使用它,這在 Go 中是非法的。當這麼做時,編譯器是會報錯的。其原因是爲了避免匯入過多未使用的包,從而導致編譯時間顯著增加。將 geometry.go中的程式碼替換爲如下程式碼:

// geometry.go
package main 

import (
    "geometry/rectangle" // 匯入自定的包
)
func main() {

}
複製程式碼

上面的程式將會拋出錯誤 geometry.go:6: imported and not used: "geometry/rectangle"

然而,在程式開發的活躍階段,又常常會先匯入包,而暫不使用它。遇到這種情況就可以使用空白識別符號 _

下面 下麪的程式碼可以避免上述程式的錯誤:

package main

import (  
    "geometry/rectangle" 
)

var _ = rectangle.Area // 錯誤遮蔽器

func main() {

}
複製程式碼

var _ = rectangle.Area這一行遮蔽了錯誤。我們應該瞭解這些錯誤遮蔽器(Error Silencer)的動態,在程式開發結束時就移除它們,包括那些還沒有使用過的包。由此建議在 import 語句下面 下麪的包級別範圍中寫上錯誤遮蔽器。

有時候我們匯入一個包,只是爲了確保它進行了初始化,而無需使用包中的任何函數或變數。例如,我們或許需要確保呼叫了 rectangle 包的 init 函數,而不需要在程式碼中使用它。這種情況也可以使用空白識別符號,如下所示。

package main 

import (
    _ "geometry/rectangle" 
)
func main() {

}
複製程式碼

執行上面的程式,會輸出 rectangle package initialized。儘管在所有程式碼裡,我們都沒有使用這個包,但還是成功初始化了它。

8. if-else 語句

if 是條件語句。if 語句的語法是

if condition {  
}
複製程式碼

如果 condition爲真,則執行 {和 }之間的程式碼。

不同於其他語言,例如 C 語言,Go 語言裡的 { }是必要的,即使在 { }之間只有一條語句。

if 語句還有可選的 else if和 else部分。

if condition {  
} else if condition {
} else {
}
複製程式碼

if-else 語句之間可以有任意數量的 else if。條件判斷順序是從上到下。如果 if或 else if條件判斷的結果爲真,則執行相應的程式碼塊。 如果沒有條件爲真,則 else程式碼塊被執行。

讓我們編寫一個簡單的程式來檢測一個數字是奇數還是偶數。

package main

import (  
    "fmt"
)

func main() {  
    num := 10
    if num % 2 == 0 { //checks if number is even
        fmt.Println("the number is even") 
    }  else {
        fmt.Println("the number is odd")
    }
}
複製程式碼

if num%2 == 0語句檢測 num 取 2 的餘數是否爲零。 如果是爲零則列印輸出 "the number is even",如果不爲零則列印輸出 "the number is odd"。在上面的這個程式中,列印輸出的是 the number is even

if還有另外一種形式,它包含一個 statement可選語句部分,該元件在條件判斷之前執行。它的語法是

if statement; condition {  
}
複製程式碼

讓我們重寫程式,使用上面的語法來查詢數位是偶數還是奇數。

package main

import (  
    "fmt"
)

func main() {  
    if num := 10; num % 2 == 0 { //checks if number is even
        fmt.Println(num,"is even") 
    }  else {
        fmt.Println(num,"is odd")
    }
}
複製程式碼

在上面的程式中,num在 if語句中進行初始化,num只能從 if和 else中存取。也就是說 num的範圍僅限於 ifelse程式碼塊。如果我們試圖從其他外部的 if或者 else存取 num,編譯器會不通過。

讓我們再寫一個使用 else if的程式。

package main

import (  
    "fmt"
)

func main() {  
    num := 99
    if num <= 50 {
        fmt.Println("number is less than or equal to 50")
    } else if num >= 51 && num <= 100 {
        fmt.Println("number is between 51 and 100")
    } else {
        fmt.Println("number is greater than 100")
    }

}
複製程式碼

在上面的程式中,如果 else if num >= 51 && num <= 100爲真,程式將輸出 number is between 51 and 100

一個注意點

else語句應該在 if語句的大括號 }之後的同一行中。如果不是,編譯器會不通過。

讓我們通過以下程式來理解它。

package main

import (  
    "fmt"
)

func main() {  
    num := 10
    if num % 2 == 0 { //checks if number is even
        fmt.Println("the number is even") 
    }  
    else {
        fmt.Println("the number is odd")
    }
}
複製程式碼

在上面的程式中,else語句不是從 if語句結束後的 }同一行開始。而是從下一行開始。這是不允許的。如果執行這個程式,編譯器會輸出錯誤,

main.go:12:5: syntax error: unexpected else, expecting }
複製程式碼

出錯的原因是 Go 語言的分號是自動插入。

在 Go 語言規則中,它指定在 }之後插入一個分號,如果這是該行的最終標記。因此,在if語句後面的 }會自動插入一個分號。

實際上我們的程式變成了

if num%2 == 0 {  
      fmt.Println("the number is even") 
};  //semicolon inserted by Go
else {  
      fmt.Println("the number is odd")
}
複製程式碼

分號插入之後。從上面程式碼片段可以看出第三行插入了分號。

由於 if{…} else {…}是一個單獨的語句,它的中間不應該出現分號。因此,需要將 else語句放置在 }之後處於同一行中。

我已經重寫了程式,將 else 語句移動到 if 語句結束後 }的後面,以防止分號的自動插入。

package main

import (  
    "fmt"
)

func main() {  
    num := 10
    if num%2 == 0 { //checks if number is even
        fmt.Println("the number is even") 
    } else {
        fmt.Println("the number is odd")
    }
}
複製程式碼

現在編譯器會很開心,我們也一樣 ?。

9. 回圈

回圈語句是用來重複執行某一段程式碼。

for是 Go 語言唯一的回圈語句。Go 語言中並沒有其他語言比如 C 語言中的 while和 do while回圈。

for 回圈語法

for initialisation; condition; post {  
}
複製程式碼

初始化語句只執行一次。回圈初始化後,將檢查回圈條件。如果條件的計算結果爲 true,則 {}內的回圈體將執行,接着執行 post 語句。post 語句將在每次成功回圈迭代後執行。在執行 post 語句後,條件將被再次檢查。如果爲 true, 則回圈將繼續執行,否則 for 回圈將終止。(譯註:這是典型的 for 回圈三個表達式,第一個爲初始化表達式或賦值語句;第二個爲回圈條件判定表達式;第三個爲回圈變數修正表達式,即此處的 post )

這三個組成部分,即初始化,條件和 post 都是可選的。讓我們看一個例子來更好地理解回圈。

例子

讓我們用 for回圈寫一個列印出從 1 到 10 的程式。

package main

import (  
    "fmt"
)

func main() {  
    for i := 1; i <= 10; i++ {
        fmt.Printf(" %d",i)
    }
}
複製程式碼

在上面的程式中,i 變數被初始化爲 1。條件語句會檢查 i 是否小於 10。如果條件成立,i 就會被列印出來,否則回圈就會終止。回圈語句會在每一次回圈完成後自增 1。一旦 i 變得比 10 要大,回圈中止。

上面的程式會列印出 1 2 3 4 5 6 7 8 9 10

在 for回圈中宣告的變數只能在回圈體內存取,因此 i 不能夠在回圈體外存取。

break

break語句用於在完成正常執行之前突然終止 for 回圈,之後程式將會在 for 回圈下一行程式碼開始執行。

讓我們寫一個從 1 列印到 5 並且使用 break跳出回圈的程式。

package main

import (  
    "fmt"
)

func main() {  
    for i := 1; i <= 10; i++ {
        if i > 5 {
            break //loop is terminated if i > 5
        }
        fmt.Printf("%d ", i)
    }
    fmt.Printf("\nline after for loop")
}
複製程式碼

在上面的程式中,在回圈過程中 i 的值會被判斷。如果 i 的值大於 5 然後 break語句就會執行,回圈就會被終止。列印語句會在 for回圈結束後執行,上面程式會輸出爲

1 2 3 4 5  
line after for loop
複製程式碼

continue

continue語句用來跳出 for回圈中當前回圈。在 continue語句後的所有的 for回圈語句都不會在本次回圈中執行。回圈體會在一下次回圈中繼續執行。

讓我們寫一個列印出 1 到 10 並且使用 continue的程式。

package main

import (  
    "fmt"
)

func main() {  
    for i := 1; i <= 10; i++ {
        if i%2 == 0 {
            continue
        }
        fmt.Printf("%d ", i)
    }
}
複製程式碼

在上面的程式中,這行程式碼 if i%2==0會判斷 i 除以 2 的餘數是不是 0,如果是 0,這個數位就是偶數然後執行 continue語句,從而控製程式進入下一個回圈。因此在 continue後面的列印語句不會被呼叫而程式會進入一下個回圈。上面程式會輸出 1 3 5 7 9

 

更多例子

讓我們寫更多的程式碼來演示 for回圈的多樣性吧

下面 下麪這個程式列印出從 0 到 10 所有的偶數。

package main

import (  
    "fmt"
)

func main() {  
    i := 0
    for ;i <= 10; { // initialisation and post are omitted
        fmt.Printf("%d ", i)
        i += 2
    }
}
複製程式碼

正如我們已經知道的那樣,for回圈的三部分,初始化語句、條件語句、post 語句都是可選的。在上面的程式中,初始化語句和 post 語句都被省略了。i 在 for回圈外被初始化成了 0。只要 i<=10回圈就會被執行。在回圈中,i 以 2 的增量自增。上面的程式會輸出 0 2 4 6 8 10

上面程式中 for回圈中的分號也可以省略。這個格式的 for回圈可以看作是二選一的 for while回圈。上面的程式可以被重寫成:

package main

import (  
    "fmt"
)

func main() {  
    i := 0
    for i <= 10 { //semicolons are ommitted and only condition is present
        fmt.Printf("%d ", i)
        i += 2
    }
}
複製程式碼

在 for回圈中可以宣告和操作多個變數。讓我們寫一個使用宣告多個變數來列印下面 下麪序列的程式。

10 * 1 = 10  
11 * 2 = 22  
12 * 3 = 36  
13 * 4 = 52  
14 * 5 = 70  
15 * 6 = 90  
16 * 7 = 112  
17 * 8 = 136  
18 * 9 = 162  
19 * 10 = 190
package main

import (  
    "fmt"
)

func main() {  
    for no, i := 10, 1; i <= 10 && no <= 19; i, no = i+1, no+1 { //multiple initialisation and increment
        fmt.Printf("%d * %d = %d\n", no, i, no*i)
    }

}
複製程式碼

在上面的程式中 no和 i被宣告然後分別被初始化爲 10 和 1 。在每一次回圈結束後 no和 i都自增 1 。布爾型操作符 &&被用來確保 i 小於等於 10 並且 no小於等於 19 。

無限回圈

無限回圈的語法是:

for {  
}
複製程式碼

下一個程式就會一直列印Hello World不會停止。

package main

import "fmt"

func main() {  
    for {
        fmt.Println("Hello World")
    }
}
複製程式碼

在你本地系統上執行,來無限的列印 「Hello World」 。

這裏還有一個 range結構,它可以被用來在 for回圈中運算元組物件。當我們學習陣列時我們會補充這方面內容。

10. switch 語句

switch 是一個條件語句,用於將表達式的值與可能匹配的選項列表進行比較,並根據匹配情況執行相應的程式碼塊。它可以被認爲是替代多個 if else子句的常用方式。

看程式碼比文字更容易理解。讓我們從一個簡單的例子開始,它將把一個手指的編號作爲輸入,然後輸出該手指對應的名字。比如 0 是拇指,1 是食指等等。

package main

import (
    "fmt"
)

func main() {
    finger := 4
    switch finger {
    case 1:
        fmt.Println("Thumb")
    case 2:
        fmt.Println("Index")
    case 3:
        fmt.Println("Middle")
    case 4:
        fmt.Println("Ring")
    case 5:
        fmt.Println("Pinky")

    }
}
複製程式碼

在上述程式中,switch finger將 finger的值與每個 case語句進行比較。通過從上到下對每一個值進行對比,並執行與選項值匹配的第一個邏輯。在上述樣例中, finger值爲 4,因此列印的結果是 Ring

在選項列表中,case不允許出現重複項。如果您嘗試執行下面 下麪的程式,編譯器會報這樣的錯誤: main.go:18:2:在tmp / sandbox887814166 / main.go:16:7

package main

import (
    "fmt"
)

func main() {
    finger := 4
    switch finger {
    case 1:
        fmt.Println("Thumb")
    case 2:
        fmt.Println("Index")
    case 3:
        fmt.Println("Middle")
    case 4:
        fmt.Println("Ring")
    case 4://重複項
        fmt.Println("Another Ring")
    case 5:
        fmt.Println("Pinky")

    }
}
複製程式碼

預設情況(Default Case)

我們每個人一隻手只有 5 個手指。如果我們輸入了不正確的手指編號會發生什麼?這個時候就應該是屬於預設情況。當其他情況都不匹配時,將執行預設情況。

package main

import (
    "fmt"
)

func main() {
    switch finger := 8; finger {
    case 1:
        fmt.Println("Thumb")
    case 2:
        fmt.Println("Index")
    case 3:
        fmt.Println("Middle")
    case 4:
        fmt.Println("Ring")
    case 5:
        fmt.Println("Pinky")
    default: // 預設情況
        fmt.Println("incorrect finger number")
    }
}
複製程式碼

在上述程式中 finger的值是 8,它不符合其中任何情況,因此會列印 incorrect finger number。default 不一定只能出現在 switch 語句的最後,它可以放在 switch 語句的任何地方。

您可能也注意到我們稍微改變了 finger變數的宣告方式。finger宣告在了 switch 語句內。在表達式求值之前,switch 可以選擇先執行一個語句。在這行 switch finger:= 8; finger中, 先宣告瞭finger變數,隨即在表達式中使用了它。在這裏,finger變數的作用域僅限於這個 switch 內。

多表達式判斷

通過用逗號分隔,可以在一個 case 中包含多個表達式。

package main

import (
    "fmt"
)

func main() {
    letter := "i"
    switch letter {
    case "a", "e", "i", "o", "u": // 一個選項多個表達式
        fmt.Println("vowel")
    default:
        fmt.Println("not a vowel")
    }
}
複製程式碼

在 case "a","e","i","o","u":這一行中,列舉了所有的元音。只要匹配該項,則將輸出 vowel

無表達式的 switch

在 switch 語句中,表達式是可選的,可以被省略。如果省略表達式,則表示這個 switch 語句等同於 switch true,並且每個 case表達式都被認定爲有效,相應的程式碼塊也會被執行。

package main

import (
    "fmt"
)

func main() {
    num := 75
    switch { // 表達式被省略了
    case num >= 0 && num <= 50:
        fmt.Println("num is greater than 0 and less than 50")
    case num >= 51 && num <= 100:
        fmt.Println("num is greater than 51 and less than 100")
    case num >= 101:
        fmt.Println("num is greater than 100")
    }

}
複製程式碼

在上述程式碼中,switch 中缺少表達式,因此預設它爲 true,true 值會和每一個 case 的求值結果進行匹配。case num >= 51 && <= 100:爲 true,所以程式輸出 num is greater than 51 and less than 100。這種型別的 switch 語句可以替代多個 if else子句。

Fallthrough 語句

在 Go 中,每執行完一個 case 後,會從 switch 語句中跳出來,不再做後續 case 的判斷和執行。使用 fallthrough語句可以在已經執行完成的 case 之後,把控制權轉移到下一個 case 的執行程式碼中。

讓我們寫一個程式來理解 fallthrough。我們的程式將檢查輸入的數位是否小於 50、100 或 200。例如我們輸入 75,程式將輸出75 is lesser than 100和 75 is lesser than 200。我們用 fallthrough 來實現了這個功能。

package main

import (
    "fmt"
)

func number() int {
    num := 15 * 5 
    return num
}

func main() {

    switch num := number(); { // num is not a constant
    case num < 50:
        fmt.Printf("%d is lesser than 50\n", num)
        fallthrough
    case num < 100:
        fmt.Printf("%d is lesser than 100\n", num)
        fallthrough
    case num < 200:
        fmt.Printf("%d is lesser than 200", num)
    }

}
複製程式碼

switch 和 case 的表達式不一定是常數。它們也可以在執行過程中通過計算得到。在上面的程式中,num 被初始化爲函數 number()的返回值。程式執行到 switch 中時,會計算出 case 的值。case num < 100:的結果爲 true,所以程式輸出 75 is lesser than 100。當執行到下一句 fallthrough時,程式控制直接跳轉到下一個 case 的第一個執行邏輯中,所以列印出 75 is lesser than 200。最後這個程式的輸出會是

75 is lesser than 100  
75 is lesser than 200
複製程式碼

fallthrough 語句應該是 case 子句的最後一個語句。如果它出現在了 case 語句的中間,編譯器將會報錯:fallthrough statement out of place

11. 陣列和切片

陣列

陣列是同一型別元素的集合。例如,整數集合 5,8,9,79,76 形成一個數組。Go 語言中不允許混合不同類型的元素,例如包含字串和整數的陣列。(譯者注:當然,如果是 interface{} 型別陣列,可以包含任意型別)

陣列的宣告

一個數組的表示形式爲 [n]Tn表示陣列中元素的數量,T代表每個元素的型別。元素的數量 n也是該型別的一部分(稍後我們將詳細討論這一點)。

可以使用不同的方式來宣告陣列,讓我們一個一個的來看。

package main

import (
    "fmt"
)

func main() {
    var a [3]int //int array with length 3
    fmt.Println(a)
}
複製程式碼

var a[3]int宣告瞭一個長度爲 3 的整型陣列。陣列中的所有元素都被自動賦值爲陣列型別的零值。在這種情況下,a是一個整型陣列,因此 a的所有元素都被賦值爲 0,即 int 型的零值。執行上述程式將 輸出[0 0 0]

陣列的索引從 0開始到 length - 1結束。讓我們給上面的陣列賦值。

package main

import (
    "fmt"
)

func main() {
    var a [3]int //int array with length 3
    a[0] = 12 // array index starts at 0
    a[1] = 78
    a[2] = 50
    fmt.Println(a)
}
複製程式碼

a[0] 將值賦給陣列的第一個元素。該程式將 輸出[12 78 50]

讓我們使用 簡略宣告來建立相同的陣列。

package main

import (
    "fmt"
)

func main() {
    a := [3]int{12, 78, 50} // short hand declaration to create array
    fmt.Println(a)
}
複製程式碼

上面的程式將會列印相同的 輸出[12 78 50]

在簡略宣告中,不需要將陣列中所有的元素賦值。

package main

import (
    "fmt"
)

func main() {
    a := [3]int{12} 
    fmt.Println(a)
}
複製程式碼

在上述程式中的第 8 行 a := [3]int{12}宣告一個長度爲 3 的陣列,但只提供了一個值 12,剩下的 2 個元素自動賦值爲 0。這個程式將輸出[12 0 0]

你甚至可以忽略宣告陣列的長度,並用 ...代替,讓編譯器爲你自動計算長度,這在下面 下麪的程式中實現。

package main

import (
    "fmt"
)

func main() {
    a := [...]int{12, 78, 50} // ... makes the compiler determine the length
    fmt.Println(a)
}
複製程式碼

陣列的大小是型別的一部分。因此 [5]int和 [25]int是不同類型。陣列不能調整大小,不要擔心這個限制,因爲 slices的存在能解決這個問題。

package main

func main() {
    a := [3]int{5, 78, 8}
    var b [5]int
    b = a // not possible since [3]int and [5]int are distinct types
}
複製程式碼

在上述程式的第 6 行中, 我們試圖將型別 [3]int的變數賦給型別爲 [5]int的變數,這是不允許的,因此編譯器將拋出錯誤 main.go:6: cannot use a (type [3]int) as type [5]int in assignment。

陣列是值型別

Go 中的陣列是值型別而不是參照型別。這意味着當陣列賦值給一個新的變數時,該變數會得到一個原始陣列的一個副本。如果對新變數進行更改,則不會影響原始陣列。

package main

import "fmt"

func main() {
    a := [...]string{"USA", "China", "India", "Germany", "France"}
    b := a // a copy of a is assigned to b
    b[0] = "Singapore"
    fmt.Println("a is ", a)
    fmt.Println("b is ", b) 
}
複製程式碼

在上述程式的第 7 行,a的副本被賦給 b。在第 8 行中,b的第一個元素改爲 Singapore。這不會在原始陣列 a中反映出來。該程式將 輸出,

a is [USA China India Germany France]  
b is [Singapore China India Germany France]
複製程式碼

同樣,當陣列作爲參數傳遞給函數時,它們是按值傳遞,而原始陣列保持不變。

package main

import "fmt"

func changeLocal(num [5]int) {
    num[0] = 55
    fmt.Println("inside function ", num)
}
func main() {
    num := [...]int{5, 6, 7, 8, 8}
    fmt.Println("before passing to function ", num)
    changeLocal(num) //num is passed by value
    fmt.Println("after passing to function ", num)
}
複製程式碼

在上述程式的 13 行中, 陣列 num實際上是通過值傳遞給函數 changeLocal,陣列不會因爲函數呼叫而改變。這個程式將輸出,

before passing to function  [5 6 7 8 8]
inside function  [55 6 7 8 8]
after passing to function  [5 6 7 8 8]
複製程式碼

陣列的長度

通過將陣列作爲參數傳遞給 len函數,可以得到陣列的長度。

package main

import "fmt"

func main() {
    a := [...]float64{67.7, 89.8, 21, 78}
    fmt.Println("length of a is",len(a))
}
複製程式碼

上面的程式輸出爲 length of a is 4

使用 range 迭代陣列

for回圈可用於遍歷陣列中的元素。

package main

import "fmt"

func main() {
    a := [...]float64{67.7, 89.8, 21, 78}
    for i := 0; i < len(a); i++ { // looping from 0 to the length of the array
        fmt.Printf("%d th element of a is %.2f\n", i, a[i])
    }
}
複製程式碼

上面的程式使用 for回圈遍歷陣列中的元素,從索引 0到 length of the array - 1。這個程式執行後列印出,

0 th element of a is 67.70  
1 th element of a is 89.80  
2 th element of a is 21.00  
3 th element of a is 78.00
複製程式碼

Go 提供了一種更好、更簡潔的方法,通過使用 for回圈的 range方法來遍歷陣列。range返回索引和該索引處的值。讓我們使用 range 重寫上面的程式碼。我們還可以獲取陣列中所有元素的總和。

package main

import "fmt"

func main() {
    a := [...]float64{67.7, 89.8, 21, 78}
    sum := float64(0)
    for i, v := range a {//range returns both the index and value
        fmt.Printf("%d the element of a is %.2f\n", i, v)
        sum += v
    }
    fmt.Println("\nsum of all elements of a",sum)
}
複製程式碼

上述程式的第 8 行 for i, v := range a利用的是 for 回圈 range 方式。 它將返回索引和該索引處的值。 我們列印這些值,並計算陣列 a中所有元素的總和。 程式的 輸出是

0 the element of a is 67.70
1 the element of a is 89.80
2 the element of a is 21.00
3 the element of a is 78.00

sum of all elements of a 256.5
複製程式碼

如果你只需要值並希望忽略索引,則可以通過用 _空白識別符號替換索引來執行。

for _, v := range a { // ignores index  
}
複製程式碼

上面的 for 回圈忽略索引,同樣值也可以被忽略。

多維陣列

到目前爲止我們建立的陣列都是一維的,Go 語言可以建立多維陣列。

package main

import (
    "fmt"
)

func printarray(a [3][2]string) {
    for _, v1 := range a {
        for _, v2 := range v1 {
            fmt.Printf("%s ", v2)
        }
        fmt.Printf("\n")
    }
}

func main() {
    a := [3][2]string{
        {"lion", "tiger"},
        {"cat", "dog"},
        {"pigeon", "peacock"}, // this comma is necessary. The compiler will complain if you omit this comma
    }
    printarray(a)
    var b [3][2]string
    b[0][0] = "apple"
    b[0][1] = "samsung"
    b[1][0] = "microsoft"
    b[1][1] = "google"
    b[2][0] = "AT&T"
    b[2][1] = "T-Mobile"
    fmt.Printf("\n")
    printarray(b)
}

複製程式碼

在上述程式的第 17 行,用簡略語法宣告一個二維字串陣列 a 。20 行末尾的逗號是必需的。這是因爲根據 Go 語言的規則自動插入分號。至於爲什麼這是必要的,如果你想瞭解更多,請閱讀golang.org/doc/effecti…

另外一個二維陣列 b 在 23 行宣告,字串通過每個索引一個一個新增。這是另一種初始化二維陣列的方法。

第 7 行的 printarray 函數使用兩個 range 回圈來列印二維陣列的內容。上述程式的 輸出是

lion tiger
cat dog
pigeon peacock

apple samsung
microsoft google
AT&T T-Mobile

複製程式碼

這就是陣列,儘管陣列看上去似乎足夠靈活,但是它們具有固定長度的限制,不可能增加陣列的長度。這就要用到 切片了。事實上,在 Go 中,切片比傳統陣列更常見。

切片

切片是由陣列建立的一種方便、靈活且功能強大的包裝(Wrapper)。切片本身不擁有任何數據。它們只是對現有陣列的參照。

建立一個切片

帶有 T 型別元素的切片由 []T表示

package main

import (
    "fmt"
)

func main() {
    a := [5]int{76, 77, 78, 79, 80}
    var b []int = a[1:4] // creates a slice from a[1] to a[3]
    fmt.Println(b)
}

複製程式碼

使用語法 a[start:end]建立一個從 a陣列索引 start開始到 end - 1結束的切片。因此,在上述程式的第 9 行中, a[1:4]從索引 1 到 3 建立了 a陣列的一個切片表示。因此, 切片 b的值爲 [77 78 79]

讓我們看看另一種建立切片的方法。

package main

import (  
    "fmt"
)

func main() {  
    c := []int{6, 7, 8} // creates and array and returns a slice reference
    fmt.Println(c)
}

複製程式碼

在上述程式的第 9 行,c:= [] int {6,7,8}建立一個有 3 個整型元素的陣列,並返回一個儲存在 c 中的切片參照。

切片的修改

切片自己不擁有任何數據。它只是底層陣列的一種表示。對切片所做的任何修改都會反映在底層陣列中。

package main

import (
    "fmt"
)

func main() {
    darr := [...]int{57, 89, 90, 82, 100, 78, 67, 69, 59}
    dslice := darr[2:5]
    fmt.Println("array before", darr)
    for i := range dslice {
        dslice[i]++
    }
    fmt.Println("array after", darr)
}

複製程式碼

在上述程式的第 9 行,我們根據陣列索引 2,3,4 建立一個切片 dslice。for 回圈將這些索引中的值逐個遞增。當我們使用 for 回圈列印陣列時,我們可以看到對切片的更改反映在陣列中。該程式的輸出是

array before [57 89 90 82 100 78 67 69 59]  
array after [57 89 91 83 101 78 67 69 59]

複製程式碼

當多個切片共用相同的底層陣列時,每個切片所做的更改將反映在陣列中。

package main

import (
    "fmt"
)

func main() {
    numa := [3]int{78, 79 ,80}
    nums1 := numa[:] // creates a slice which contains all elements of the array
    nums2 := numa[:]
    fmt.Println("array before change 1", numa)
    nums1[0] = 100
    fmt.Println("array after modification to slice nums1", numa)
    nums2[1] = 101
    fmt.Println("array after modification to slice nums2", numa)
}

複製程式碼

在 9 行中,numa [:]缺少開始和結束值。開始和結束的預設值分別爲 0和 len (numa)。兩個切片 nums1和 nums2共用相同的陣列。該程式的輸出是

array before change 1 [78 79 80]  
array after modification to slice nums1 [100 79 80]  
array after modification to slice nums2 [100 101 80]

複製程式碼

從輸出中可以清楚地看出,當切片共用同一個陣列時,每個所做的修改都會反映在陣列中。

切片的長度和容量

切片的長度是切片中的元素數。切片的容量是從建立切片索引開始的底層陣列中元素數。

讓我們寫一段程式碼來更好地理解這點。

package main

import (
    "fmt"
)

func main() {
    fruitarray := [...]string{"apple", "orange", "grape", "mango", "water melon", "pine apple", "chikoo"}
    fruitslice := fruitarray[1:3]
    fmt.Printf("length of slice %d capacity %d", len(fruitslice), cap(fruitslice)) // length of is 2 and capacity is 6
}

複製程式碼

在上面的程式中,fruitslice是從 fruitarray的索引 1 和 2 建立的。 因此,fruitlice的長度爲 2

fruitarray的長度是 7。fruiteslice是從 fruitarray的索引 1建立的。因此, fruitslice的容量是從 fruitarray索引爲 1開始,也就是說從 orange開始,該值是 6。因此, fruitslice的容量爲 6。該[程式]輸出切片的 長度爲 2 容量爲 6

切片可以重置其容量。任何超出這一點將導致程式執行時拋出錯誤。

package main

import (
    "fmt"
)

func main() {
    fruitarray := [...]string{"apple", "orange", "grape", "mango", "water melon", "pine apple", "chikoo"}
    fruitslice := fruitarray[1:3]
    fmt.Printf("length of slice %d capacity %d\n", len(fruitslice), cap(fruitslice)) // length of is 2 and capacity is 6
    fruitslice = fruitslice[:cap(fruitslice)] // re-slicing furitslice till its capacity
    fmt.Println("After re-slicing length is",len(fruitslice), "and capacity is",cap(fruitslice))
}

複製程式碼

在上述程式的第 11 行中,fruitslice的容量是重置的。以上程式輸出爲,

length of slice 2 capacity 6 
After re-slicing length is 6 and capacity is 6

複製程式碼

使用 make 建立一個切片

func make([]T,len,cap)[]T 通過傳遞型別,長度和容量來建立切片。容量是可選參數, 預設值爲切片長度。make 函數建立一個數組,並返回參照該陣列的切片。

package main

import (
    "fmt"
)

func main() {
    i := make([]int, 5, 5)
    fmt.Println(i)
}

複製程式碼

使用 make 建立切片時預設情況下這些值爲零。上述程式的輸出爲 [0 0 0 0 0]

追加切片元素

正如我們已經知道陣列的長度是固定的,它的長度不能增加。 切片是動態的,使用 append可以將新元素追加到切片上。append 函數的定義是 func append(s[]T,x ... T)[]T

x ... T在函數定義中表示該函數接受參數 x 的個數是可變的。這些型別的函數被稱爲[可變函數]。

有一個問題可能會困擾你。如果切片由陣列支援,並且陣列本身的長度是固定的,那麼切片如何具有動態長度。以及內部發生了什麼,當新的元素被新增到切片時,會建立一個新的陣列。現有陣列的元素被複制到這個新陣列中,並返回這個新陣列的新切片參照。現在新切片的容量是舊切片的兩倍。下面 下麪的程式會讓你清晰理解。

package main

import (
    "fmt"
)

func main() {
    cars := []string{"Ferrari", "Honda", "Ford"}
    fmt.Println("cars:", cars, "has old length", len(cars), "and capacity", cap(cars)) // capacity of cars is 3
    cars = append(cars, "Toyota")
    fmt.Println("cars:", cars, "has new length", len(cars), "and capacity", cap(cars)) // capacity of cars is doubled to 6
}

複製程式碼

在上述程式中,cars的容量最初是 3。在第 10 行,我們給 cars 新增了一個新的元素,並把 append(cars, "Toyota")返回的切片賦值給 cars。現在 cars 的容量翻了一番,變成了 6。上述程式的輸出是

cars: [Ferrari Honda Ford] has old length 3 and capacity 3  
cars: [Ferrari Honda Ford Toyota] has new length 4 and capacity 6

複製程式碼

切片型別的零值爲 nil。一個 nil切片的長度和容量爲 0。可以使用 append 函數將值追加到 nil切片。

package main

import (  
    "fmt"
)

func main() {  
    var names []string //zero value of a slice is nil
    if names == nil {
        fmt.Println("slice is nil going to append")
        names = append(names, "John", "Sebastian", "Vinay")
        fmt.Println("names contents:",names)
    }
}

複製程式碼

在上面的程式 names是 nil,我們已經新增 3 個字串給 names。該程式的輸出是

slice is nil going to append  
names contents: [John Sebastian Vinay]

複製程式碼

也可以使用 ...運算子將一個切片新增到另一個切片。 你可以在[可變參數函數]教學中瞭解有關此運算子的更多資訊。

package main

import (
    "fmt"
)

func main() {
    veggies := []string{"potatoes", "tomatoes", "brinjal"}
    fruits := []string{"oranges", "apples"}
    food := append(veggies, fruits...)
    fmt.Println("food:",food)
}

複製程式碼

在上述程式的第 10 行,food 是通過 append(veggies, fruits...) 建立。程式的輸出爲 food: [potatoes tomatoes brinjal oranges apples]

切片的函數傳遞

我們可以認爲,切片在內部可由一個結構體型別表示。這是它的表現形式,

type slice struct {  
    Length        int
    Capacity      int
    ZerothElement *byte
}

複製程式碼

切片包含長度、容量和指向陣列第零個元素的指針。當切片傳遞給函數時,即使它通過值傳遞,指針變數也將參照相同的底層陣列。因此,當切片作爲參數傳遞給函數時,函數內所做的更改也會在函數外可見。讓我們寫一個程式來檢查這點。

package main

import (
    "fmt"
)

func subtactOne(numbers []int) {
    for i := range numbers {
        numbers[i] -= 2
    }
}
func main() {
    nos := []int{8, 7, 6}
    fmt.Println("slice before function call", nos)
    subtactOne(nos)                               // function modifies the slice
    fmt.Println("slice after function call", nos) // modifications are visible outside
}

複製程式碼

上述程式的行號 17 中,呼叫函數將切片中的每個元素遞減 2。在函數呼叫後列印切片時,這些更改是可見的。如果你還記得,這是不同於陣列的,對於函數中一個數組的變化在函數外是不可見的。上述[程式]的輸出是,

array before function call [8 7 6]  
array after function call [6 5 4]

複製程式碼

多維切片

類似於陣列,切片可以有多個維度。

package main

import (
    "fmt"
)

func main() {  
     pls := [][]string {
            {"C", "C++"},
            {"JavaScript"},
            {"Go", "Rust"},
            }
    for _, v1 := range pls {
        for _, v2 := range v1 {
            fmt.Printf("%s ", v2)
        }
        fmt.Printf("\n")
    }
}

複製程式碼

程式的輸出爲,

C C++  
JavaScript  
Go Rust

複製程式碼

記憶體優化

切片持有對底層陣列的參照。只要切片在記憶體中,陣列就不能被垃圾回收。在記憶體管理方面,這是需要注意的。讓我們假設我們有一個非常大的陣列,我們只想處理它的一小部分。然後,我們由這個陣列建立一個切片,並開始處理切片。這裏需要重點注意的是,在切片參照時陣列仍然存在記憶體中。

一種解決方法是使用 [copy] 函數 func copy(dst,src[]T)int來生成一個切片的副本。這樣我們可以使用新的切片,原始陣列可以被垃圾回收。

package main

import (
    "fmt"
)

func countries() []string {
    countries := []string{"USA", "Singapore", "Germany", "India", "Australia"}
    neededCountries := countries[:len(countries)-2]
    countriesCpy := make([]string, len(neededCountries))
    copy(countriesCpy, neededCountries) //copies neededCountries to countriesCpy
    return countriesCpy
}
func main() {
    countriesNeeded := countries()
    fmt.Println(countriesNeeded)
}

複製程式碼

在上述程式的第 9 行,neededCountries := countries[:len(countries)-2建立一個去掉尾部 2 個元素的切片 countries,在上述程式的 11 行,將 neededCountries複製到 countriesCpy同時在函數的下一行返回 countriesCpy。現在 countries陣列可以被垃圾回收, 因爲 neededCountries不再被參照。

 

12. 可變參數函數

什麼是可變參數函數

可變參數函數是一種參數個數可變的函數。

語法

如果函數最後一個參數被記作 ...T,這時函數可以接受任意個 T型別參數作爲最後一個參數。

請注意只有函數的最後一個參數才允許是可變的。

通過一些例子理解可變參數函數如何工作

你是否曾經想過 append 函數是如何將任意個參數值加入到切片中的。這樣 append 函數可以接受不同數量的參數。

func append(slice []Type, elems ...Type) []Type
複製程式碼

上面是 append 函數的定義。在定義中 elems 是可變參數。這樣 append 函數可以接受可變化的參數。

讓我們建立一個我們自己的可變參數函數。我們將寫一段簡單的程式,在輸入的整數列表裏查詢某個整數是否存在。

package main

import (
    "fmt"
)

func find(num int, nums ...int) {
    fmt.Printf("type of nums is %T\n", nums)
    found := false
    for i, v := range nums {
        if v == num {
            fmt.Println(num, "found at index", i, "in", nums)
            found = true
        }
    }
    if !found {
        fmt.Println(num, "not found in ", nums)
    }
    fmt.Printf("\n")
}
func main() {
    find(89, 89, 90, 95)
    find(45, 56, 67, 45, 90, 109)
    find(78, 38, 56, 98)
    find(87)
}
複製程式碼

在上面程式中 func find(num int, nums ...int)中的 nums可接受任意數量的參數。在 find 函數中,參數 nums相當於一個整型切片。

可變參數函數的工作原理是把可變參數轉換爲一個新的切片。以上面程式中的第 22 行爲例,find 函數中的可變參數是 89,90,95 。 find 函數接受一個 int 型別的可變參數。因此這三個參數被編譯器轉換爲一個 int 型別切片 int []int{89, 90, 95} 然後被傳入 find函數。

在第 10 行, for回圈遍歷 nums切片,如果 num在切片中,則列印 num的位置。如果 num不在切片中,則列印提示未找到該數位。

上面程式碼的輸出值如下,

type of nums is []int
89 found at index 0 in [89 90 95]

type of nums is []int
45 found at index 2 in [56 67 45 90 109]

type of nums is []int
78 not found in  [38 56 98]

type of nums is []int
87 not found in  []
複製程式碼

在上面程式的第 25 行,find 函數僅有一個參數。我們沒有給可變參數 nums ...int傳入任何參數。這也是合法的,在這種情況下 nums是一個長度和容量爲 0 的 nil切片。

給可變參數函數傳入切片

下面 下麪例子中,我們給可變參數函數傳入一個切片,看看會發生什麼。

package main

import (
    "fmt"
)

func find(num int, nums ...int) {
    fmt.Printf("type of nums is %T\n", nums)
    found := false
    for i, v := range nums {
        if v == num {
            fmt.Println(num, "found at index", i, "in", nums)
            found = true
        }
    }
    if !found {
        fmt.Println(num, "not found in ", nums)
    }
    fmt.Printf("\n")
}
func main() {
    nums := []int{89, 90, 95}
    find(89, nums)
}
複製程式碼

在第 23 行中,我們將一個切片傳給一個可變參數函數。

這種情況下無法通過編譯,編譯器報出錯誤 main.go:23: cannot use nums (type []int) as type int in argument to find

爲什麼無法運作呢?原因很直接,find函數的說明如下,

func find(num int, nums ...int)
複製程式碼

由可變參數函數的定義可知,nums ...int意味它可以接受 int型別的可變參數。

在上面程式的第 23 行,nums作爲可變參數傳入 find函數。前面我們知道,這些可變參數參數會被轉換爲 int型別切片然後在傳入 find函數中。但是在這裏 nums已經是一個 int 型別切片,編譯器試圖在 nums基礎上再建立一個切片,像下面 下麪這樣

find(89, []int{nums})
複製程式碼

這裏之所以會失敗是因爲 nums是一個 []int型別 而不是 int型別。

那麼有沒有辦法給可變參數函數傳入切片參數呢?答案是肯定的。

有一個可以直接將切片傳入可變參數函數的語法糖,你可以在在切片後加上 ... 後綴。如果這樣做,切片將直接傳入函數,不再建立新的切片

在上面的程式中,如果你將第 23 行的 find(89, nums)替換爲 find(89, nums...),程式將成功編譯並有如下輸出

type of nums is []int
89 found at index 0 in [89 90 95]
複製程式碼

下面 下麪是完整的程式供您參考。

package main

import (
    "fmt"
)

func find(num int, nums ...int) {
    fmt.Printf("type of nums is %T\n", nums)
    found := false
    for i, v := range nums {
        if v == num {
            fmt.Println(num, "found at index", i, "in", nums)
            found = true
        }
    }
    if !found {
        fmt.Println(num, "not found in ", nums)
    }
    fmt.Printf("\n")
}
func main() {
    nums := []int{89, 90, 95}
    find(89, nums...)
}
複製程式碼

不直觀的錯誤

當你修改可變參數函數中的切片時,請確保你知道你正在做什麼。

下面 下麪讓我們來看一個簡單的例子。

package main

import (
    "fmt"
)

func change(s ...string) {  
    s[0] = "Go"
}

func main() {
    welcome := []string{"hello", "world"}
    change(welcome...)
    fmt.Println(welcome)
}
複製程式碼

你認爲這段程式碼將輸出什麼呢?如果你認爲它輸出 [Go world]。恭喜你!你已經理解了可變參數函數和切片。如果你猜錯了,那也不要緊,讓我來解釋下爲什麼會有這樣的輸出。

在第 13 行,我們使用了語法糖 ...並且將切片作爲可變參數傳入 change函數。

正如前面我們所討論的,如果使用了 ...welcome切片本身會作爲參數直接傳入,不需要再建立一個新的切片。這樣參數 welcome將作爲參數傳入 change函數

在 change函數中,切片的第一個元素被替換成 Go,這樣程式產生了下面 下麪的輸出值

[Go world]
複製程式碼

這裏還有一個例子來理解可變參數函數。

package main

import (
    "fmt"
)

func change(s ...string) {
    s[0] = "Go"
    s = append(s, "playground")
    fmt.Println(s)
}

func main() {
    welcome := []string{"hello", "world"}
    change(welcome...)
    fmt.Println(welcome)
}
複製程式碼

13. Maps

什麼是 map ?

map 是在 Go 中將值(value)與鍵(key)關聯的內建型別。通過相應的鍵可以獲取到值。

如何建立 map ?

通過向 make函數傳入鍵和值的型別,可以建立 map。make(map[type of key]type of value)是建立 map 的語法。

personSalary := make(map[string]int)
複製程式碼

上面的程式碼建立了一個名爲 personSalary的 map,其中鍵是 string 型別,而值是 int 型別。

map 的零值是 nil。如果你想新增元素到 nil map 中,會觸發執行時 panic。因此 map 必須使用 make函數初始化。

package main

import (
    "fmt"
)

func main() {  
    var personSalary map[string]int
    if personSalary == nil {
        fmt.Println("map is nil. Going to make one.")
        personSalary = make(map[string]int)
    }
}
複製程式碼

上面的程式中,personSalary 是 nil,因此需要使用 make 方法初始化,程式將輸出 map is nil. Going to make one.

給 map 新增元素

給 map 新增新元素的語法和陣列相同。下面 下麪的程式給 personSalarymap 新增了幾個新元素。

package main

import (
    "fmt"
)

func main() {
    personSalary := make(map[string]int)
    personSalary["steve"] = 12000
    personSalary["jamie"] = 15000
    personSalary["mike"] = 9000
    fmt.Println("personSalary map contents:", personSalary)
}
複製程式碼

上面的程式輸出:personSalary map contents: map[steve:12000 jamie:15000 mike:9000]

你也可以在宣告的時候初始化 map。

package main

import (  
    "fmt"
)

func main() {  
    personSalary := map[string]int {
        "steve": 12000,
        "jamie": 15000,
    }
    personSalary["mike"] = 9000
    fmt.Println("personSalary map contents:", personSalary)
}
複製程式碼

上面的程式宣告瞭 personSalary,並在宣告的同時新增兩個元素。之後又新增了鍵 mike。程式輸出:

personSalary map contents: map[steve:12000 jamie:15000 mike:9000]
複製程式碼

鍵不一定只能是 string 型別。所有可比較的型別,如 boolean,interger,float,complex,string 等,都可以作爲鍵。關於可比較的型別,如果你想瞭解更多,請存取 golang.org/ref/spec#Co…

獲取 map 中的元素

目前我們已經給 map 新增了幾個元素,現在學習下如何獲取它們。獲取 map 元素的語法是 map[key]

package main

import (
    "fmt"
)

func main() {
    personSalary := map[string]int{
        "steve": 12000,
        "jamie": 15000,
    }
    personSalary["mike"] = 9000
    employee := "jamie"
    fmt.Println("Salary of", employee, "is", personSalary[employee])
}
複製程式碼

上面的程式很簡單。獲取並列印員工 jamie的薪資。程式輸出 Salary of jamie is 15000

如果獲取一個不存在的元素,會發生什麼呢?map 會返回該元素型別的零值。在 personSalary這個 map 裡,如果我們獲取一個不存在的元素,會返回 int型別的零值 0

package main

import (  
    "fmt"
)

func main() {
    personSalary := map[string]int{
        "steve": 12000,
        "jamie": 15000,
    }
    personSalary["mike"] = 9000
    employee := "jamie"
    fmt.Println("Salary of", employee, "is", personSalary[employee])
    fmt.Println("Salary of joe is", personSalary["joe"])
}
複製程式碼

上面程式輸出:

Salary of jamie is 15000
Salary of joe is 0
複製程式碼

上面程式返回 joe的薪資是 0。personSalary中不包含 joe的情況下我們不會獲取到任何執行時錯誤。

如果我們想知道 map 中到底是不是存在這個 key,該怎麼做:

value, ok := map[key]
複製程式碼

上面就是獲取 map 中某個 key 是否存在的語法。如果 ok是 true,表示 key 存在,key 對應的值就是 value,反之表示 key 不存在。

package main

import (
    "fmt"
)

func main() {
    personSalary := map[string]int{
        "steve": 12000,
        "jamie": 15000,
    }
    personSalary["mike"] = 9000
    newEmp := "joe"
    value, ok := personSalary[newEmp]
    if ok == true {
        fmt.Println("Salary of", newEmp, "is", value)
    } else {
        fmt.Println(newEmp,"not found")
    }
}
複製程式碼

上面的程式中,第 15 行,joe不存在,所以 ok是 false。程式將輸出:

joe not found
複製程式碼

遍歷 map 中所有的元素需要用 for range回圈。

package main

import (
    "fmt"
)

func main() {
    personSalary := map[string]int{
        "steve": 12000,
        "jamie": 15000,
    }
    personSalary["mike"] = 9000
    fmt.Println("All items of a map")
    for key, value := range personSalary {
        fmt.Printf("personSalary[%s] = %d\n", key, value)
    }

}
複製程式碼

上面程式輸出:

All items of a map
personSalary[mike] = 9000
personSalary[steve] = 12000
personSalary[jamie] = 15000
複製程式碼

有一點很重要,當使用 for range 遍歷 map 時,不保證每次執行程式獲取的元素順序相同。

刪除 map 中的元素

刪除 map中 key的語法是 [delete(map, key)]。這個函數沒有返回值。

package main

import (  
    "fmt"
)

func main() {  
    personSalary := map[string]int{
        "steve": 12000,
        "jamie": 15000,
    }
    personSalary["mike"] = 9000
    fmt.Println("map before deletion", personSalary)
    delete(personSalary, "steve")
    fmt.Println("map after deletion", personSalary)

}
複製程式碼

上述程式刪除了鍵 "steve",輸出:

map before deletion map[steve:12000 jamie:15000 mike:9000]
map after deletion map[mike:9000 jamie:15000]
複製程式碼

獲取 map 的長度

獲取 map 的長度使用 [len]函數。

package main

import (
    "fmt"
)

func main() {
    personSalary := map[string]int{
        "steve": 12000,
        "jamie": 15000,
    }
    personSalary["mike"] = 9000
    fmt.Println("length is", len(personSalary))

}
複製程式碼

上述程式中的 len(personSalary)函數獲取了 map 的長度。程式輸出 length is 3

Map 是參照型別

和 [slices]類似,map 也是參照型別。當 map 被賦值爲一個新變數的時候,它們指向同一個內部數據結構。因此,改變其中一個變數,就會影響到另一變數。

package main

import (
    "fmt"
)

func main() {
    personSalary := map[string]int{
        "steve": 12000,
        "jamie": 15000,
    }
    personSalary["mike"] = 9000
    fmt.Println("Original person salary", personSalary)
    newPersonSalary := personSalary
    newPersonSalary["mike"] = 18000
    fmt.Println("Person salary changed", personSalary)

}
複製程式碼

上面程式中的第 14 行,personSalary被賦值給 newPersonSalary。下一行 ,newPersonSalary中 mike的薪資變成了 18000personSalary中 Mike的薪資也會變成 18000。程式輸出:

Original person salary map[steve:12000 jamie:15000 mike:9000]
Person salary changed map[steve:12000 jamie:15000 mike:18000]
複製程式碼

當 map 作爲函數參數傳遞時也會發生同樣的情況。函數中對 map 的任何修改,對於外部的呼叫都是可見的。

Map 的相等性

map 之間不能使用 ==操作符判斷,==只能用來檢查 map 是否爲 nil

package main

func main() {
    map1 := map[string]int{
        "one": 1,
        "two": 2,
    }

    map2 := map1

    if map1 == map2 {
    }
}
複製程式碼

上面程式拋出編譯錯誤 invalid operation: map1 == map2 (map can only be compared to nil)

判斷兩個 map 是否相等的方法是遍歷比較兩個 map 中的每個元素。我建議你寫一段這樣的程式實現這個功能