Go語言入門教程系列——函式、迴圈與分支

TechFlow2019發表於2020-05-09

本文始發於個人公眾號: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當中這些新特性初見的時候往往會覺得不喜歡和排斥,怎麼看怎麼怪異,但是寫多了之後還是蠻香的。

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

各位看官大大,賞賜個關注再走吧~

相關文章