函數
//常規的函數定義
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的語句,常用於釋放資源、記錄函數執行耗時等,有一下幾個特點:
- 當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。
非常重要的一點是,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
通過上面的例子,我們可以發現一些知識點:
- 使用第二種函數定義的方法,那麼就和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無論是對象方法,還是類型的方法,都能賦值給別的變量,可以參照例子中的寫法。