1. 程式人生 > >Go基礎系列:函式(1)

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是錯誤的

例如:

// 單個返回值
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中。

但函式有多個返回值時,如果其中某個或某幾個返回值不想使用,可以通過下劃線_這個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來理解...的行為。另外,下面的例子也能很好的解釋:

func f1(s...string){
    f2(s...)
    f3(s)
}

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})

defer關鍵字

defer關鍵字可以讓函式或語句延遲到函式語句塊的最結尾時,即即將退出函式時執行,即便函式中途報錯結束、即便已經panic()、即便函式已經return了,也都會執行defer所推遲的物件。

例如:

func main() {
    a()
}

func a() {
    println("in a")
    defer b()
    println("leaving a")
    //到了這裡才會執行b()
}

func b() {
    println("in b")
    println("leaving b")
}

上面將輸出:

in a
leaving a
in b
leaving b

即便是函式已經報錯,或函式已經return返回,defer的物件也會在函式退出前的最後一刻執行。

func a() TYPE{
    ...CODE...
    
    defer b()
    
    ...CODE...
    
    // 函式執行出了錯誤
    
    return args
    // 函式b()都會在這裡執行
}

但注意,由於Go的作用域採用的是詞法作用域,defer的定義位置決定了它推遲物件能看見的變數值,而不是推遲物件被呼叫時所能看見的值。

例如:

package main

var x = 10
func main() {
    a()
}

func a() {
    println("start a:",x)   // 輸出10
    x = 20
    defer b(x)
    x = 30
    println("leaving a:",x)  // 輸出30
    // 呼叫defer延遲的物件b(),輸出20
}

func b(x int) {
    println("start b:",x)
}

如果語句塊內有多個defer,則defer的物件以LIFO(last in first out)的方式執行,也就是說,先定義的defer後執行。

func main() {
    println("start...")
    defer println("1")
    defer println("2")
    defer println("3")
    defer println("4")
    println("end...")
}

將輸出:

start...
end...
4
3
2
1

defer有什麼用呢?一般用來做善後操作,例如清理垃圾、釋放資源,無論是否報錯都執行defer物件。另一方面,defer可以讓這些善後操作的語句和開始語句放在一起,無論在可讀性上還是安全性上都很有改善,畢竟寫完開始語句就可以直接寫defer語句,永遠也不會忘記關閉、善後等操作。

例如,開啟檔案,關閉檔案的操作寫在一起:

open()
defer file.Close()
... 操作檔案 ...

以下是defer的一些常用場景:

  • 開啟關閉檔案
  • 鎖定、釋放鎖
  • 建立連線、釋放連線
  • 作為結尾輸出結尾資訊
  • 清理垃圾(如臨時檔案)

panic()和recover()

panic()用於產生錯誤資訊並終止當前的goroutine,一般將其看作是退出panic()所在函式以及退出呼叫panic()所在函式的函式。例如,G()中呼叫F(),F()中呼叫panic(),則F()退出,G()也退出。

注意,defer關鍵字推遲的物件是函式最後呼叫的,即使出現了panic也會呼叫defer推遲的物件。

例如,下面的程式碼中,main()中輸出一個start main之後呼叫a(),它會輸出start a,然後就panic了,panic()會輸出panic: panic in a,然後報錯,終止程式。

func main() {
    println("start main")
    a()
    println("end main")
}

func a() {
    println("start a")
    panic("panic in a")
    println("end a")
}

執行結果如下:

start main
start a
panic: panic in a

goroutine 1 [running]:
main.a()
        E:/learning/err.go:14 +0x63
main.main()
        E:/learning/err.go:8 +0x4c
exit status 2

注意上面的end aend main都沒有被輸出。

可以使用recover()去捕獲panic()並恢復執行。recover()用於捕捉panic()錯誤,並返回這個錯誤資訊。但注意,即使recover()捕獲到了panic(),但呼叫含有panic()函式的函式(即上面的G()函式)也會退出,所以如果recover()定義在G()中,則G()中呼叫F()函式之後的程式碼都不會執行(見下面的通用格式)。

以下是比較通用的panic()和recover()的格式:

func main() {
    G()
    // 下面的程式碼會執行
    ...CODE IN MAIN...
}
func G(){
    defer func (){
        if str := recover(); str != nil {
            fmt.Println(str)
        }
    }()
    ...CODE IN G()...
    
    // F()的呼叫必須在defer關鍵字之後
    F()
    // 該函式內下面的程式碼不會執行
    ...CODE IN G()...
}
func F() {
    ...CODE1...
    panic("error found")
    // 下面的程式碼不會執行
    ...CODE IN F()...
}

可以使用recover()去捕獲panic()並恢復執行。但以下程式碼是錯誤的:

func main() {
    println("start main")
    a()
    println("end main")
}

func a() {
    println("start a")
    panic("panic in a")

    // 直接放在panic後是錯誤的
    panic_str := recover()
    println(panic_str)

    println("end a")
}

之所以錯誤,是因為panic()一出現就直接退出函式a()和main()了。要想recover()真正捕獲panic(),需要將recover()放在defer的推遲物件中,且defer的定義必須在panic()發生之前。

例如,下面是通用格式的示例:

package main

import "fmt"

func main() {
    println("start main")
    b()
    println("end main")
}

func a() {
    println("start a")
    panic("panic in a")
    println("end a")
}

func b() {
    println("start b")
    defer func() {
        if str := recover(); str != nil {
            fmt.Println(str)
        }
    }()
    a()
    println("end b")
}

以下是輸出結果:

start main
start b
start a
panic in a
end main

注意上面的end bend a都沒有被輸出,但是end main輸出了。

panic()是內建的函式(在包builtin中),在log包中也有一個Panic()函式,它呼叫Print()輸出資訊後,再呼叫panic()。go doc log Panic一看便知:

$ go doc log Panic
func Panic(v ...interface{})
    Panic is equivalent to Print() followed by a call to panic().

內建函式

在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

遞迴函式

函式內部呼叫函式自身的函式稱為遞迴函式。

使用遞迴函式最重要的三點:

  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)
}