函數
函數聲明
函數由5部分組成:函數名、形參列表、返回列表、和函數體。func
為定義函數的關鍵字
func name(parameters-list) (result-list) {
body
}
形參列表的格式是參數名稱+參數類型,相同類型的參數可以寫在一起
// 這兩種個寫法等價的
func f(x, y float64) float64 {}
func f(x float64, y float64) float64 {}
返回列表的格式是(返回值名稱+返回值類型...)
返回值名稱可以省,當函數存在返回列表時,必須顯示地已return語句結束
// 方式一:返回值名稱+類型
func sum(x, y int) (z int) {
z = x + y // 變量z已經被聲明
return // 必須顯示地以return結束,可以不用指出返回的變量,因為函數第一行已寫
}
// 方式二:只有返回類型
func sum2(x, y int) int {
z := x + y // 注意 這里z要初始化
return z
}
// 如果有多個返回值,需要用括號包起來
func foo() (x, y int) {
x, y = 1, 1
return
}
func foo2() (int, int) {
x, y := 1, 1
return x, y
}
函數的類型稱作函數簽名
,由函數的形參列表和返回列表確定,形參和返回值名稱不會形象函數類型
func add(x, y int) int { return x + y }
func sub(a int, b int) (c int) { c = a - b; return }
fmt.Printf("%T\n", add) // func(int, int) int
fmt.Printf("%T\n", sub) // func(int, int) int
我們可以只定義函數簽名,函數實現放在其他地方或其他語言
func Sin(x float64) float64
函數的形參
形參變量是函數的局部變量。通常情況下,調用函數時實參是按值
傳遞的,因此函數內修改變量不會改變實參的值。
func incr(x int) {
x++
}
func main() {
a := 1
incr(a)
fmt.Println(a) // 1
}
但是,如果實參是引用類型,比如:指針、slice、map、函數或者通道,那么就有可能
改到實參的值
func updateSlice(s []int, index int, val int) {
s[index] = val
}
func updateMap(m map[int]int, k int, v int) {
m[k] = v
}
func main() {
s1 := []int{1, 1, 3, 4, 5}
updateSlice(s1, 1, 2)
fmt.Println(s1) // [1 2 3 4 5]
m1 := map[int]int{0: 1, 1: 1, 2: 3}
updateMap(m1, 0, 100)
fmt.Println(m1) // map[0:100 1:1 2:3]
}
函數的遞歸
遞歸實現斐波那契數列
func fib(n int) int {
if n < 2 {
return n
}
return fib(n-1) + fib(n-2)
}
func main() {
fib(5) // 5
}
遞歸的實現使用棧結構來保存當前上下文信息。Go語言的實現使用了可變長的棧,棧的長度可以隨着使用增加。
函數多指返回
Go語言支持函數的返回值不止一個,一般情況是一個期望計算得到的結果和一個錯誤值或者一個表示函數調用是否正確的布爾值。
func calculate(expr string) (result float64, err error) {
...
}
func main() {
res, err := calculate("30*50")
}
函數變量
Go語言中,可以聲明函數類型的變量,即函數變量。函數變量之間不能比較,只能和nil
比較。
var sum func(int, int) int
fmt.Printf("%T\n", sum) // func(int, int) int
sum = func(a, b int) int {
return a + b
}
fmt.Println(sum(1, 1)) // 2
var f func(int, int) int
if f != sum { // 編譯錯誤 f != sum (func can only be compared to nil)
f = sum
}
函數變量可以作為參數傳遞
func add1(r rune) rune { return r + 1 } // 將字符的Unicode值加1
fmt.Println(strings.Map(add1, "HAL-9000")) // IBM.:111
可以在函數內部聲明遞歸函數
func main() {
var fib func(int) int
fib := func(n int) int {
if n < 2 {
return n
}
return fib(n-1) + fib(n-2)
}
fmt.Println(fib(10)) // 5
}
/*下面的寫法是錯誤的
func main() {
func fib(n int) int {
if n < 2 {
return n
}
return fib(n-1) + fib(n-2)
}
fmt.Println(fib(10)) // 5
}
*/
變長函數
函數可以支持可變的參數數量,比如:fmt.Printf
就是支持可變的數量。在參數列表最后的類型名稱前使用...
表示聲明一個邊長函數,下面我們來實現一個簡易的Sprintf
:
func Sprintf(format string, params ...interface{}) string {
i, j := 0, 0
s := ""
for i < len(format)-1 {
if format[i] == '%' && format[i+1] == 'd' {
s = s + strconv.Itoa(params[j])
j++
i++
} else {
s = s + string(format[i])
}
i++
}
return s
}
func main() {
var s string
s = Sprintf("%d+%d=%d", 1, 2, 3)
fmt.Println(s) // 1+2=3
s = Sprintf("%d+%d+%d=%d", 1, 2, 3, 6)
fmt.Println(s) // 1+2+3=6
fmt.Printf("%T", Sprintf) // func(string, ...int) string
}
可變長度參數只能聲明在最后,並且只能有一個,這樣就限制了可變參數只能是一種類型,但是fmt.Printf
可以這樣寫:
fmt.Printf("%d %s", 1, "abc") // 1 abc
這是因為它將可變長度參數的類型聲明生成了interface{}
,將會在后面的章節研究。
延遲函數
在一個函數調用或者方法調用前加上defer
關鍵字,就聲明了這個函數(方法)延遲執行
- 延遲到return語句后執行
- 延遲到函數執行完畢后執行
- 延遲到發生宕機時執行
在一個函數作用域內,可以有聲明多次延遲函數,執行的時候是以調用defer
語句順序的倒序進行。
延遲函數一般用於聲明函數正常或異常結束后釋放資源。
conn, err := Client.GetConn()
defer coon.Close()
...
此外,還可以結合閉包
實現對一個函數執行時的監控
func clock(msg string) func() {
start := time.Now()
fmt.Printf("enter %s\n", msg)
// 因為匿名函數可以得到其外層函數作用域內的變量(包括命名的結果)
return func() { fmt.Printf("exit %s (%s)\n", msg, time.Since(start)) }
}
func SlowFunc() {
defer clock("SlowFunc")()
time.Sleep(3 * time.Second)
}
函數的宕機和恢復
宕機發生在程序的運行時出現了嚴重的異常情況,比如:錯誤的輸入、配置或者I/O失敗等。此時程序執行會終止,goroutine
中的所有延遲函數會執行,然后程序會異常退出。
一些標准庫會對不可能發生
的情況做宕機處理,我們自己也可以同宕機函數 panic
來實現:
switch isRight {
case true: //...
case false: // ...
default:
panic("invalid")
}
有些情況下,當程序發生宕機,我們也不期望程序退出,比如,當Web服務器遇到處理用戶請求時遇到宕機情況,不能直接退出,而是要給用戶返回當前遇到的錯誤:
- 如果是用戶查詢的記錄不存在,應該返回404
- 如果是用戶輸入的參數有問題,應該返回400
...
我們可以通過在函數的延遲函數中調用recover
函數來終止當前的宕機狀態並做一些邏輯處理
func RequestHandler(c *Context) (res Response) {
defer func() {
switch p := recover(); p {
case notFound{}:
res = NotFoundRes{}
case invalidParam{}:
res = InvalidParamRes{}
default:
res = InternalErrorRes{}
}
}
//具體處理邏輯
//...
return
}
需要注意的是,要合理評估當前情況是否需要對宕機進行恢復,恢復會有一定風險,比如導致資源泄露或使失敗的處理函數處於未定義的狀態從而導致其他問題。