Go語言入門教程系列——函數、循環與分支

本文始發於個人公眾號:TechFlow,原創不易,求個關注

今天是Golang專題的第四篇,這一篇文章將會介紹golang當中的函數、循環以及選擇判斷的具體用法。

函數

在之前的文章當中其實我們已經接觸過函數了,因為我們寫的main函數本質上也是一個函數。只不過由於main函數沒有返回值,也沒有傳參,所以省略了很多資訊。

func main() {
 fmt.Println("Hello World")
}

下面,我們來看看一個完整的函數是怎樣的,這是golang官網上的例子。

func add(x int, y int) int {
    return x + y
}

這是一個非常簡單的a+b的函數,我想大家應該都能看懂。我們來重點關注一下函數的格式。首先是func關鍵字,我們使用這個關鍵字定義一個函數,之後跟著的是函數名,然後是函數的傳參,最後是函數的返回值。

這個順序可能和我們之前普遍接觸的語法不太一樣,例如C++當中是把函數返回類型寫在最前面,然後是函數名和傳參。再比如Python當中則是沒有返回值的任何資訊,只有def關鍵字和函數名以及傳入的參數。

golang有些像是Python和C++的綜合體,總體來說我覺得內涵上更接近C++,但是寫法上和Python更接近一些。

我們理解了函數的定義之後,下面來看看golang當中支援的一些特性。

變數簡寫

在變數聲明的時候,我們如果定義兩個相同類型的變數是可以把它們進行縮寫的。比如我們定義兩個int類型的變數,分別叫做a和b。那麼可以簡寫成這樣:

var a, b int

同樣,在函數當中,如果傳入的參數類型相同,也一樣是可以簡寫的。我們可以把x和y兩個參數縮寫在一起,用逗號分開,共享變數類型。

func add(x, y int) int {
    return x + y
}

多值返回

在前面介紹golang特性的時候曾經提到過,golang作為一個看起來很守舊的語言,但是卻支援很多新鮮的特性。其中最知名的一個特性就是函數支援多值返回,即使是現在,也只有少量的語言支援這一特性。

在許多語言當中,如果需要返回多個值,往往需要用一個結構體或者是tuple、list等數據結構將它們包裝起來。但是在golang當中支援同時返回多個結果,這將會極大地方便我們的編碼。

func sample() (string, string) {
    return "sample1", "sample2"
}

多值返回也會有一個小小的問題,就是如果我們要返回的值過多,會導致這個return會寫得很長,或者是組裝的邏輯變得很複雜。或者是很容易產生遺漏、搞混順序之類的問題,golang當中針對這個問題也進行優化,支援我們對返回值進行命名。當命名的變數賦值完成之後,我們就可以直接用return關鍵字返回所有數據。

這個操作很難用語言描述很清楚,我們來看下面的例子:

func sample(x, y, z int) (xPrime, yPrime, zPrime int) {
    xPrime, yPrime, zPrime = x-1, y+1, z-2
    return 
}

在上面的程式碼當中,在返回之前,我們先給要返回的值起好了名字,我們在函數體當中對這些值進行賦值完成之後,我們就可以直接return了,golang會自動將它們的值填充進行返回。這樣不但可以簡化一定的編碼過程,也可以增加可讀性。

defer

golang的函數當中有一個特殊的用法,就是defer。這個用法據說其他語言也有,但是我暫時沒有見到過。defer是一個關鍵字,用它修飾的語句會被存入棧中,直到函數退出的時候執行

比如:

func main() {
 defer fmt.Println("world")

 fmt.Println("hello")
}

上面這兩行程式碼雖然defer的那一行在先,但是並不會被先執行,而是等main函數執行退出之前才會執行。

看起來這個用法有一點點怪,但是它的用處很大,經常用到。比如當我們打開一個文件的時候,不管文件有沒有打開成功,我們都需要記得關閉文件。但如果文件打開不成功可能就會有異常或者是報錯,如果我們把這些情況全部都考慮到,會變得非常複雜。所以這個時候我們通常都會用defer來執行文件的關閉。

要注意的是,defer修飾的程式碼會被放入棧中。所以最後會按照先進後出的原則進行執行。比如:

func main() {
 for i := 0; i < 10; i++ {
  defer fmt.Println(i)
 }

 fmt.Println("done")
}

最後執行的結果是9876543210,而不是相反。這一點蠻重要的,有的時候如果搞混了,很容易出現問題。

循環

和其他語言不同,Golang當中只有一種循環,就是for循環。沒有while,更沒有do while循環。在golang的設計中設想當中,只需要一種循環,就可以實現所有的功能。從某種程度上來說,也的確如此,golang中的循環有點像是C++和Python循環的結合體,集合兩種所長。

首先,我們先來看下for循環的語法,在for循環當中,我們使用分號分開循環條件。循環條件分為三個部分,第一個部分是初始化部分,我們對循環體進行初始化,第二個部分是判斷部分,判斷循環結束的終止條件,第三個部分是循環變數的改變部分。

寫出來大概是這樣的:

for i := 0; i < 10; i++ {
    fmt.Println(i)
}

這個語法是不是和C++中的循環很像呢?可以說除了沒有括弧之外,基本上就是一樣的。golang當中同樣支援++的自增操作,不過golang中只支援i++,而不支援++i。

和C++一樣,這三段當中的任何一段都是可以省略的,比如我們可以省略判斷條件:

for i := 0; ; i++ {
    fmt.Println(i)
    if i > 10 {
        break
    }
}

我們也可以省略循環變數的自增條件:

for i := 0; i < 10; {
    i += 2
    fmt.Println(i)
}

甚至可以全部省略,如果全部省略的話,等價於C++中的while(true)循環,也就是死循環。

range的用法

如果我們用循環遍歷一個數組或者是map,它的這個用法和Python中的用法非常類似。我們來看下,假如我們有一個數組是:

nums := []int{2, 3, 4}
sum := 0
for i, v := range nums {
    sum += v
    fmt.Println(i)
}

這個用法等價於Python中的for i, v in enumerate(nums)。也就是通過range會同時返回數組和map中的下標與對應的值,我們再來看下map,其實也是一樣的。

kvs := map[string]string{"a": "apple", "b": "banana"}
for k, v := range kvs {
    fmt.Printf("%s -> %s\n", k, v)
}

如果你看不懂map和數組的定義沒有關係,我們會在之後的文章當中再來詳細講解,這篇的主要內容是循環。我們只需要看得懂for循環的range操作即可。

判斷

golang當中支援if與switch進行條件判斷。我們先來看if,在golang當中的if和Python比較接近,在if的判斷條件外面不需要加上小括弧(),但是if的執行條件當中必須要大括弧{},即使只有一行程式碼。

比如剛才我們寫的循環中的那個break。

for i := 0; ; i++ {
    fmt.Println(i)
    if i > 10 {
        break
    }
}

在判斷中初始化

上面的邏輯在各個語言中都大同小異,很多語言都是這麼寫的。但是golang對於if還有特殊的支援,golang支援在if條件當中加上初始化資訊。

比如:

if v := sample(); v < 10 {
    fmt.Println(v)
}

上面當中的v是在if執行的時候才進行的初始化,也就是說我們將變數的初始化和if判斷結合在了一起。這個用法非常重要,在golang當中也大規模使用,所以我們一定要學會這個用法。

switch

golang當中也支援switch用法,它的基本套路和C++一樣,但是在細微的地方又做了優化。

比如和if一樣,switch也支援在執行的時候初始化。比如:

switch flag := sample(); flag {
case "a":
    fmt.Println(flag)
case "b":
    fmt.Println(flag)
default:
    fmt.Println(flag)
}

看明白了嗎,程式碼當中的flag是我們執行switch的時候才創建出來的。分號之前的都是初始化的程式碼,分號之後的表達式才是switch進行判斷的內容。

還有一個小細節需要注意,在golang當中使用switch的時候,每個case的判斷條件後面不需要再加上break。我們在寫其他語言的時候,如果用到switch要麼就是忘記了case的執行條件後面要加上break,要麼就是寫很多break非常麻煩。golang的設計者覺得每個case都加上break太二了,因為大家基本上都只用switch執行一個case,所以就去掉了必須要加上break這個設定。

switch執行順序

在golang當中,switch的判斷條件按照順序執行。

為什麼要強調這個呢?因為你很有可能會看到有些人的程式碼里的switch沒有判斷條件,比如:


switch a := sample();{
case a < 5:
    fmt.Println(a)
case a > 5:
    fmt.Println(a)
default:
    fmt.Println("end")
}

在上面這段程式碼當中,我們根本沒有為switch設置判斷的根據,這段邏輯完全等同於若干個if-else條件的羅列,它在golang當中同樣是允許的。

題外話

今天本來是分散式專題,但實在是沒有想到什麼很好的題目,我也不喜歡強求,乾脆就換個主題吧。以後分散式專題還會更新,不過可能要改成間歇式的了,後面想少寫點理論,能夠分享一點可以實際用上的東西(所以需要的時間比較久)。

不知道大家從今天的內容當中有沒有感受到golang這門語言的個性,很多地方看起來中規中矩,卻又能創造出新的用法來,至少我是很佩服設計者的想法的。golang當中這些新特性初見的時候往往會覺得不喜歡和排斥,怎麼看怎麼怪異,但是寫多了之後還是蠻香的。

今天的文章就到這裡,掃碼關注,獲取更多優質文章。