Go語言-函數

函數定義

函數是結構化編程的最小模塊單元,使用關鍵字‘func’定義函數。Go語言定義函數的一些特點總結如下:
- 無需前置聲明
- 不支持命名嵌套定義
- 不支持同名函數重載
- 不支持默認參數
- 支持不定長變參
- 支持多返回值
- 支持命名返回值
- 支持匿名函數和閉包
函數屬於第一類對象,具備相同簽名(參數及返回值類型)的視爲同一類型。
定義一個函數:
func test(x int,s string)int{} //其中func爲關鍵字,test爲函數名,(x int,s string)爲參數列表,int爲返回值類型

package main
import(
    "fmt"
)
func hello(){                 //定義一個名爲hello的函數,參數列表爲空返回值爲空
    fmt.Println("hello word!")
}
func main(){                 //main函數程序的入口
    hello()
}

函數只能判斷其是否爲nil,不支持其它比較操作,從函數返回局部變量指針是安全的。

package main
import(
    "fmt"
)
func test()*int{
    a := 10
    return &a
}
func main(){
    var a *int = test()
    fmt.Println(a,*a)
}

輸出:
0xc04203e1d0 10

參數

Go語言不支持有默認值得可選參數,不支持命名實參。調用時,必須按簽名順序傳遞指定類型和數量的實參,就算以“_”忽略的也不能省略掉。
不管是指針、引用類型,還是其他類型參數,都是值拷貝傳遞。如果函數參數過多,建議將其定義爲一個複合結構類型。

變參

變參本質上就是一個切片。只能接受一個到多個同類型參數,並且放在列表尾部。

package main
import(
    "fmt"
)
func test(s string,a ...int){      //定義一個不定參數的函數,注意'...'不可省略
    fmt.Printf("%s,%T,%v\n",s,a,a) //輸出類型及值
}
func main(){
    test("adb",1,2,3,4)
}

輸出:
adb,[]int,[1 2 3 4]
既然變參是切片,那麼參數複製的僅是切片自身,並不包含底層數組,因此可修改原數據。如果需要可使用內置函數copy複製底層數據。

package main
import(
    "fmt"
)
func test(a ...int){
    for i := range a{
        a[i] += 10
    }
}
func main(){
    a := []int{1,2,3,4}
    test(a...)
    fmt.Println(a)
}

輸出:
[11 12 13 14]

返回值

有返回值得函數必須有明確的return語句,除非有panic或者無break的死循環則無需return語句。

package main
import(
    "fmt"
    "errors"
)
func test(x,y int)(int,error){    //函數多返回值
    if 0 == y{
        return 0,errors.New("Can not be zero")
    }
    return x/y ,nil
}
func main(){
    a,ret := test(2,4)
    if nil != ret{
        fmt.Println(ret)
    }else{
        fmt.Println(a)
    }
}

輸出:
0

命名返回值

使用命名返回值,可直接使用return隱式返回,如果返回值類型能明確表明其含義,就儘量不要對其命名

package main
import(
    "fmt"
    "errors"
)
func test(x,y int)(a int,err error){   //顯示的定義了函數返回值
    if 0 == y{
        err = errors.New("Can not be zero")
        return
    }
    a = x/y
    return 
}
func main(){
    a,ret := test(4,0)
    if nil != ret{
        fmt.Println(ret)
    }else{
        fmt.Println(a)
    }
}

輸出:
Can not be zero

匿名函數

匿名函數是指沒有定義名字的函數。除沒有名字外,匿名函數和普通函數完全相同。最大的區別是我們可在函數內部定義匿名函數,形成類似嵌套效果。匿名函數可直接調用,保存到變量,作爲參數或返回值。
直接執行

package main
import(
    "fmt"
)
func main(){
    func(s string){      //匿名函數,無函數名
        fmt.Println(s)
    }("hello word")
}

輸出:
hello word
賦值給變量

package main
import(
    "fmt"
)
func main(){
    add := func(x,y int)int{
        return x+y
    }
    fmt.Println(add(1,2))
}

輸出:
3
作爲參數:

package main
import(
    "fmt"
)
func test(f func()){
    f()
}
func main(){
    test(func(){
        fmt.Println("hello")
    })
}

輸出:
hello
作爲返回值

package main
import(
    "fmt"
)
func test() func(int,int)int{
    return func(x,y int)int{
        return x+y
    }
}
func main(){
    add := test()
    fmt.Println(add(1,2))
}

輸出:
3
普通函數和匿名函數都可以作爲結構體字段,或經通道傳遞。不曾使用的匿名函數會被編譯器當做錯誤。
閉包
閉包(closure)是在其詞法上下問引用了自由變量的函數,或者說是函數和其引用的環境組合體。

package main
import(
    "fmt"
)
func test(x int) func(){
    return func(){
        fmt.Println(x)
    }
}
func main(){
    f := test(123)
    f()
}

輸出:123
就以上代碼而言,test返回的匿名函數會引用上下文環境變量x。當該函數在main中執行時,它依然可正確讀取x的值,這種現象就稱作閉包。閉包直接引用了原環境變量。
正因爲閉包通過指針引用環境變量,那麼可能會導致其生命週期延長,甚至被分配到堆內。解決的辦法就是每次用不同的環境變量或傳參複製,讓各自閉包環境各不相同。

延遲調用

語句defer向當前函數註冊稍後執行的函數調用。這些函數被稱作延遲調用,因爲它們直到當前函數函數執行結束前才被執行,常用語資源釋放、解除鎖定、以及錯誤處理等操作。

func main(){
    f,err := os.Open("./test.go")
    if err != nil{
        log.Fatalln(err)
    }
    defer f.Close()    //僅註冊,直到mian函數退出之前才執行
    ...do something...
}

注意:延遲調用註冊的是調用,必須提供執行所需參數(哪怕爲空)。參數值在註冊時被複制並緩存起來。如對狀態敏感,可改用指針或閉包。

package main
import(
    "fmt"
)
func main(){
    x,y := 1,2
    defer func(a int){
        fmt.Println("defer x,y = ",a,y)
    }(x)
    x += 10
    y += 20
    fmt.Println(x,y)
}

輸出:
11 22
defer x,y = 1 22
延遲調用可修改當前函數命名返回值,但其自身返回值被拋棄,多個延遲註冊按FILO次序(先進後出)執行。

錯誤處理

error
官方推薦的標準做法是返回error狀態
func test(a …interface{})(n int,err error)
標準庫將error定義爲接口類型,以便實現自定義錯誤類型。按照慣例,error總是最後一個返回參數。標準庫提供了相關創建函數,可方便的創建包含簡單錯誤文本的error對象。

package main
import(
    "fmt"
    "errors"
)
func test(x,y int)(a int,err error){   
    if 0 == y{
        err = errors.New("Can not be zero")//設置了返回錯誤
        return
    }
    a = x/y
    return 
}
func main(){
    a,ret := test(4,0)
    if nil != ret{
        fmt.Println(ret)
    }else{
        fmt.Println(a)
    }
}

某些時候我們需要自定義錯誤類型,以容納更多上下文狀態信息。這樣做還可以基於類型做出判斷

package main
import(
    "fmt"
)
type DivError struct{    //自定義錯誤類型
    x,y int
}
func (DivError) Error() string{  //實現Error接口方法
    return "division by zero"
}
func div(x,y int)(int,error){
    if y == 0{
        return -1,DivError{x,y}    //返回自定義錯誤類型
    }
    return x/y,nil
}
func main(){
    z,err := div(5,0)
    if err != nil{
        switch e := err.(type){     //根據錯誤類型匹配
            case DivError:
                fmt.Println(e,e.x,e.y)
            default:
                fmt.Println(e)
        }
    }
    fmt.Println(z)
}

輸出:
division by zero 5 0
-1
panic,recover
與error相比,panic/recover在使用方法更接近try/catch結構化異常。但是它們是內置函數而非語句。panic會立即中斷當前函數流程,執行延遲調用。而在延遲調用函數中recover可捕獲並返回panic提交的錯誤對象。

func main(){
    defer func(){
        if err := recover(); err != nil{ //捕獲錯誤
            fmt.Println("erro")
        }
    }()
    panic("dead")                        //引發錯誤
    fmt.Println("exit")                  //永不會執行
}

因爲panic參數是空接口類型,因此可使用任何對象作爲錯誤狀態。而recover返回結果同樣要做轉型才能獲得具體信息。
無論是否執行recover,所有延遲調用都會被執行。但中斷性錯誤會沿用堆棧向外傳遞,要麼被外層捕獲,要麼進程崩潰。連續的調用panic,僅最後一個會被recover捕獲。
在延遲函數中panic,不會影響後續延遲調用執行。而recover之後panic,可被再次捕獲。另外,recover必須在延遲調用 函數中執行才能正常工作。
建議:除非是不可恢復性、導致系統無法正常工作的錯誤,否則不建議使用panic

發佈了27 篇原創文章 · 獲贊 6 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章