這小節我們要介紹Go里面的流程控制以及函數操作。
流程控制
流程控制在編程語言中是最偉大的發明了,因為有了它,你可以通過很簡單的流程描述來表達很復雜的邏輯。Go中流程控制分三大類:條件判斷,循環控制和無條件跳轉。
if
if也許是各種編程語言中最常見的了,它的語法概括起來就是:如果滿足條件就做某事,否則做另一件事。
Go里面if條件判斷語句中不需要括號,如下代碼所示:
Go的if還有一個強大的地方就是條件判斷語句里面允許聲明一個變量,這個變量的作用域只能在該條件邏輯塊內,其他地方就不起作用了,如下所示:
//這個地方如果這樣調用就編譯出錯了,因為x是條件里面的變量
fmt.Println(x)
多個條件的時候如下所示:
goto
Go有goto語句——請明智地使用它。用goto跳轉到必須在當前函數內定義的標簽。例如假設這樣一個循環:
標簽名是大小寫敏感的。
for
Go里面最強大的一個控制邏輯就是for,它即可以用來循環讀取數據,又可以當作while來控制邏輯,還能迭代操作。它的語法如下:
一個例子比上面講那么多更有用,那么我們看看下面的例子吧:
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
有些時候如果我們忽略expression1和expression3:
在循環里面有兩個關鍵操作break和continue ,break操作是跳出當前循環,continue是跳過本次循環。當嵌套過深的時候,break可以配合標簽使用,即跳轉至標簽所指定的位置,詳細參考如下例子:
break和continue還可以跟著標號,用來跳到多重循環中的外層循環
for配合range可以用于讀取slice和map的數據:
switch
有些時候你需要寫很多的if-else來實現一些邏輯處理,這個時候代碼看上去就很丑很冗長,而且也不易于以后的維護,這個時候switch就能很好的解決這個問題。它的語法如下
sExpr和expr1、expr2、expr3的類型必須一致。Go的switch非常靈活,表達式不必是常量或整數,執行的過程從上至下,直到找到匹配項;而如果switch沒有表達式,它會匹配true。
在第5行中,我們把很多值聚合在了一個case里面,同時,Go里面switch默認相當于每個case最后帶有break,匹配成功后不會自動向下執行其他case,而是跳出整個switch, 但是可以使用fallthrough強制執行后面的case代碼。
函數
函數是Go里面的核心設計,它通過關鍵字func來聲明,它的格式如下:
上面的代碼我們看出
1.關鍵字func用來聲明一個函數funcName
2.函數可以有一個或者多個參數,每個參數后面帶有類型,通過,分隔
3.函數可以返回多個值
4.上面返回值聲明了兩個變量output1和output2,如果你不想聲明也可以,直接就兩個類型
5.如果只有一個返回值且不聲明返回值變量,那么你可以省略 包括返回值 的括號
6.如果沒有返回值,那么就直接省略最后的返回信息
7.如果有返回值, 那么必須在函數的外層添加return語句
下面我們來看一個實際應用函數的例子(用來計算Max值)
// 返回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)) // 也可在這直接調用它
}
多個返回值
Go語言比C更先進的特性,其中一點就是函數能夠返回多個值。
我們直接上代碼看例子:
//返回 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)
}
上面的例子我們可以看到直接返回了兩個參數,當然我們也可以命名返回參數的變量,這個例子里面只是用了兩個類型,我們也可以改成如下這樣的定義,然后返回的時候不用帶上變量名,因為直接在函數里面初始化了。但如果你的函數是導出的(首字母大寫),官方建議:最好命名返回值,因為不命名返回值,雖然使得代碼更加簡潔了,但是會造成生成的文檔可讀性差。
變參
Go函數支持變參。接受變參的函數是有著不定數量的參數的。為了做到這點,首先需要定義函數使其接受變參:
傳值與傳指針
當我們傳一個參數值到被調用函數里面時,實際上是傳了這個值的一份copy,當在被調用函數中修改參數值的時候,調用函數中相應實參不會發生任何變化,因為數值變化只作用在copy上。
為了驗證我們上面的說法,我們來看一個例子
//簡單的一個函數,實現了參數+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的是一個指針。請看下面的例子:
//簡單的一個函數,實現了參數+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的目的。那么到底傳指針有什么好處呢?
1.傳指針使得多個函數能操作同一個對象。
2.傳指針比較輕量級 (8bytes),只是傳內存地址,我們可以用指針傳遞體積大的結構體。如果用參數值傳遞的話, 在每次copy上面就會花費相對較多的系統開銷(內存和時間)。所以當你要傳遞大的結構體的時候,用指針是一個明智的選擇。
3.Go語言中string,slice,map這三種類型的實現機制類似指針,所以可以直接傳遞,而不用取地址后傳遞指針。(注:若函數需改變slice的長度,則仍需要取地址傳遞指針)
defer
Go語言中有種不錯的設計,即延遲(defer)語句,你可以在函數中添加多個defer語句。當函數執行到最后時,這些defer語句會按照逆序執行,最后該函數返回。特別是當你在進行一些打開資源的操作時,遇到錯誤需要提前返回,在返回前你需要關閉相應的資源,不然很容易造成資源泄露等問題。如下代碼所示,我們一般寫打開一個資源是這樣操作的:
if failureY {
file.Close()
return false
}
file.Close()
return true
}
我們看到上面有很多重復的代碼,Go的defer有效解決了這個問題。使用它后,不但代碼量減少了很多,而且程序變得更優雅。在defer后指定的函數會在函數退出前調用。
函數作為值、類型
在Go中函數也是一種變量,我們可以通過type來定義它,它的類型就是所有擁有相同的參數,相同的返回值的一種類型
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)
}
Panic和Recover
Go沒有像Java那樣的異常機制,它不能拋出異常,而是使用了panic和recover機制。一定要記住,你應當把它作為最后的手段來使用,也就是說,你的代碼中應當沒有,或者很少有panic的東西。這是個強大的工具,請明智地使用它。那么,我們應該如何使用它呢?
Panic
是一個內建函數,可以中斷原有的控制流程,進入一個令人恐慌的流程中。當函數F調用panic,函數F的執行被中斷,但是F中的延遲函數會正常執行,然后F返回到調用它的地方。在調用的地方,F的行為就像調用了panic。這一過程繼續向上,直到發生panic的goroutine中所有調用的函數返回,此時程序退出??只趴梢灾苯诱{用panic產生。也可以由運行時錯誤產生,例如訪問越界的數組。
Recover
是一個內建的函數,可以讓進入令人恐慌的流程中的goroutine恢復過來。recover僅在延遲函數中有效。在正常的執行過程中,調用recover會返回nil,并且沒有其它任何效果。如果當前的goroutine陷入恐慌,調用recover可以捕獲到panic的輸入值,并且恢復正常的執行。
下面這個函數演示了如何在過程中使用panic
func init() {
if user == "" {
panic("no value for $USER")
}
}
下面這個函數檢查作為其參數的函數在執行時是否會產生panic:
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這個命令用來導入包文件,而我們經常看到的方式參考如下:
1.相對路徑
上面展示了一些import常用的幾種方式,但是還有一些特殊的import,讓很多新手很費解,下面我們來一一講解一下到底是怎么一回事
點操作
我們有時候會看到如下的方式導入包
這個點操作的含義就是這個包導入之后在你調用這個包的函數時,你可以省略前綴的包名,也就是前面你調用的fmt.Println("hello world")可以省略的寫成Println("hello world")
別名操作
別名操作顧名思義我們可以把包命名成另一個我們用起來容易記憶的名字
_操作
這個操作經常是讓很多人費解的一個操作符,請看下面這個import
新聞熱點
疑難解答