Go語言函數詳解

函數是結構化編程的最小模塊。它將複雜的算法過程分解為若干較小的任務,隱藏相關細節,是的程序結構更加清晰,易於維護。函數被設計成相對獨立,通過接受輸入參數完成一段算法指令,輸出或存儲相關的結果。函數是代碼複用和測試的基本單位。

函數的定義

函數構成代碼執行的基本邏輯結構。在Go語言中,函數的基本組成為:關鍵字func、函數名、參數列表、返回值、函數體和返回語句。

func add(a int,b int)(ret int,err error){

if (a < 0 || b < 0){

err = errors.New("should be non-negative number!")

return

}

return a+b,nil

}

如果參數列表中的若干相鄰的參數類型相同,如上例中的a和b,則可以在參數列表中省略前面變量的類型聲明:

func add(a,b int)(ret int,err error){

///...

}

Go語言函數有一些限制:

  • 無須前置聲明
  • 不支持命名嵌套定義
  • 不支持同名函數重載
  • 不支持默認參數
  • 支持不定長變參
  • 支持多返回值
  • 支持命名返回值
  • 支持匿名函數和閉包

函數中,左花括號不能另起一行。

func test()

{ //錯誤,Go語言規定函數左括號不能在新的一行開頭

}

Go語言中函數不能嵌套

func main(){

func test(a,b int ,err error){

//錯誤,函數不支持嵌套操作

...

}

}

函數的調用

Go語言函數調用只需要導入該函數所在的包,直接調用:

import "mymath"

c:=mymath.add(1,2)

Go語言通過函數名字的大小寫來顯示函數的可見性,小寫字母開頭的函數只在本包內可見,大寫字母開頭的函數才能被其他包使用。

這個規則也適用於類型和變量的可見性。

函數的參數

Go語言不支持默認值的可選參數,不支持實名實參。調用時,必須按簽名順序指定類型和數量實參,就算以”_”命名的參數也不能忽略。

func test(x,y int,s string,_ bool){

return nil

}

func main(){

test(1,2,"abc")

//錯誤,"_"命名的參數也不能忽略

}

參數可視作函數局部變量,因此不能在相同的層次定義同名變量。

func add(x,y int) int{

x:=100 //錯誤

...

}

不管是指針、引用類型,還是其他類型參數,都是值拷貝傳遞。區別無非是拷貝目標對象,還是拷貝指針而已。在函數調用前,會為形參和返回值分配內存空間,並將實參拷貝到形參內存。

被複制的指針會延長目標對象的生命週期,還有可能導致它被分配到堆上。在棧上覆制小對象只需要很少的指令,比運行時堆內存分配快。在併發編程的時候,儘量使用不可變對象,可以消除數據同步等麻煩。如果複製成本過高,或者需要修改原狀態,直接使用指針更好。

不定參數

變參本質上是一個切片,只能接受一到多個同類型參數,且必須放在列表尾部:

func test(s string,a ...int){

...

}

將切片作為變參時,須進行展開操作。如果是數組,必將其轉換成切片。

func test(a ...int){

fmt.Println(a)

}

func main(){

a := [3]int{10,20,30}

test(a[:]...)

}

既然變參時切片,那麼參數複製的僅是切片本身,並不包括底層數組,也因此可修改原數據。

返回值

有返回值的函數,必須有明確的return終止語句。

函數可以返回多值模式,函數可以返回更多狀態,尤其是error模式。

func dic(x,y int)(int,error){

if y==0 {

return 0,errors.New("division by zero")

}

return x/y,nil

}

命名返回值

命名返回值和參數一樣,可當做函數局部變量使用,最後由return隱式返回。

func dic(x,y int)(z int,err error){

if y==0 {

err=errors.New("division by zero")

return

}

z = x/y

return

}

命名返回值會被不同層級的同名變量屏蔽。編譯器可以檢查這類錯誤,只需要顯示return返回即可。

func add(x,y int)(z int){

z:= x + y //同名局部變量進行了覆蓋

return //錯誤,改成return z

}

如果使用命名返回值,則需要全部使用命名返回值。

func test()(int,s string){

//錯誤

...

}

匿名函數

匿名函數是指沒有定義名字符號的函數。

除了沒有名字外,在函數內部定義匿名函數可以形成嵌套效果。匿名函數可直接調用,保存到變量,作為參數或返回值。

func main(){

func(s string){

fmt.Println(s)

}("hellow world")

}

除閉包因素外,匿名函數也是常見的重構的手段。可將大函數分解成多個相對獨立的匿名函數塊,然後相對簡潔的完成調用邏輯流程,實現框架和細節分離。

閉包

Go語言匿名函數就是一個閉包,閉包是可以包含自由變量(未綁定到特定變量)的代碼塊,這些變量不在這個代碼塊內或者任何全局上下文中定義,而是在定義代碼塊的環境中定義。要執行的代碼塊(由於自由變量包含在代碼塊中,所以這些自由變量以及他們引用的對象沒有被釋放)為自由變量提供綁定的計算環境(作用域。

func test(x int)func(){

return func(){

println(x)

}

}

func main(){

f := test(123)

f()

}

test返回的匿名函數會引用上下文環境變量x,當main函數執行時,依舊可以讀取到x的值。

正因為閉包通過指針引用環境變量,可能導致其生命週期延長,甚至被分配到堆內存。另外,還有”延遲求值”特性。

func test()[]func(){

var s []func()

for i := 0;i < 2;i++{

s = append(s,func(){

fmt.Println(i)

})

}

return s

}

func main(){

for _,f := rang test(){

f()

}

}

結果是:

2

2

在for循環內部複用局部變量i,每次添加的匿名函數引用的是同意變量。添加僅僅把匿名函數放入列表,並未執行。當main函數執行這些函數時,讀取的是環境變量i最後循環的值。

修改為:

func test()[]func(){

var s []func()

for i := 0;i < 2;i++{

x := i

//每次用不同的環境變量或傳參複製,讓各自的閉包環境各不相同。

s = append(s,func(){

fmt.Println(x)

})

}

return s

}


分享到:


相關文章: