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 (limit 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的語句 ,常用於釋放資源、記錄函式執行耗時等,有一下幾個特點:
- 當defer被宣告時,其引數就會被實時解析
- 執行順序和宣告順序相反
- 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。
看下面這個例子:
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
通過上面的例子,我們可以發現一些知識點:
- 使用第二種函式定義的方法,那麼就和c++的類差不多。本質上和普通函式一樣,就是語法上的差別而已。
- 就算給type型別定義方法,函式引數也是按值傳遞的,所以type引數使用指標才能修改變數。
- 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無論是物件方法,還是型別的方法,都能賦值給別的變數 ,可以參照例子中的寫法。