ETH官方钱包

前往
大廳
主題 達人專欄

Golang 函式的各種用法

解凍豬腳 | 2021-10-15 19:15:01 | 巴幣 3524 | 人氣 1878

 
在程式設(shè)計的領(lǐng)域裡,「把工作包裝成函式再供人呼叫」算是所有程式設(shè)計師都必須學(xué)會的事情,而「如何寫得漂亮」更是所有程式設(shè)計師一生都必須追求的目標(biāo)。今天就統(tǒng)整一些我從 2019 年開始寫 Golang 累積至今關(guān)於函式的知識吧。

本篇文章有不少程式碼,如果這裡的排版讓你感到閱讀困難,請服用 HackMD 好讀版


? 函式的定義

我們使用 func 關(guān)鍵字來定義函式,然後呼叫它:

func main() {
    result := add(555, 777)
    fmt.Println("Sum:", result) // -> Sum: 1332
}

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

這裡的 (x int, y int) 表示的是輸入兩個自變數(shù)作為 x 和 y 兩個變數(shù),緊接在後的 int 則代表這個函式回傳的值會是一個 int。

由於範(fàn)例中的自變數(shù) x 和 y 同樣都是 int 類型,我們可以省略前面連續(xù)的相同型態(tài),只要標(biāo)記最後一個就行了:

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

也就是說,如果有一個函數(shù)長這樣:

func myFunction(a, b, c int, x, y string, p int) {
    // ...
}

它就相當(dāng)於:

func myFunction(a int, b int, c int, x string, y string, p int) {
    // ...
}

在這方面,Golang 並沒有嚴格規(guī)範(fàn)是否要省略,端看你自己的選擇。

既然都說到函式了,就順帶講講檔案的管理吧。如果你的 code 規(guī)模非常小,通常我們會習(xí)慣把這些函式通通丟在同一個檔案裡,這麼做當(dāng)然無傷大雅,可是一旦內(nèi)容多了起來,你的專案就會變得難以維護,光是找個函式就要翻半天了。

因此,假如你發(fā)現(xiàn)你的 code 越寫越多,那你就可以開始適當(dāng)?shù)卣磉@些函式,把同類型的函式放到同一個檔案裡。只要它們屬於同一個 package,就可以自由呼叫:



? 函式的多重回傳值

這算是 Golang 的一種特色吧?雖然像 Python 這樣的程式語言也不是沒有,但你應(yīng)該可以在 Golang 裡面看到大量的多重回傳應(yīng)用,例如錯誤處理。錯誤處理就擱著等等說,我們可以先來看一下多重回傳的例子和應(yīng)用。

在 Golang,我們是可以一次 return 好幾個變數(shù)回來的。比如說我們希望可以寫個函數(shù) eqTriangle() 來計算正三角形的周長和面積,那麼只要在函式裡面分別計算出來,然後用括號把要回傳的型態(tài)定義括起來,就可以分成兩個變數(shù)傳回來了:

func main() {
    perimeter, area := eqTriangle(3)
    fmt.Printf("周長: %.4f\n面積: %.4f\n", perimeter, area)
}

func eqTriangle(sideLength float64) (float64, float64) {
    perimeter := sideLength * 3
    area := (math.Sqrt(3) / 4) * math.Pow(sideLength, 2)
    return perimeter, area
}

如果專案稍微大一點(或者是別人也需要用到你寫的函數(shù)的情況下),你也可以幫回傳的值命名,計算好之後直接賦值並且 return 即可:

func eqTriangle(sideLength float64) (perimeter, area float64) {
    perimeter = sideLength * 3
    area = (math.Sqrt(3) / 4) * math.Pow(sideLength, 2)
    return
}

這樣在開發(fā)程式的時候就可以直接靠 IDE 上面自動完成的提示視窗裡面看到回傳的變數(shù)名字,以便於開發(fā)者一看就知道哪個是周長、哪個是面積:


你還可以在函式的頭上寫註釋,這裡的註釋一樣也會被顯示在自動完成的提示視窗裡:


這對於開發(fā)大型專案很有用,甚至可以說是必須的。除了把函式跟變數(shù)名字取好之外,只要專案夠大、有給其他人用或是長期自用的需求,建議可以把一些複雜的函式寫上註釋,以避免日後沒有人看得懂這個函式在做什麼。


? 函式的錯誤處理

執(zhí)行程式的時候總會有些例外狀況。

Golang 並不像其他語言一樣使用 try-catch 的句法來捕捉執(zhí)行過程發(fā)生的錯誤,而是在發(fā)生問題時產(chǎn)生一個 error 對象,然後直接 return 回來。我們只要檢查本來應(yīng)該回傳 error 的地方是否為 nil,就可以知道這個函式有沒有正常運作。

舉個例子,strconv 這個 package 裡面的 Atoi 函數(shù),專門用來把字串裡的數(shù)字轉(zhuǎn)換成 int。如果這個字串不符合格式,那就會產(chǎn)生一個 error 並且傳回來:

package main

import (
    "fmt"
    "log"
    "strconv"
)

func main() {
    numberStr := "123456A"
    number, err := strconv.Atoi(numberStr)
    if err != nil {
        log.Fatal(err) // -> strconv.Atoi: parsing "123456A": invalid syntax
    }
    fmt.Printf("Number: %d\n", number)
    fmt.Printf("Number×2 = %d", number*2)
}

當(dāng)然你也可以選擇在發(fā)生錯誤的時候什麼都不做,強制把回傳的 error 直接拋掉:

func main() {
    numberStr := "123456A"
    number, _ := strconv.Atoi(numberStr)
    fmt.Printf("Number: %d\n", number)
    fmt.Printf("Number×2 = %d", number*2)
}

Atoi 這個函式本身的設(shè)計是發(fā)生錯誤的時候回傳 0 和新產(chǎn)生出來的 error,所以即使把這個 error 拋掉了,程式也不會因此崩潰或退出。

其實發(fā)生錯誤時,你有很多種方式可以選擇:
1. 讓程式把錯誤訊息印出來,並且強制退出程式(使用 log.Fatal() 或是 panic())
2. 讓程式把錯誤訊息印出來,但不強制退出程式
3. 什麼都不管,讓它繼續(xù)運作

「錯誤處理」這件事情很吃程式設(shè)計師的基本功,你得事先預(yù)想好程式本身可能會發(fā)生什麼樣的錯誤,然後把每一種情況都考慮進去。倘若沒有把發(fā)生錯誤的所有狀況預(yù)先設(shè)想好,那麼程式就可能會把錯誤的資料繼續(xù)往下接著拿來用,導(dǎo)致你的錯誤越滾越大,甚至造成不可挽回的悲慘結(jié)果。

所以我們會需要考慮什麼樣的錯誤是嚴重的、什麼樣的錯誤是不嚴重的,自行決定這個函式在發(fā)生問題時應(yīng)該直接退出程式還是單純提示錯誤訊息。這種時候我們自然就需要檢測錯誤的類型了,畢竟遇到不同的錯誤時我們可能會採取不同做法。

如果要檢測錯誤類型的話,你可以使用 errors(註)這個 package 裡面內(nèi)建的 Is 函數(shù),來檢查收到的 error 是不是特定的類型:

numberStr := "561681949849849498499"
number, err := strconv.Atoi(numberStr)
if errors.Is(err, strconv.ErrSyntax) {
    fmt.Println("數(shù)字格式有誤")
} else if errors.Is(err, strconv.ErrRange) {
    fmt.Println("數(shù)字超出了 int 可表示的範(fàn)圍")
} else if err != nil {
    fmt.Println("發(fā)生了其他不可預(yù)期的錯誤")
} else {
    fmt.Printf("Number: %d\n", number)
    fmt.Printf("Number×2 = %d", number*2)
}

註:Golang 內(nèi)建的 errors, strings, bytes 這些 package 分別是用來專門處理 error, string, byte 類型內(nèi)容的函式庫。


? 自訂錯誤內(nèi)容

承上,如果我們寫了一個需要回報各種錯誤情況的函式,我們可以用 errors.New() 來產(chǎn)生錯誤對象。

但要注意的是,每一次 errors.New() 產(chǎn)生出來的錯誤都是不同的對象,就算訊息一模一樣,errors.Is() 也會把兩個錯誤視為不同的錯誤。因此,我們需要把錯誤事先定義出來存到一個變數(shù)裡,等到真的發(fā)生錯誤而需要回傳的時候才取用:

var ErrDivisionByZero = errors.New("Cannot divide by zero.")

func main() {
    result, err := divide(5, 0)
    if errors.Is(err, ErrDivisionByZero) {
        fmt.Println("不能用 0 當(dāng)分母!")
        log.Fatal(err.Error())
    } else if err != nil {
        fmt.Println("遇到了預(yù)期之外的錯誤!")
        log.Fatal(err.Error())
    }
    fmt.Println("Result:", result)
}

func divide(x, y float64) (float64, error) {
    if y == 0 {
        return 0, ErrDivisionByZero
    }
    return (x / y), nil
}

這對於往後需要把特定的功能封裝成 package 的時候特別有用,就好像前面示範(fàn)到的 strconv.ErrSyntax,不只程式可讀性會提高很多,寫起來也會很順手。

錯誤處理說完了,接下來就講一些比較進階(或是奇怪)的用法吧。


? 把函式存進變數(shù)裡

我們可以把函式存在變數(shù)裡面,需要用到的時候再拿出來用:

square := func(i float64) float64 { return math.Pow(i, 5) }
result := square(3)


這個我個人不太常用就是了,我覺得寫起來很醜。


? 匿名函式

你可以當(dāng)場定義一個匿名函式,直接呼叫它:

func main() {
    fmt.Println("Hi")
    func(i int) {
        fmt.Println(i)
    }(48763)
    fmt.Println("Bye")
}


注意如果你的函數(shù)沒有傳入的自變數(shù),右大括號後面一樣要加上 () 表示呼叫的行為,不然它就只會是一個宣告好卻無法被使用的函數(shù)。

這在某些場景有機會用到,例如不需要佔用命名空間的簡單函式,或者是需要非同步、並行處理的函式。至於在 Golang 裡面要怎麼樣才能夠讓函式並行處理(就像多執(zhí)行緒),我們就之後再挑個好時機拿出來講吧。


? 不確定的自變數(shù)數(shù)量

如果我們希望這個函式一次傳入很多個值,但不知道具體有幾個,我們可以在型態(tài)前面使用「…」來表示不確定的數(shù)量(注意,這種不定數(shù)量的自變數(shù)必須被擺在最後面):

func main() {
    sum("總和:", 1, 2, 3, 4, 5, 6, 7, 8)
}

func sum(message string, numbers ...int) {
    sum := 0
    for _, number := range numbers { // it's like for-each
        sum += number
    }
    fmt.Println(message, sum)
}

得到的 numbers 會是一個 []int(也就是 int slice),我們只要用 for-range 的方式就可以把裡面的每一個值都取出來了。

像是我們最常用到的 fmt.Println() 和 fmt.Printf() 就是像這樣設(shè)計,我們也才能直接把很多個不確定數(shù)量的變數(shù)填進去。


? 把語句排定到函式結(jié)束時執(zhí)行

這也是 Golang 的特色。有時候我們可能一個函式裡面會出現(xiàn)很多個 return,同時又有些任務(wù)是 return 前必做的(例如關(guān)閉跟資料庫之間的連線、關(guān)閉檔案等等),那麼我們就可以用 defer 來把函式排到函數(shù)結(jié)束時執(zhí)行,避免一直把類似的東西複製貼上,也減少了被漏掉的風(fēng)險:


如果需要執(zhí)行的事情有好幾句,那剛才提到的匿名函式就派上用場了:


那要是一個函式裡面有很多個 defer,較晚 defer 的會先被執(zhí)行:


除此之外,panic 的時候會執(zhí)行完所有層級的 defer,然後才離開程式(log.Fatal 的話就不會執(zhí)行 defer 了,印完錯誤訊息之後直接退出程式)。

關(guān)於函式的各種眉角,大概就是這樣了。這系列的下一篇可能是關(guān)於迭代和陣列,可能是關(guān)於 package 的用法,也可能是我最近搞哈哈姆特 BOT 弄出來的小成果,就讓我再花點時間決定吧……




縮圖素材原作者:Renée French(CC BY-SA 3.0)
送禮物贊助創(chuàng)作者 !
83
留言

創(chuàng)作回應(yīng)

NTR是什麼
才華洋溢...[e11]
2021-10-15 19:38:10
解凍豬腳
我身上洋溢著藝術(shù)家氣息
2021-10-18 07:32:29
? 勳章向創(chuàng)作者進行贊助 ?
2021-10-28 11:01:09
解凍豬腳
感謝勳章大~ [e38]
2021-10-29 11:45:10
費玟
那個匿名函式最後的()讓我想起了JavaScript 呢…
宣告完直接呼叫它的感覺

話說宣告ErrDivisionByZero的時候為什麼要首字大寫?
是因為他是var?還是因為要註明他是error.new出來的常數(shù)?
2022-04-17 01:21:25
解凍豬腳
在 Golang 裡面,如果一個 var 或 func 的名稱是首字大寫,那麼它就會是一個 exported 的變數(shù),可以被其他的 package 存取

既然我們要封裝成一個 package,別人在引用它的時候總也有需要錯誤處理的情境,比如我們今天使用 strconv 這個 package,那麼當(dāng)我們需要 handle 從它那邊拋出來的錯誤,就會有類似這種情形:


// 在 main package 裡面使用 strconv 的功能

str := "test123"
result, err := strconv.Atoi(str)
if errors.Is(err, strconv.ErrSyntax) {
fmt.Println("An error occured")
}

如果今天 strconv 裡面把 ErrSyntax 取名叫做 errSyntax 的話,這個 error 的對象就會是 unexported,不能被其他的 package 引用,那這樣 strconv 裡面的 errSyntax 就等於白定義了、有寫跟沒寫一樣,畢竟只有 strconv 內(nèi)部自己能用
2022-04-17 01:42:21
費玟
哦哦哦哦了解
難怪每個第三方package的function都是首字大寫
感覺有點像java在定義存取權(quán)限那樣

不過在Go這裡是只有首字大寫的才能被別人用啊…
會用這種方式區(qū)分還滿酷的 真有特色

也許這就是Go的可讀性那麼高的原因吧…
不像java宣告?zhèn)€變數(shù)/寫個function囉哩囉嗦
2022-04-17 01:50:39
解凍豬腳
TEST
2022-04-18 17:36:35
解凍豬腳
1234567
4567898
6541948
[e13]
2022-04-18 17:37:54
追蹤 創(chuàng)作集

作者相關(guān)創(chuàng)作

更多創(chuàng)作