函數定義
函數是結構化編程的最小模塊單元,使用關鍵字‘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