函數(shù)
//常規(guī)的函數(shù)定義
func 方法名(參數(shù)列表) 返回值 {
定義
}
函數(shù)的值(閉包)
在Go中各墨,函數(shù)被看作第一類值(first-class values):函數(shù)像其他值一樣,擁有類型启涯,可以被賦值給其他變量贬堵,傳遞給函數(shù)恃轩,從函數(shù)返回。函數(shù)類型的零值是nil黎做。調(diào)用值為nil的函數(shù)值會引起panic錯誤:
var f func(int) int
f(3) // 此處f的值為nil, 會引起panic錯誤
函數(shù)值不僅僅是一串代碼叉跛,還記錄了狀態(tài)。Go使用閉包(closures)技術(shù)實現(xiàn)函數(shù)值蒸殿,Go程序員也把函數(shù)值叫做閉包筷厘。我們看個閉包的例子:
func f1(limit int) (func(v int) bool) {
//編譯器發(fā)現(xiàn)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函數(shù)傳入limit參數(shù)酥艳,返回一個閉包,閉包接受一個參數(shù)v爬骤,判斷v是否大于之前設(shè)置進去的limit充石。
可變參數(shù)列表
可變參數(shù),即參數(shù)不是固定的霞玄,例如fmt.Printf函數(shù)那樣骤铃,注意只有最后一個參數(shù)才可以是聲明為可變參數(shù),聲明:
func 函數(shù)名(變量名...類型) 返回值
我們看個例子:
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
}
函數(shù)的延遲執(zhí)行 defer
包含defer語句的函數(shù)執(zhí)行完畢后(例如return坷剧、panic)惰爬,釋放堆棧前會調(diào)用被聲明defer的語句,常用于釋放資源惫企、記錄函數(shù)執(zhí)行耗時等撕瞧,有一下幾個特點:
- 當(dāng)defer被聲明時,其參數(shù)就會被實時解析
- 執(zhí)行順序和聲明順序相反
- defer可以讀取有名返回值
看個例子:
package main
import (
"fmt"
)
//演示defer的函數(shù)可以訪問返回值
func f2() (v int) {
defer func (){ v++}()
return 1 //執(zhí)行這個時,把v置為1
}
//演示defer聲明即解釋
func f3(i int) (v int) {
defer func(j int) { v += j} (i) //此時函數(shù)i已被解析為10,后面修改i的值無影響
v = i
i = i*2
return
}
//演示defer的執(zhí)行順序,與聲明順序相反
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
}
典型的使用場景雅任,函數(shù)執(zhí)行完畢關(guā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()是否成功,因為在一些文件系統(tǒng)中沪么,尤其是NFS硼婿,寫文件出錯往往被延遲到Close的時候才反饋,所以必須檢查Close的狀態(tài)禽车。
異常panic
Go有別于那些將函數(shù)運行失敗看作是異常的語言寇漫。雖然Go有各種異常機制,但這些機制僅僅用于嚴重的錯誤殉摔,而不是那些在健壯程序中應(yīng)該被避免的程序錯誤州胳。runtime在一些情況下會拋出異常,例如除0栓撞,我們也能使用panic關(guān)鍵字自己拋出異常
panic(異常的值) //值是啥都行
出現(xiàn)異常之后,默認情況就是程序退出并打印堆棧:
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挽懦。在沒發(fā)生panic的情況下,調(diào)用recover會返回nil木人,發(fā)生了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 + 匿名函數(shù) + recover去捕捉異常蒲列,發(fā)現(xiàn)panic的值是shouldRecover那么就不退出,而是返回error蝗岖。
方法
//這種只能給type定義的類型用
func (type類型參數(shù)) 方法名(參數(shù)列表) 返回值 {
定義
}
//eg:
func (t TestType) testFunc() int {
//...
}
例子中t稱為接收器,可以是該類型本身抵赢,或該類型的指針,由于是值傳遞划提,所以是接收器是該類型時,會復(fù)制值鹏往,類型比較大時開銷大,可以選擇使用指針降低開銷伊履。而且在使用defer的時候款违,由于值復(fù)制,如果不用指針插爹,變量發(fā)生了變化,但是defer運行時還是基于老變量運行的,容易會造成一些坑溢陪,除非你明確知道自己要這么做。建議func (*type)而不是func(type)。但是如果一個類型低層實際是一個指針杉编,那么不允許在使用該類型的指針作為接收器。
當(dāng)我們使用指針作為接收器時嘶朱,記得檢查是否是nil光酣。
非常重要的一點是疏遏,T使用接收者是T和*T的方法救军,而T只使用接收者是T的方法,T能直接調(diào)接受者是*T方法,僅僅是一個語法糖唱遭,編譯器幫我們?nèi)〉刂妨?/strong>。
看下面這個例子:
type myInt struct {
owner string
value int
}
func (a myInt) Owner(suffix string) string { //golang不支持默認參數(shù)
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函數(shù)參數(shù)按值傳遞,所以這個方法實際只是修改臨時變量的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") //相當(dāng)于SetOwner(&k, "ak")
fmt.Print(k.value, " ", k.Owner("bb"), "\n") //輸出3 akbb
k.SetOwner2("sss") //相當(dāng)于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
通過上面的例子疫鹊,我們可以發(fā)現(xiàn)一些知識點:
- 使用第二種函數(shù)定義的方法司致,那么就和c++的類差不多。本質(zhì)上和普通函數(shù)一樣脂矫,就是語法上的差別而已。
- 就算給type類型定義方法奕枢,函數(shù)參數(shù)也是按值傳遞的,所以type參數(shù)使用指針才能修改變量缝彬。
- nil指針也能調(diào)用方法哺眯,但是如果方法里面沒判斷指針是否是nil谷浅,那么就會core
面向?qū)ο罄^承語義
可以通過使用匿名成員 + 定義方法,實現(xiàn)部分繼承的語義:
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中定義的方法墩邀,被外層的同名方法覆蓋,需要顯式指明才能調(diào)用到Base中的方法眉睹。注意golang中不存在真正的繼承,這是嵌入匿名成員竹海,用匿名成員的方法去理解這樣的語法。另外孔飒,方法的值也是第一類變量,能賦值給別的變量罩润,比c/c++靈活作岖,golang無論是對象方法昔馋,還是類型的方法猛频,都能賦值給別的變量,可以參照例子中的寫法鹿寻。