ETH官方钱包

前往
大廳
主題 達(dá)人專欄

Golang 函式的各種用法

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

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

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


? 函式的定義

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

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) 表示的是輸入兩個(gè)自變數(shù)作為 x 和 y 兩個(gè)變數(shù),緊接在後的 int 則代表這個(gè)函式回傳的值會(huì)是一個(gè) int。

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

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

也就是說(shuō),如果有一個(gè)函數(shù)長(zhǎng)這樣:

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 並沒有嚴(yán)格規(guī)範(fàn)是否要省略,端看你自己的選擇。

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

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



? 函式的多重回傳值

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

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

func main() {
    perimeter, area := eqTriangle(3)
    fmt.Printf("周長(zhǎng): %.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
}

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

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

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


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


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


? 函式的錯(cuò)誤處理

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

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

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

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ā)生錯(cuò)誤的時(shí)候什麼都不做,強(qiáng)制把回傳的 error 直接拋掉:

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

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

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

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

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

如果要檢測(cè)錯(cuò)誤類型的話,你可以使用 errors(註)這個(gè) package 裡面內(nèi)建的 Is 函數(shù),來(lái)檢查收到的 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ù)期的錯(cuò)誤")
} else {
    fmt.Printf("Number: %d\n", number)
    fmt.Printf("Number×2 = %d", number*2)
}

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


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

承上,如果我們寫了一個(gè)需要回報(bào)各種錯(cuò)誤情況的函式,我們可以用 errors.New() 來(lái)產(chǎn)生錯(cuò)誤對(duì)象。

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

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ù)期之外的錯(cuò)誤!")
        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
}

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

錯(cuò)誤處理說(shuō)完了,接下來(lái)就講一些比較進(jìn)階(或是奇怪)的用法吧。


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

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

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


這個(gè)我個(gè)人不太常用就是了,我覺得寫起來(lái)很醜。


? 匿名函式

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

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


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

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


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

如果我們希望這個(gè)函式一次傳入很多個(gè)值,但不知道具體有幾個(gè),我們可以在型態(tài)前面使用「…」來(lá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 會(huì)是一個(gè) []int(也就是 int slice),我們只要用 for-range 的方式就可以把裡面的每一個(gè)值都取出來(lái)了。

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


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

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


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


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


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

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




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

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

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

話說(shuō)宣告ErrDivisionByZero的時(shí)候?yàn)槭颤N要首字大寫?
是因?yàn)樗莢ar?還是因?yàn)橐]明他是error.new出來(lái)的常數(shù)?
2022-04-17 01:21:25
解凍豬腳
在 Golang 裡面,如果一個(gè) var 或 func 的名稱是首字大寫,那麼它就會(huì)是一個(gè) exported 的變數(shù),可以被其他的 package 存取

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


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

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

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

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

也許這就是Go的可讀性那麼高的原因吧…
不像java宣告?zhèn)€變數(shù)/寫個(gè)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)作