Go基礎系列:函數(1)

Go中函數特性簡介

對Go中的函數特性作一個總結。懂則看,不懂則算。編程

  1. Go中有3種函數:普通函數、匿名函數(沒有名稱的函數)、方法(定義在struct上的函數)。
  2. Go編譯時不在意函數的定義位置,但建議init()定義在最前面(若是有的話),main函數定義在init()以後,而後再根據函數名的字母順序或者根據調用順序放置各函數的位置。
  3. 函數的參數、返回值以及它們的類型,結合起來成爲函數的簽名(signature)。
  4. 函數調用的時候,若是有參數傳遞給函數,則先拷貝參數的副本,再將副本傳遞給函數。
    • 因爲引用類型(slice、map、interface、channel)自身就是指針,因此這些類型的值拷貝給函數參數,函數內部的參數仍然指向它們的底層數據結構。
  5. 函數參數能夠沒有名稱,例如func myfunc(int,int)
  6. Go中的函數能夠做爲一種type類型,例如type myfunc func(int,int) int
    • 實際上,在Go中,函數自己就是一種類型,它的signature就是所謂的type,例如func(int,int) int。因此,當函數ab()賦值給一個變量ref_abref_ab := ab,不能再將其它函數類型的函數cd()賦值給變量ref_ab
  7. Go中做用域是詞法做用域,意味着函數的定義位置決定了它能看見的變量。
  8. Go中不容許函數重載(overload),也就是說不容許函數同名。
  9. Go中的函數不能嵌套函數,但能夠嵌套匿名函數。
  10. Go實現了一級函數(first-class functions),Go中的函數是高階函數(high-order functions)。這意味着:
    • 函數是一個值,能夠將函數賦值給變量,使得這個變量也成爲函數
    • 函數能夠做爲參數傳遞給另外一個函數
    • 函數的返回值能夠是一個函數
    • 這些特性使得函數變得無比的靈活,例如回調函數、閉包等等功能都依賴於這些特性。
  11. 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是錯誤的

例如:app

// 單個返回值
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,existsreturn value,okreturn value,err等。編程語言

當函數的返回值過多時,例若有4個以上的返回值,應該將這些返回值收集到容器中,而後以返回容器的方式去返回。例如,同類型的返回值能夠放進slice中,不一樣類型的返回值能夠放進map中。ide

但函數有多個返回值時,若是其中某個或某幾個返回值不想使用,能夠經過下劃線_這個blank identifier來丟棄這些返回值。例以下面的func_a函數兩個返回值,調用該函數時,丟棄了第二個返回值b,只保留了第一個返回值a賦值給了變量a函數

func func_a() (a,b int){
    return
}

func main() {
    a,_ := func_a()
}

按值傳參

Go中是經過傳值的方式傳參的,意味着傳遞給函數的是拷貝後的副本,因此函數內部訪問、修改的也是這個副本。性能

例如:ui

a,b := 10,20
min(a,b)
func min(x,y int) int{}

上面調用min()時,是將a和b的值拷貝一份,而後將拷貝的副本賦值給變量x,y的,因此min()函數內部,訪問、修改的一直是a、b的副本,和原始的數據對象a、b沒有任何關係。lua

若是想要修改外部數據(即上面的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用於關閉channel
  • delete用於刪除map中的元素
  • copy用於拷貝slice
  • append用於追加slice
  • cap用於獲取slice的容量
  • len用於獲取
    • slice的長度
    • map的元素個數
    • array的元素個數
    • 指向array的指針時,獲取array的長度
    • string的字節數
    • channel的channel buffer中的未讀隊列長度
  • printprintln:底層的輸出函數,用來調試用。在實際程序中,應該使用fmt中的print類函數
  • compleximagreal:操做複數(虛數)
  • panicrecover:處理錯誤
  • newmake:分配內存並初始化
    • new適用於爲值類(value type)的數據類型(如array,int等)和struct類型的對象分配內存並初始化,並返回它們的指針給變量。如v := new(int)
    • make適用於爲內置的引用類的類型(如slice、map、channel等)分配內存並初始化底層數據結構,並返回它們的指針給變量,同時可能會作一些額外的操做

注意,地址和指針是不一樣的。地址就是數據對象在內存中的地址,指針則是佔用一個機器字長(32位機器是4字節,64位機器是8字節)的數據,這個數據中存儲的是它所指向數據對象的地址。

a -> AAAA
b -> Pointer -> BBBB

new()和make()構造數據對象賦值給變量的都是指向數據對象的指針。

遞歸函數

函數內部調用函數自身的函數稱爲遞歸函數。

使用遞歸函數最重要的三點:

  1. 必須先定義函數的退出條件,退出條件基本上都使用退出點來定義,退出點經常也稱爲遞歸的基點,是遞歸函數的最後一次遞歸點,或者說沒有東西可遞歸時就是退出點。
  2. 遞歸函數極可能會產生一大堆的goroutine(其它編程語言則是出現一大堆的線程、進程),也極可能會出現棧空間內存溢出問題。在其它編程語言可能只能設置最大遞歸深度或改寫遞歸函數來解決這個問題,在Go中可使用channel+goroutine設計的"lazy evaluation"來解決。
  3. 遞歸函數一般可使用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)
}
相關文章
相關標籤/搜索