Go中函數特性簡介
對Go中的函數特性做一個總結。懂則看,不懂則算。
- Go中有3種函數:普通函數、匿名函數(沒有名稱的函數)、方法(定義在struct上的函數)。
- Go編譯時不在乎函數的定義位置,但建議init()定義在最前面(如果有的話),main函數定義在init()之后,然后再根據函數名的字母順序或者根據調用順序放置各函數的位置。
- 函數的參數、返回值以及它們的類型,結合起來成為函數的簽名(signature)。
- 函數調用的時候,如果有參數傳遞給函數,則先拷貝參數的副本,再將副本傳遞給函數。
- 由於引用類型(slice、map、interface、channel)自身就是指針,所以這些類型的值拷貝給函數參數,函數內部的參數仍然指向它們的底層數據結構。
- 函數參數可以沒有名稱,例如
func myfunc(int,int)
。 - Go中的函數可以作為一種type類型,例如
type myfunc func(int,int) int
。- 實際上,在Go中,函數本身就是一種類型,它的signature就是所謂的type,例如
func(int,int) int
。所以,當函數ab()賦值給一個變量ref_ab
時ref_ab := ab
,不能再將其它函數類型的函數cd()賦值給變量ref_ab
。
- 實際上,在Go中,函數本身就是一種類型,它的signature就是所謂的type,例如
- Go中作用域是詞法作用域,意味着函數的定義位置決定了它能看見的變量。
- Go中不允許函數重載(overload),也就是說不允許函數同名。
- Go中的函數不能嵌套函數,但可以嵌套匿名函數。
- Go實現了一級函數(first-class functions),Go中的函數是高階函數(high-order functions)。這意味着:
- 函數是一個值,可以將函數賦值給變量,使得這個變量也成為函數
- 函數可以作為參數傳遞給另一個函數
- 函數的返回值可以是一個函數
- 這些特性使得函數變得無比的靈活,例如回調函數、閉包等等功能都依賴於這些特性。
- Go中的函數不支持泛型(目前不支持),但如果需要泛型的情況,大多數時候都可以通過接口、type switch、reflection的方式來解決。但使用這些技術使得代碼變得更復雜,性能更低。
參數和返回值
函數可以有0或多個參數,0或多個返回值,參數和返回值都需要指定數據類型,返回值通過return關鍵字來指定。
return可以有參數,也可以沒有參數,這些返回值可以有名稱,也可以沒有名稱。Go中的函數可以有多個返回值。
- (1).當返回值有多個時,這些返回值必須使用括號包圍,逗號分隔
- (2).return關鍵字中指定了參數時,返回值可以不用名稱。如果return省略參數,則返回值部分必須帶名稱
- (3).當返回值有名稱時,必須使用括號包圍,逗號分隔,即使只有一個返回值
- (4).但即使返回值命名了,return中也可以強制指定其它返回值的名稱,也就是說return的優先級更高
- (5).命名的返回值是預先聲明好的,在函數內部可以直接使用,無需再次聲明。命名返回值的名稱不能和函數參數名稱相同,否則報錯提示變量重復定義
- (6).return中可以有表達式,但不能出現賦值表達式,這和其它語言可能有所不同。例如
return a+b
是正確的,但return c=a+b
是錯誤的
例如:
// 單個返回值
func func_a() int{
return a
}
// 只要命名了返回值,必須括號包圍
func func_b() (a int){
// 變量a int已存在,無需再次聲明
a = 10
return
// 等價於:return a
}
// 多個返回值,且在return中指定返回的內容
func func_c() (int,int){
return a,b
}
// 多個返回值
func func_d() (a,b int){
return
// 等價於:return a,b
}
// return覆蓋命名返回值
func func_e() (a,b int){
return x,y
}
Go中經常會使用其中一個返回值作為函數是否執行成功、是否有錯誤信息的判斷條件。例如return value,exists
、return value,ok
、return value,err
等。
當函數的返回值過多時,例如有4個以上的返回值,應該將這些返回值收集到容器中,然后以返回容器的方式去返回。例如,同類型的返回值可以放進slice中,不同類型的返回值可以放進map中。
但函數有多個返回值時,如果其中某個或某幾個返回值不想使用,可以通過下划線_
這個blank identifier來丟棄這些返回值。例如下面的func_a
函數兩個返回值,調用該函數時,丟棄了第二個返回值b,只保留了第一個返回值a賦值給了變量a
。
func func_a() (a,b int){
return
}
func main() {
a,_ := func_a()
}
按值傳參
Go中是通過傳值的方式傳參的,意味着傳遞給函數的是拷貝后的副本,所以函數內部訪問、修改的也是這個副本。
例如:
a,b := 10,20
min(a,b)
func min(x,y int) int{}
上面調用min()時,是將a和b的值拷貝一份,然后將拷貝的副本賦值給變量x,y的,所以min()函數內部,訪問、修改的一直是a、b的副本,和原始的數據對象a、b沒有任何關系。
如果想要修改外部數據(即上面的a、b),需要傳遞指針。
例如,下面兩個函數,func_value()
是傳值函數,func_ptr()
是傳指針函數,它們都修改同一個變量的值。
package main
import "fmt"
func main() {
a := 10
func_value(a)
fmt.Println(a) // 輸出的值仍然是10
b := &a
func_ptr(b)
fmt.Println(*b) // 輸出修改后的值:11
}
func func_value(x int) int{
x = x + 1
return x
}
func func_ptr(x *int) int{
*x = *x + 1
return *x
}
map、slice、interface、channel這些數據類型本身就是指針類型的,所以就算是拷貝傳值也是拷貝的指針,拷貝后的參數仍然指向底層數據結構,所以修改它們可能會影響外部數據結構的值。
另外注意,賦值操作b = a+1
這種類型的賦值也是拷貝賦值。換句話說,現在底層已經有兩個數據對象,一個是a,一個是b。但a = a+1
這種類型的賦值雖然本質上是拷貝賦值,但因為a的指針指向特性,使得結果上看是原地修改數據對象而非生成新數據對象。
變長參數"..."(variadic)
有時候參數過多,或者想要讓函數處理任意多個的參數,可以在函數定義語句的參數部分使用ARGS...TYPE
的方式。這時會將...
代表的參數全部保存到一個名為ARGS的slice中,注意這些參數的數據類型都是TYPE。
...
在Go中稱為variadic,在使用...
的時候(如傳遞、賦值),可以將它看作是一個slice,下面的幾個例子可以說明它的用法。
例如:func myfunc(a,b int,args...int) int {}
。除了前兩個參數a和b外,其它的參數全都保存到名為args的slice中,且這些參數全都是int類型。所以,在函數內部就已經有了一個args = []int{....}
的數據結構。
例如,下面的例子中,min()函數要從所有參數中找出最小的值。為了實驗效果,特地將前兩個參數a和b獨立到slice的外面。min()函數內部同時會輸出保存到args中的參數值。
package main
import "fmt"
func main() {
a,b,c,d,e,f := 10,20,30,40,50,60
fmt.Println(min(a,b,c,d,e,f))
}
func min(a,b int,args...int) int{
// 輸出args中保存的參數
// 等價於 args := []int{30,40,50,60}
for index,value := range args {
fmt.Printf("%s%d%s %d\n","args[",index,"]:",value)
}
// 取出a、b中較小者
min_value := a
if a>b {
min_value = b
}
// 取出所有參數中最小值
for _,value := range args{
if min_value > value {
min_value = value
}
}
return min_value
}
但上面代碼中調用函數時傳遞參數的方式顯然比較笨重。如果要傳遞的參數過多(要比較的值很多),可以先將這些參數保存到一個slice中,再傳遞slice給min()函數。傳遞slice給函數的時候,使用SLICE...
的方式即可。
func main() {
s1 := []int{30,40,50,60,70}
fmt.Println(min(10,20,s1...))
}
上面的賦值方式已經能說明能使用slice來理解...
的行為。另外,下面的例子也能很好的解釋:
// 聲明f1()
func f1(s...string){
// 調用f2()和f3()
f2(s...)
f3(s)
}
// 聲明f2()和f3()
func f2(s...string){}
func f3(s []string){}
如果各參數的類型不同,又想定義成變長參數,該如何?第一種方式,可以使用struct,第二種方式可以使用接口。接口暫且不說,如果使用struct,大概如下:
type args struct {
arg1 string
arg2 int
arg3 type3
}
然后可以將args傳遞給函數:f(a,b int,args{})
,如果args結構中需要初始化,則f(a,b int,args{arg1:"hello",arg2:22})
。
內置函數
在builtin包中有一些內置函數,這些內置函數額外的導入包就能使用。
有以下內置函數:
$ go doc builtin | grep func
func close(c chan<- Type)
func delete(m map[Type]Type1, key Type)
func panic(v interface{})
func print(args ...Type)
func println(args ...Type)
func recover() interface{}
func complex(r, i FloatType) ComplexType
func imag(c ComplexType) FloatType
func real(c ComplexType) FloatType
func append(slice []Type, elems ...Type) []Type
func make(t Type, size ...IntegerType) Type
func new(Type) *Type
func cap(v Type) int
func copy(dst, src []Type) int
func len(v Type) int
close
用於關閉channeldelete
用於刪除map中的元素copy
用於拷貝sliceappend
用於追加slicecap
用於獲取slice的容量len
用於獲取- slice的長度
- map的元素個數
- array的元素個數
- 指向array的指針時,獲取array的長度
- string的字節數
- channel的channel buffer中的未讀隊列長度
print
和println
:底層的輸出函數,用來調試用。在實際程序中,應該使用fmt中的print類函數complex
、imag
、real
:操作復數(虛數)panic
和recover
:處理錯誤new
和make
:分配內存並初始化- new適用於為值類(value type)的數據類型(如array,int等)和struct類型的對象分配內存並初始化,並返回它們的指針給變量。如
v := new(int)
- make適用於為內置的引用類的類型(如slice、map、channel等)分配內存並初始化底層數據結構,並返回它們的指針給變量,同時可能會做一些額外的操作
- new適用於為值類(value type)的數據類型(如array,int等)和struct類型的對象分配內存並初始化,並返回它們的指針給變量。如
注意,地址和指針是不同的。地址就是數據對象在內存中的地址,指針則是占用一個機器字長(32位機器是4字節,64位機器是8字節)的數據,這個數據中存儲的是它所指向數據對象的地址。
a -> AAAA
b -> Pointer -> BBBB
new()和make()構造數據對象賦值給變量的都是指向數據對象的指針。
遞歸函數
函數內部調用函數自身的函數稱為遞歸函數。
使用遞歸函數最重要的三點:
- 必須先定義函數的退出條件,退出條件基本上都使用退出點來定義,退出點常常也稱為遞歸的基點,是遞歸函數的最后一次遞歸點,或者說沒有東西可遞歸時就是退出點。
- 遞歸函數很可能會產生一大堆的goroutine(其它編程語言則是出現一大堆的線程、進程),也很可能會出現棧空間內存溢出問題。在其它編程語言可能只能設置最大遞歸深度或改寫遞歸函數來解決這個問題,在Go中可以使用channel+goroutine設計的"lazy evaluation"來解決。
- 遞歸函數通常可以使用level級數的方式進行改寫,使其不再是遞歸函數,這樣就不會有第2點的問題。
例如,遞歸最常見的示例,求一個給定整數的階乘。因為階乘的公式為n*(n-1)*...*3*2*1
,它在參數為1的時候退出函數,也就是說它的遞歸基點是1,所以對是否為基點進行判斷,然后再寫遞歸表達式。
package main
import "fmt"
func main() {
fmt.Println(a(5))
}
func a(n int) int{
// 判斷退出點
if n == 1 {
return 1
}
// 遞歸表達式
return n * a(n-1)
}
它的調用過程大概是這樣的:
再比如斐波那契數列,它的計算公式為f(n)=f(n-1)+f(n-2)
且f(2)=f(1)=1
。它在參數為1和2的時候退出函數,所以它的退出點為1和2。
package main
import "fmt"
func main() {
fmt.Println(f(3))
}
func f(n int) int{
// 退出點判斷
if n == 1 || n == 2 {
return 1
}
// 遞歸表達式
return f(n-1)+f(n-2)
}
如何遞歸一個目錄?它的遞歸基點是文件,只要是文件就返回,只要是目錄就進入。所以,偽代碼如下:
func recur(dir FILE) FILE{
// 退出點判斷
if (dir is a file){
return dir
}
// 當前目錄的文件列表
file_slice := filelist()
// 遍歷所有文件
for _,file := range file_slice {
return recur(file)
}
}
匿名函數
匿名函數是沒有名稱的函數。一般匿名函數嵌套在函數內部,或者賦值給一個變量,或者作為一個表達式。
定義的方式:
// 聲明匿名函數
func(args){
...CODE...
}
// 聲明匿名函數並直接執行
func(args){
...CODE...
}(parameters)
下面的示例中,先定義了匿名函數,將其賦值給了一個變量,然后在需要的地方再去調用執行它。
package main
import "fmt"
func main() {
// 匿名函數賦值給變量
a := func() {
fmt.Println("hello world")
}
// 調用匿名函數
a()
fmt.Printf("%T\n", a) // a的type類型:func()
fmt.Println(a) // 函數的地址
}
如果給匿名函數的定義語句后面加上()
,表示聲明這個匿名函數的同時並執行:
func main() {
msg := "Hello World"
func(m string) {
fmt.Println(m)
}(msg)
}
其中func(c string)
表示匿名函數的參數,func(m string){}(msg)
的msg
表示傳遞msg變量給匿名函數,並執行。
func type
可以將func作為一種type,以后可以直接使用這個type來定義函數。
package main
import "fmt"
type add func(a,b int) int
func main() {
var a add = func(a,b int) int{
return a+b
}
s := a(3,5)
fmt.Println(s)
}