chapter5、6 golang的函數與方法

函數

//常規的函數定義
func 方法名(參數列表) 返回值 {
    定義
}

函數的值(閉包)

在Go中,函數被看作第一類值(first-class values):函數像其他值一樣,擁有類型,可以被賦值給其他變量,傳遞給函數,從函數返回。函數類型的零值是nil。調用值爲nil的函數值會引起panic錯誤:

var f func(int) int
f(3) // 此處f的值爲nil, 會引起panic錯誤

函數值不僅僅是一串代碼,還記錄了狀態。Go使用閉包(closures)技術實現函數值,Go程序員也把函數值叫做閉包。我們看個閉包的例子:

func f1(limit int) (func(v int) bool) {
    //編譯器發現limit逃逸了,自動在堆上分配
    return func (v int) bool { return v>limit} 
}

func main() {
    closure := f1(5)
    fmt.Printf("%v\n", closure(1)) //false
    fmt.Printf("%v\n", closure(5)) //false
    fmt.Printf("%v\n", closure(10)) //true
}

在這個例子中,f1函數傳入limit參數,返回一個閉包,閉包接受一個參數v,判斷v是否大於之前設置進去的limit。

可變參數列表

可變參數,即參數不是固定的,例如fmt.Printf函數那樣,注意只有最後一個參數纔可以是聲明爲可變參數,聲明:

func 函數名(變量名...類型) 返回值

我們看個例子:

package main

import (
    "fmt"
)

func f1(name string, vals... int) (sum int) {
    for _, v := range vals {
        sum += v
    }
    sum += len(name)
    return
}

func main() {
    fmt.Printf("%d\n", f1("abc", 1,2,3,4 )) //13
}

函數的延遲執行 defer

包含defer語句的函數執行完畢後(例如return、panic),釋放堆棧前會調用被聲明defer的語句,常用於釋放資源、記錄函數執行耗時等,有一下幾個特點:

  1. 當defer被聲明時,其參數就會被實時解析
  2. 執行順序和聲明順序相反
  3. defer可以讀取有名返回值

看個例子:

package main

import (
    "fmt"
)

//演示defer的函數可以訪問返回值
func f2() (v int) {
    defer func (){ v++}()
    return 1 //執行這個時,把v置爲1
}

//演示defer聲明即解釋
func f3(i int) (v int) {
    defer func(j int) { v += j} (i) //此時函數i已被解析爲10,後面修改i的值無影響
    v = i
    i = i*2
    return
}

//演示defer的執行順序,與聲明順序相反
func f4() {
    defer func() {fmt.Printf("first\n")} ()
    defer func() {fmt.Printf("second\n")} ()
}

func main() {
    fmt.Printf("%d\n", f2()) // 13
    fmt.Printf("%d\n", f3(10)) // 20
    f4() //second\nfirst\n
}

典型的使用場景,函數執行完畢關閉資源:

func do() error {
    f, err := os.Open("book.txt")
    if err != nil {
        return err
    }

    defer func(f io.Closer) {
        if err := f.Close(); err != nil {
            // log etc
        }
    }(f)

    // ..code...

    f, err = os.Open("another-book.txt")
    if err != nil {
        return err
    }

    defer func(f io.Closer) {
        if err := f.Close(); err != nil {
            // log etc
        }
    }(f)

    return nil
}

在這裏例子中可以看到,我們判斷了Close()是否成功,因爲在一些文件系統中,尤其是NFS,寫文件出錯往往被延遲到Close的時候才反饋,所以必須檢查Close的狀態。

異常panic

Go有別於那些將函數運行失敗看作是異常的語言。雖然Go有各種異常機制,但這些機制僅僅用於嚴重的錯誤,而不是那些在健壯程序中應該被避免的程序錯誤。runtime在一些情況下會拋出異常,例如除0,我們也能使用panic關鍵字自己拋出異常

panic(異常的值) //值是啥都行

出現異常之後,默認情況就是程序退出並打印堆棧:

package main


func f6() {
    func () {
        func () int {
            x := 0
            y := 5/x
            return y
        }()
    }()
}

func main() {

    f6()

}

輸出

panic: runtime error: integer divide by zero

goroutine 1 [running]:
main.f6.func1.1(...)
    /Users/kitmanzheng/study/go/src/test_func.go:8
main.f6.func1()
    /Users/kitmanzheng/study/go/src/test_func.go:10 +0x11
main.f6()
    /Users/kitmanzheng/study/go/src/test_func.go:11 +0x20
main.main()
    /Users/kitmanzheng/study/go/src/test_func.go:16 +0x20
exit status 2

如果不想程序退出的話,也有辦法,就是使用recover捕捉異常,然後返回error。在沒發生panic的情況下,調用recover會返回nil,發生了panic,那麼就是panic的值。看個例子:

package main

import (
    "fmt"
)

type shouldRecover struct{}
type emptyStruct struct{}

func f6() (err error) {
    defer func () {
        switch p := recover(); p {
            case nil: //donoting
        case shouldRecover{}:
            err = fmt.Errorf("occur panic but had recovered")
        default:
            panic(p)
        }
    } ()

    func () {
        func () int {
            panic(shouldRecover{})
            //panic(emptyStruct{})
            x := 0
            y := 5/x
            return y
        }()
    }()

    return
}


func main() {
    err := f6()
    if err != nil {
        fmt.Printf("fail %v\n", err)
    } else {
        fmt.Printf("success\n")
    }
}

輸出
fail occur panic but had recovered
在這個例子中,我們手動拋出一個panic,值是shouldRecover,然後外層使用defer + 匿名函數 + recover去捕捉異常,發現panic的值是shouldRecover那麼就不退出,而是返回error。

方法

//這種只能給type定義的類型用 
func (type類型參數) 方法名(參數列表) 返回值 {
    定義
}
//eg:
func (t TestType) testFunc() int {
    //...
}

例子中t稱爲接收器,可以是該類型本身,或該類型的指針,由於是值傳遞,所以是接收器是該類型時,會複製值,類型比較大時開銷大,可以選擇使用指針降低開銷。而且在使用defer的時候,由於值複製,如果不用指針,變量發生了變化,但是defer運行時還是基於老變量運行的,容易會造成一些坑,除非你明確知道自己要這麼做。建議func (*type)而不是func(type)。但是如果一個類型低層實際是一個指針,那麼不允許在使用該類型的指針作爲接收器。

當我們使用指針作爲接收器時,記得檢查是否是nil

非常重要的一點是,T使用接收者是T和*T的方法,而T只使用接收者是T的方法,T能直接調接受者是*T方法,僅僅是一個語法糖,編譯器幫我們取地址了

看下面這個例子:

type myInt struct {
    owner string
    value int
}

func (a myInt) Owner(suffix string) string { //golang不支持默認參數
    return a.owner + suffix
}

func (a *myInt) SetOwner(owner string) {
    if a == nil {
        fmt.Println("set owner to nil point is invalid")
        return
    }
    a.owner = owner
}

func (a myInt) SetOwner2(owner string) { //golang函數參數按值傳遞,所以這個方法實際只是修改臨時變量的owner
    a.owner = owner
}

func SetOwner3(a *myInt, owner string) {
    if a == nil {
        fmt.Println("set owner to nil point is invalid")
        return
    }
    a.owner = owner
}

func main() {
    var k = myInt{"kitman", 3}

    fmt.Print(k.value, " ", k.Owner("aa"), "\n") //輸出3 kitmanaa

    k.SetOwner("ak") //相當於SetOwner(&k, "ak")
    fmt.Print(k.value, " ", k.Owner("bb"), "\n") //輸出3 akbb

    k.SetOwner2("sss")  //相當於SetOwner(k, "sss")
    fmt.Print(k.value, " ", k.Owner("bb"), "\n") //輸出3 akbb

    SetOwner3(&k, "sss")
    fmt.Print(k.value, " ", k.Owner("bb"), "\n") //輸出3 sssbb

    var k2 *myInt = nil
    k2.SetOwner("aa")     //輸出set owner to nil point is invalid
}

輸出
3 kitmanaa
3 akbb
3 akbb
3 sssbb
set owner to nil point is invalid
通過上面的例子,我們可以發現一些知識點:

  1. 使用第二種函數定義的方法,那麼就和c++的類差不多。本質上和普通函數一樣,就是語法上的差別而已。
  2. 就算給type類型定義方法,函數參數也是按值傳遞的,所以type參數使用指針才能修改變量。
  3. nil指針也能調用方法,但是如果方法裏面沒判斷指針是否是nil,那麼就會core

面向對象繼承語義

可以通過使用匿名成員 + 定義方法,實現部分繼承的語義:

package main

import (
    "fmt"
)

type Base struct {
    y int
    Y int
}

func (b *Base) FuncByPoint() int {
    if (b == nil) {
        return 0;
    }

    return b.y*b.Y
}

func (b Base) FuncByValue() int {
    return b.y*b.Y
}

type Child struct {
    Base
    x int
    X int
}

func (c *Child) FuncByPoint() int {
    if (c == nil) {
        return 0
    }

    return c.x*c.X
}

func main() {
    var c Child
    c.y = 2
    c.Y = 3
    fmt.Printf("%v\n", c.FuncByPoint())          //0
    fmt.Printf("%v\n", c.Base.FuncByPoint())//6
    fmt.Printf("%v\n", c.FuncByValue()).  //6

    var f1 func() int
    f1 = c.FuncByPoint
    fmt.Printf("%v\n", f1())   //0

    var f2 func(*Child) int
    f2 = (*Child).FuncByPoint
    fmt.Printf("%v\n", f2(&c)) //0
}

這個例子可以看到,Base中定義的方法,被外層的同名方法覆蓋,需要顯式指明才能調用到Base中的方法。注意golang中不存在真正的繼承,這是嵌入匿名成員,用匿名成員的方法去理解這樣的語法。另外,方法的值也是第一類變量,能賦值給別的變量,比c/c++靈活,golang無論是對象方法,還是類型的方法,都能賦值給別的變量,可以參照例子中的寫法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章