流程和函式
這小節我們要介紹 Go 裡面的流程控制以及函式操作。
流程控制
流程控制在程式語言中是最偉大的發明了,因為有了它,你可以透過很簡單的流程描述來表達很複雜的邏輯。Go 中流程控制分三大類別:條件判斷,迴圈控制和無條件跳轉。
if
if
也許是各種程式語言中最常見的了,它的語法概括起來就是:如果滿足條件就做某事,否則做另一件事。
Go 裡面 if
條件判斷語句中不需要括號,如下程式碼所示
Go 的 if
還有一個強大的地方就是條件判斷語句裡面允許宣告一個變數,這個變數的作用域只能在該條件邏輯區塊內,其他地方就無法使用,如下所示
多個條件的時候如下所示:
goto
Go 有 goto
語句——請明智地使用它。用 goto
跳轉到必須在當前函式內定義的標籤。例如假設這樣一個迴圈:
標籤名稱(label)是區分大小寫的的。
for
Go 裡面最強大的一個控制邏輯就是for
,它既可以用來迴圈讀取資料,又可以當作 while
來控制邏輯,還能迭代操作。它的語法如下:
expression1
、expression2
和 expression3
都是表示式,其中 expression1
和expression3
是變數宣告或者函式呼叫回傳值之類別的,expression2
是用來條件判斷,expression1
在迴圈開始之前呼叫,expression3
在每輪迴圈結束之時呼叫。
一個例子比上面講那麼多更有用,那麼我們看看下面的例子吧:
有些時候需要進行多個賦值操作,由於 Go 裡面沒有,
運算子,那麼可以使用平行賦值i, j = i+1, j-1
有些時候如果我們忽略 expression1
和expression3
:
其中 ;
也可以省略,那麼就變成如下的程式碼了,是不是似曾相識?對,這就是 while
的功能。
在迴圈裡面有兩個關鍵操作 break
和continue
,break
操作是跳出當前迴圈,continue
是跳過本次迴圈。當巢狀過深的時候,break
可以配合標籤使用,即跳轉至標籤所指定的位置,詳細參考如下例子:
break
和 continue
還可以跟著標號,用來跳到多重迴圈中的外層迴圈
for
配合 range
可以用於讀取 slice
和map
的資料:
由於 Go 支援 “多值回傳”, 而對於“宣告而未被呼叫”的變數, 編譯器會報錯, 在這種情況下, 可以使用 _
來丟棄不需要的回傳值 例如
switch
有些時候你需要寫很多的if-else
來實現一些邏輯處理,這個時候程式碼看上去就很醜很冗長,而且也不易於以後的維護,這個時候 switch
就能很好的解決這個問題。它的語法如下
sExpr
和expr1
、expr2
、expr3
的型別必須一致。Go 的 switch
非常靈活,表示式不必是常數或整數,執行的過程從上至下,直到找到匹配項;而如果 switch
沒有表示式,它會匹配true
。
在第 5 行中,我們把很多值聚合在了一個 case
裡面,同時,Go 裡面 switch
預設相當於每個 case
最後帶有break
,匹配成功後不會自動向下執行其他 case,而是跳出整個switch
, 但是可以使用 fallthrough
強制執行後面的 case 程式碼。
上面的程式將輸出
函式
函式是 Go 裡面的核心設計,它透過關鍵字 func
來宣告,它的格式如下:
上面的程式碼我們看出
關鍵字
func
用來宣告一個函式funcName
函式可以有一個或者多個參數,每個參數後面帶有型別,透過
,
分隔函式可以回傳多個值
上面回傳值宣告了兩個變數
output1
和output2
,如果你不想宣告也可以,直接就兩個型別如果只有一個回傳值且不宣告回傳值變數,那麼你可以省略 包括回傳值 的括號
如果沒有回傳值,那麼就直接省略最後的回傳資訊
如果有回傳值, 那麼必須在函式的外層新增 return 語句
下面我們來看一個實際應用函式的例子(用來計算 Max 值)
上面這個裡面我們可以看到 max
函式有兩個參數,它們的型別都是int
,那麼第一個變數的型別可以省略(即 a,b int,而非 a int, b int),預設為離它最近的型別,同理多於 2 個同類型的變數或者回傳值。同時我們注意到它的回傳值就是一個型別,這個就是省略寫法。
多個回傳值
Go 語言比 C 更先進的特性,其中一點就是函式能夠回傳多個值。
我們直接上程式碼看例子
上面的例子我們可以看到直接回傳了兩個參數,當然我們也可以命名回傳參數的變數,這個例子裡面只是用了兩個型別,我們也可以改成如下這樣的定義,然後回傳的時候不用帶上變數名,因為直接在函式裡面初始化了。但如果你的函式是匯出的(首字母大寫),官方建議:最好命名回傳值,因為不命名回傳值,雖然使得程式碼更加簡潔了,但是會造成產生的文件可讀性差。
可變參數函式 (Variadic functions)
Go 函式支援可變參數函式。接受可變參數的函式是有著不定數量的參數的。為了做到這點,首先需要定義函式使其接受可變參數:
arg ...int
告訴 Go 這個函式接受不定數量的參數。注意,這些參數的型別全部是 int
。在函式體中,變數 arg
是一個 int
的 slice
:
傳值與傳指標
當我們傳一個參數值到被呼叫函式裡面時,實際上是傳了這個值的一份 copy,當在被呼叫函式中修改參數值的時候,呼叫函式中相應參數不會發生任何變化,因為數值變化只作用在 copy 上。
為了驗證我們上面的說法,我們來看一個例子
看到了嗎?雖然我們呼叫了 add1
函式,並且在 add1
中執行a = a+1
操作,但是上面例子中 x
變數的值沒有發生變化
理由很簡單:因為當我們呼叫 add1
的時候,add1
接收的參數其實是 x
的 copy,而不是 x
本身。
那你也許會問了,如果真的需要傳這個 x
本身,該怎麼辦呢?
這就牽扯到了所謂的指標。我們知道,變數在記憶體中是存放於一定地址上的,修改變數實際是修改變數地址處的記憶體。只有 add1
函式知道 x
變數所在的地址,才能修改 x
變數的值。所以我們需要將 x
所在地址&x
傳入函式,並將函式的參數的型別由 int
改為*int
,即改為指標型別,才能在函式中修改 x
變數的值。此時參數仍然是按 copy 傳遞的,只是 copy 的是一個指標。請看下面的例子
這樣,我們就達到了修改 x
的目的。那麼到底傳指標有什麼好處呢?
傳指標使得多個函式能操作同一個物件。
傳指標比較輕量級 (8bytes),只是傳記憶體地址,我們可以用指標傳遞體積大的結構體。如果用參數值傳遞的話, 在每次 copy 上面就會花費相對較多的系統開銷(記憶體和時間)。所以當你要傳遞大的結構體的時候,用指標是一個明智的選擇。
Go 語言中
channel
,slice
,map
這三種類型的實現機制類似指標,所以可以直接傳遞,而不用取地址後傳遞指標。(注:若函式需改變slice
的長度,則仍需要取地址傳遞指標)
defer
Go 語言中有種不錯的設計,即延遲(defer)語句,你可以在函式中新增多個 defer 語句。當函式執行到最後時,這些 defer 語句會按照逆序執行,最後該函式回傳。特別是當你在進行一些開啟資源的操作時,遇到錯誤需要提前回傳,在回傳前你需要關閉相應的資源,不然很容易造成資源洩露等問題。如下程式碼所示,我們一般寫開啟一個資源是這樣操作的:
我們看到上面有很多重複的程式碼,Go 的 defer
有效解決了這個問題。使用它後,不但程式碼量減少了很多,而且程式變得更優雅。在 defer
後指定的函式會在函式退出前呼叫。
如果有很多呼叫defer
,那麼 defer
是採用後進先出模式,所以如下程式碼會輸出4 3 2 1 0
函式作為值、型別
在 Go 中函式也是一種變數,我們可以透過 type
來定義它,它的型別就是所有擁有相同的參數,相同的回傳值的一種型別
函式作為型別到底有什麼好處呢?那就是可以把這個型別的函式當做值來傳遞,請看下面的例子
函式當做值和型別在我們寫一些通用介面的時候非常有用,透過上面例子我們看到 testInt
這個型別是一個函式型別,然後兩個 filter
函式的參數和回傳值與 testInt
型別是一樣的,但是我們可以實現很多種的邏輯,這樣使得我們的程式變得非常的靈活。
Panic 和 Recover
Go 沒有像 Java 那樣的異常機制,它不能丟擲異常,而是使用了 panic
和recover
機制。一定要記住,你應當把它作為最後的手段來使用,也就是說,你的程式碼中應當沒有,或者很少有 panic
的東西。這是個強大的工具,請明智地使用它。那麼,我們應該如何使用它呢?
Panic
是一個內建函式,可以中斷原有的控制流程,進入一個
panic
狀態中。當函式F
呼叫panic
,函式 F 的執行被中斷,但是F
中的延遲函式會正常執行,然後 F 回傳到呼叫它的地方。在呼叫的地方,F
的行為就像呼叫了panic
。這一過程繼續向上,直到發生panic
的goroutine
中所有呼叫的函式回傳,此時程式退出。panic
可以直接呼叫panic
產生。也可以由執行時錯誤產生,例如存取越界的陣列。
Recover
是一個內建的函式,可以讓進入
panic
狀態的goroutine
恢復過來。recover
僅在延遲函式中有效。在正常的執行過程中,呼叫recover
會回傳nil
,並且沒有其它任何效果。如果當前的goroutine
陷入panic
狀態,呼叫recover
可以捕獲到panic
的輸入值,並且恢復正常的執行。
下面這個函式示範了如何在過程中使用panic
下面這個函式檢查作為其參數的函式在執行時是否會產生panic
:
main
函式和 init
函式
main
函式和 init
函式Go 裡面有兩個保留的函式:init
函式(能夠應用於所有的package
)和 main
函式(只能應用於package main
)。這兩個函式在定義時不能有任何的參數和回傳值。雖然一個 package
裡面可以寫任意多個 init
函式,但這無論是對於可讀性還是以後的可維護性來說,我們都強烈建議使用者在一個 package
中每個檔案只寫一個 init
函式。
Go 程式會自動呼叫init()
和main()
,所以你不需要在任何地方呼叫這兩個函式。每個 package
中的 init
函式都是可選的,但package main
就必須包含一個 main
函式。
程式的初始化和執行都起始於 main
套件。如果 main
套件還匯入了其它的套件,那麼就會在編譯時將它們依次匯入。有時一個套件會被多個套件同時匯入,那麼它只會被匯入一次(例如很多套件可能都會用到 fmt
套件,但它只會被匯入一次,因為沒有必要匯入多次)。當一個套件被匯入時,如果該套件還匯入了其它的套件,那麼會先將其它套件匯入進來,然後再對這些套件中的 "套件級" (package-level) 常數和變數進行初始化,接著執行 init
函式(如果有的話),依次類別推。等所有被匯入的套件都載入完畢了,就會開始對 main
套件中的 "套件級" 常數和變數進行初始化,然後執行 main
套件中的 init
函式(如果存在的話),最後執行 main
函式。下圖詳細地解釋了整個執行過程:
圖 2.6 main 函式引入套件初始化流程圖
import
我們在寫 Go 程式碼的時候經常用到 import 這個命令用來匯入套件檔案,而我們經常看到的方式參考如下:
然後我們程式碼裡面可以透過如下的方式呼叫
上面這個 fmt 是 Go 語言的標準函式庫,其實是去 GOROOT
環境變數指定目錄下去載入該模組,當然 Go 的 import 還支援如下兩種方式來載入自己寫的模組:
相對路徑
import “./model” //當前檔案同一目錄的 model 目錄,但是不建議這種方式來 import
絕對路徑
import “shorturl/model” //載入 gopath/src/shorturl/model 模組
上面展示了一些 import 常用的幾種方式,但是還有一些特殊的 import,讓很多新手很難以理解,下面我們來一一講解一下到底是怎麼一回事
點操作
我們有時候會看到如下的方式匯入包
這個點操作的含義就是這個套件匯入之後在你呼叫這個套件的函式時,你可以省略字首的套件名,也就是前面你呼叫的 fmt.Println("hello world")可以省略的寫成 Println("hello world")
別名操作
別名操作顧名思義我們可以把套件命名成另一個我們用起來容易記憶的名字
別名操作的話呼叫套件函式時字首變成了我們的字首,即 f.Println("hello world")
_操作
這個操作經常是讓很多人難以理解的一個運算子,請看下面這個 import
links
Last updated