Files
build-web-application-with-…/zh-tw/02.3.md
2019-02-26 01:40:54 +08:00

19 KiB
Raw Blame History

2.3 流程和函式

這小節我們要介紹Go裡面的流程控制以及函式操作。

流程控制

流程控制在程式語言中是最偉大的發明了因為有了它你可以透過很簡單的流程描述來表達很複雜的邏輯。Go中流程控制分三大類別條件判斷迴圈控制和無條件跳轉。

if

if也許是各種程式語言中最常見的了,它的語法概括起來就是:如果滿足條件就做某事,否則做另一件事。

Go裡面if條件判斷語句中不需要括號,如下程式碼所示


if x > 10 {
	fmt.Println("x is greater than 10")
} else {
	fmt.Println("x is less than 10")
}

Go的if還有一個強大的地方就是條件判斷語句裡面允許宣告一個變數,這個變數的作用域只能在該條件邏輯塊內,其他地方就不起作用了,如下所示


// 計算取得值x,然後根據x返回的大小判斷是否大於10。
if x := computedValue(); x > 10 {
	fmt.Println("x is greater than 10")
} else {
	fmt.Println("x is less than 10")
}

//這個地方如果這樣呼叫就編譯出錯了因為x是條件裡面的變數
fmt.Println(x)

多個條件的時候如下所示:


if integer == 3 {
	fmt.Println("The integer is equal to 3")
} else if integer < 3 {
	fmt.Println("The integer is less than 3")
} else {
	fmt.Println("The integer is greater than 3")
}

goto

Go有goto語句——請明智地使用它。用goto跳轉到必須在當前函式內定義的標籤。例如假設這樣一個迴圈:


func myFunc() {
	i := 0
Here:   //這行的第一個詞,以冒號結束作為標籤
	println(i)
	i++
	goto Here   //跳轉到Here去
}

標籤名是大小寫敏感的。

for

Go裡面最強大的一個控制邏輯就是for,它既可以用來迴圈讀取資料,又可以當作while來控制邏輯,還能迭代操作。它的語法如下:


for expression1; expression2; expression3 {
	//...
}

expression1expression2expression3都是表示式,其中expression1expression3是變數宣告或者函式呼叫返回值之類別的,expression2是用來條件判斷,expression1在迴圈開始之前呼叫,expression3在每輪迴圈結束之時呼叫。

一個例子比上面講那麼多更有用,那麼我們看看下面的例子吧:


package main

import "fmt"

func main(){
	sum := 0;
	for index:=0; index < 10 ; index++ {
		sum += index
	}
	fmt.Println("sum is equal to ", sum)
}
// 輸出sum is equal to 45

有些時候需要進行多個賦值操作由於Go裡面沒有,運算子,那麼可以使用平行賦值i, j = i+1, j-1

有些時候如果我們忽略expression1expression3


sum := 1
for ; sum < 1000;  {
	sum += sum
}

其中;也可以省略,那麼就變成如下的程式碼了,是不是似曾相識?對,這就是while的功能。


sum := 1
for sum < 1000 {
	sum += sum
}

在迴圈裡面有兩個關鍵操作breakcontinue ,break操作是跳出當前迴圈,continue是跳過本次迴圈。當巢狀過深的時候,break可以配合標籤使用,即跳轉至標籤所指定的位置,詳細參考如下例子:


for index := 10; index>0; index-- {
	if index == 5{
		break // 或者continue
	}
	fmt.Println(index)
}
// break打印出來10、9、8、7、6
// continue打印出來10、9、8、7、6、4、3、2、1

breakcontinue還可以跟著標號,用來跳到多重迴圈中的外層迴圈

for配合range可以用於讀取slicemap的資料:


for k,v:=range map {
	fmt.Println("map's key:",k)
	fmt.Println("map's val:",v)
}

由於 Go 支援 “多值返回”, 而對於“宣告而未被呼叫”的變數, 編譯器會報錯, 在這種情況下, 可以使用_來丟棄不需要的返回值 例如


for _, v := range map{
	fmt.Println("map's val:", v)
}

switch

有些時候你需要寫很多的if-else來實現一些邏輯處理,這個時候程式碼看上去就很醜很冗長,而且也不易於以後的維護,這個時候switch就能很好的解決這個問題。它的語法如下


switch sExpr {
case expr1:
	some instructions
case expr2:
	some other instructions
case expr3:
	some other instructions
default:
	other code
}

sExprexpr1expr2expr3的型別必須一致。Go的switch非常靈活,表示式不必是常量或整數,執行的過程從上至下,直到找到匹配項;而如果switch沒有表示式,它會匹配true


i := 10
switch i {
case 1:
	fmt.Println("i is equal to 1")
case 2, 3, 4:
	fmt.Println("i is equal to 2, 3 or 4")
case 10:
	fmt.Println("i is equal to 10")
default:
	fmt.Println("All I know is that i is an integer")
}

在第5行中我們把很多值聚合在了一個case裡面同時Go裡面switch預設相當於每個case最後帶有break匹配成功後不會自動向下執行其他case而是跳出整個switch, 但是可以使用fallthrough強制執行後面的case程式碼。


integer := 6
switch integer {
case 4:
	fmt.Println("The integer was <= 4")
	fallthrough
case 5:
	fmt.Println("The integer was <= 5")
	fallthrough
case 6:
	fmt.Println("The integer was <= 6")
	fallthrough
case 7:
	fmt.Println("The integer was <= 7")
	fallthrough
case 8:
	fmt.Println("The integer was <= 8")
	fallthrough
default:
	fmt.Println("default case")
}

上面的程式將輸出


The integer was <= 6
The integer was <= 7
The integer was <= 8
default case

函式

函式是Go裡面的核心設計它透過關鍵字func來宣告,它的格式如下:


func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) {
	//這裡是處理邏輯程式碼
	//返回多個值
	return value1, value2
}

上面的程式碼我們看出

  • 關鍵字func用來宣告一個函式funcName
  • 函式可以有一個或者多個引數,每個引數後面帶有型別,透過,分隔
  • 函式可以返回多個值
  • 上面返回值聲明瞭兩個變數output1output2,如果你不想宣告也可以,直接就兩個型別
  • 如果只有一個返回值且不宣告返回值變數,那麼你可以省略 包括返回值 的括號
  • 如果沒有返回值,那麼就直接省略最後的返回資訊
  • 如果有返回值, 那麼必須在函式的外層新增return語句

下面我們來看一個實際應用函式的例子用來計算Max值


package main

import "fmt"

// 返回a、b中最大值.
func max(a, b int) int {
	if a > b {
		return a
	}
	return b
}

func main() {
	x := 3
	y := 4
	z := 5

	max_xy := max(x, y) //呼叫函式max(x, y)
	max_xz := max(x, z) //呼叫函式max(x, z)

	fmt.Printf("max(%d, %d) = %d\n", x, y, max_xy)
	fmt.Printf("max(%d, %d) = %d\n", x, z, max_xz)
	fmt.Printf("max(%d, %d) = %d\n", y, z, max(y,z)) // 也可在這直接呼叫它
}

上面這個裡面我們可以看到max函式有兩個引數,它們的型別都是int,那麼第一個變數的型別可以省略(即 a,b int,而非 a int, b int)預設為離它最近的型別同理多於2個同類型的變數或者返回值。同時我們注意到它的返回值就是一個型別這個就是省略寫法。

多個返回值

Go語言比C更先進的特性其中一點就是函式能夠返回多個值。

我們直接上程式碼看例子


package main

import "fmt"

//返回 A+B 和 A*B
func SumAndProduct(A, B int) (int, int) {
	return A+B, A*B
}

func main() {
	x := 3
	y := 4

	xPLUSy, xTIMESy := SumAndProduct(x, y)

	fmt.Printf("%d + %d = %d\n", x, y, xPLUSy)
	fmt.Printf("%d * %d = %d\n", x, y, xTIMESy)
}

上面的例子我們可以看到直接返回了兩個引數,當然我們也可以命名返回引數的變數,這個例子裡面只是用了兩個型別,我們也可以改成如下這樣的定義,然後返回的時候不用帶上變數名,因為直接在函式裡面初始化了。但如果你的函式是匯出的(首字母大寫),官方建議:最好命名返回值,因為不命名返回值,雖然使得程式碼更加簡潔了,但是會造成產生的文件可讀性差。


func SumAndProduct(A, B int) (add int, Multiplied int) {
	add = A+B
	Multiplied = A*B
	return
}

變參

Go函式支援變參。接受變參的函式是有著不定數量的引數的。為了做到這點首先需要定義函式使其接受變參


func myfunc(arg ...int) {}

arg ...int告訴Go這個函式接受不定數量的引數。注意這些引數的型別全部是int。在函式體中,變數arg是一個intslice


for _, n := range arg {
	fmt.Printf("And the number is: %d\n", n)
}

傳值與傳指標

當我們傳一個引數值到被呼叫函式裡面時實際上是傳了這個值的一份copy當在被呼叫函式中修改引數值的時候呼叫函式中相應實參不會發生任何變化因為數值變化只作用在copy上。

為了驗證我們上面的說法,我們來看一個例子


package main

import "fmt"

//簡單的一個函式,實現了引數+1的操作
func add1(a int) int {
	a = a+1 // 我們改變了a的值
	return a //返回一個新值
}

func main() {
	x := 3

	fmt.Println("x = ", x)  // 應該輸出 "x = 3"

	x1 := add1(x)  //呼叫add1(x)

	fmt.Println("x+1 = ", x1) // 應該輸出"x+1 = 4"
	fmt.Println("x = ", x)    // 應該輸出"x = 3"
}

看到了嗎?雖然我們呼叫了add1函式,並且在add1中執行a = a+1操作,但是上面例子中x變數的值沒有發生變化

理由很簡單:因為當我們呼叫add1的時候,add1接收的引數其實是x的copy而不是x本身。

那你也許會問了,如果真的需要傳這個x本身,該怎麼辦呢?

這就牽扯到了所謂的指標。我們知道,變數在記憶體中是存放於一定地址上的,修改變數實際是修改變數地址處的記憶體。只有add1函式知道x變數所在的地址,才能修改x變數的值。所以我們需要將x所在地址&x傳入函式,並將函式的引數的型別由int改為*int,即改為指標型別,才能在函式中修改x變數的值。此時引數仍然是按copy傳遞的只是copy的是一個指標。請看下面的例子


package main

import "fmt"

//簡單的一個函式,實現了引數+1的操作
func add1(a *int) int { // 請注意,
	*a = *a+1 // 修改了a的值
	return *a // 返回新值
}

func main() {
	x := 3

	fmt.Println("x = ", x)  // 應該輸出 "x = 3"

	x1 := add1(&x)  // 呼叫 add1(&x) 傳x的地址

	fmt.Println("x+1 = ", x1) // 應該輸出 "x+1 = 4"
	fmt.Println("x = ", x)    // 應該輸出 "x = 4"
}

這樣,我們就達到了修改x的目的。那麼到底傳指標有什麼好處呢?

  • 傳指標使得多個函式能操作同一個物件。
  • 傳指標比較輕量級 (8bytes),只是傳記憶體地址,我們可以用指標傳遞體積大的結構體。如果用引數值傳遞的話, 在每次copy上面就會花費相對較多的系統開銷記憶體和時間。所以當你要傳遞大的結構體的時候用指標是一個明智的選擇。
  • Go語言中channelslicemap這三種類型的實現機制類似指標,所以可以直接傳遞,而不用取地址後傳遞指標。(注:若函式需改變slice的長度,則仍需要取地址傳遞指標)

defer

Go語言中有種不錯的設計即延遲defer語句你可以在函式中新增多個defer語句。當函式執行到最後時這些defer語句會按照逆序執行最後該函式返回。特別是當你在進行一些開啟資源的操作時遇到錯誤需要提前返回在返回前你需要關閉相應的資源不然很容易造成資源洩露等問題。如下程式碼所示我們一般寫開啟一個資源是這樣操作的


func ReadWrite() bool {
	file.Open("file")
// 做一些工作
	if failureX {
		file.Close()
		return false
	}

	if failureY {
		file.Close()
		return false
	}

	file.Close()
	return true
}

我們看到上面有很多重複的程式碼Go的defer有效解決了這個問題。使用它後,不但程式碼量減少了很多,而且程式變得更優雅。在defer後指定的函式會在函式退出前呼叫。


func ReadWrite() bool {
	file.Open("file")
	defer file.Close()
	if failureX {
		return false
	}
	if failureY {
		return false
	}
	return true
}

如果有很多呼叫defer,那麼defer是採用後進先出模式,所以如下程式碼會輸出4 3 2 1 0


for i := 0; i < 5; i++ {
	defer fmt.Printf("%d ", i)
}

函式作為值、型別

在Go中函式也是一種變數我們可以透過type來定義它,它的型別就是所有擁有相同的引數,相同的返回值的一種型別

type typeName func(input1 inputType1 , input2 inputType2 [, ...]) (result1 resultType1 [, ...])

函式作為型別到底有什麼好處呢?那就是可以把這個型別的函式當做值來傳遞,請看下面的例子


package main

import "fmt"

type testInt func(int) bool // 聲明瞭一個函式型別

func isOdd(integer int) bool {
	if integer%2 == 0 {
		return false
	}
	return true
}

func isEven(integer int) bool {
	if integer%2 == 0 {
		return true
	}
	return false
}

// 宣告的函式型別在這個地方當做了一個引數

func filter(slice []int, f testInt) []int {
	var result []int
	for _, value := range slice {
		if f(value) {
			result = append(result, value)
		}
	}
	return result
}

func main(){
	slice := []int {1, 2, 3, 4, 5, 7}
	fmt.Println("slice = ", slice)
	odd := filter(slice, isOdd)    // 函式當做值來傳遞了
	fmt.Println("Odd elements of slice are: ", odd)
	even := filter(slice, isEven)  // 函式當做值來傳遞了
	fmt.Println("Even elements of slice are: ", even)
}

函式當做值和型別在我們寫一些通用介面的時候非常有用,透過上面例子我們看到testInt這個型別是一個函式型別,然後兩個filter函式的引數和返回值與testInt型別是一樣的,但是我們可以實現很多種的邏輯,這樣使得我們的程式變得非常的靈活。

Panic和Recover

Go沒有像Java那樣的異常機制它不能丟擲異常而是使用了panicrecover機制。一定要記住,你應當把它作為最後的手段來使用,也就是說,你的程式碼中應當沒有,或者很少有panic的東西。這是個強大的工具,請明智地使用它。那麼,我們應該如何使用它呢?

Panic

是一個內建函式,可以中斷原有的控制流程,進入一個panic狀態中。當函式F呼叫panic函式F的執行被中斷但是F中的延遲函式會正常執行然後F返回到呼叫它的地方。在呼叫的地方F的行為就像呼叫了panic。這一過程繼續向上,直到發生panicgoroutine中所有呼叫的函式返回,此時程式退出。panic可以直接呼叫panic產生。也可以由執行時錯誤產生,例如訪問越界的陣列。

Recover

是一個內建的函式,可以讓進入panic狀態的goroutine恢復過來。recover僅在延遲函式中有效。在正常的執行過程中,呼叫recover會返回nil,並且沒有其它任何效果。如果當前的goroutine陷入panic狀態,呼叫recover可以捕獲到panic的輸入值,並且恢復正常的執行。

下面這個函式示範瞭如何在過程中使用panic


var user = os.Getenv("USER")

func init() {
	if user == "" {
		panic("no value for $USER")
	}
}

下面這個函式檢查作為其引數的函式在執行時是否會產生panic


func throwsPanic(f func()) (b bool) {
	defer func() {
		if x := recover(); x != nil {
			b = true
		}
	}()
	f() //執行函式f如果f中出現了panic那麼就可以恢復回來
	return
}

main函式和init函式

Go裡面有兩個保留的函式init函式(能夠應用於所有的package)和main函式(只能應用於package main)。這兩個函式在定義時不能有任何的引數和返回值。雖然一個package裡面可以寫任意多個init函式,但這無論是對於可讀性還是以後的可維護性來說,我們都強烈建議使用者在一個package中每個檔案只寫一個init函式。

Go程式會自動呼叫init()main(),所以你不需要在任何地方呼叫這兩個函式。每個package中的init函式都是可選的,但package main就必須包含一個main函式。

程式的初始化和執行都起始於main套件。如果main套件還匯入了其它的套件,那麼就會在編譯時將它們依次匯入。有時一個套件會被多個套件同時匯入,那麼它只會被匯入一次(例如很多套件可能都會用到fmt套件,但它只會被匯入一次,因為沒有必要匯入多次)。當一個套件被匯入時,如果該套件還匯入了其它的套件,那麼會先將其它套件匯入進來,然後再對這些套件中的套件級常量和變數進行初始化,接著執行init函式(如果有的話),依次類別推。等所有被匯入的套件都載入完畢了,就會開始對main套件中的套件級常量和變數進行初始化,然後執行main套件中的init函式(如果存在的話),最後執行main函式。下圖詳細地解釋了整個執行過程:

圖2.6 main函式引入套件初始化流程圖

import

我們在寫Go程式碼的時候經常用到import這個命令用來匯入套件檔案而我們經常看到的方式參考如下


import(
    "fmt"
)

然後我們程式碼裡面可以透過如下的方式呼叫


fmt.Println("hello world")

上面這個fmt是Go語言的標準函式庫其實是去GOROOT環境變數指定目錄下去載入該模組當然Go的import還支援如下兩種方式來載入自己寫的模組

  1. 相對路徑

    import “./model” //當前檔案同一目錄的model目錄但是不建議這種方式來import

  2. 絕對路徑

    import “shorturl/model” //載入gopath/src/shorturl/model模組

上面展示了一些import常用的幾種方式但是還有一些特殊的import讓很多新手很費解下面我們來一一講解一下到底是怎麼一回事

  1. 點操作

    我們有時候會看到如下的方式匯入包

     import(
         . "fmt"
     )
    

    這個點操作的含義就是這個套件匯入之後在你呼叫這個套件的函式時你可以省略字首的套件名也就是前面你呼叫的fmt.Println("hello world")可以省略的寫成Println("hello world")

  2. 別名操作

    別名操作顧名思義我們可以把套件命名成另一個我們用起來容易記憶的名字

     import(
         f "fmt"
     )
    

    別名操作的話呼叫套件函式時字首變成了我們的字首即f.Println("hello world")

  3. _操作

    這個操作經常是讓很多人費解的一個運算子請看下面這個import


	import (
	    "database/sql"
	    _ "github.com/ziutek/mymysql/godrv"
	)
_操作其實是引入該套件而不直接使用套件裡面的函式而是呼叫了該套件裡面的init函式。