1. 程式人生 > >golang學習筆記---函式、方法和介面

golang學習筆記---函式、方法和介面

函式:對應操作序列,是程式的基本組成元素。

函式有具名和匿名之分:
具名函式一般對應於包級的函式,是匿名函式的一種特例,當匿名函式引用了外部
作用域中的變數時就成了閉包函式,閉包函式是函數語言程式設計語言的核心。方法是綁

定到一個具體型別的特殊函式,Go語言中的方法是依託於型別的,必須在編譯時靜
態繫結

介面:定義了方法的集合,這些方法依託於執行時的介面物件,因此介面對
應的方法是在執行時動態繫結的。

Go程式函式啟動順序的示意圖:

 

要注意的是,在 main.main 函式執行之前所有程式碼都執行在同一個goroutine,也
就是程式的主系統執行緒中。

因此,如果某個 init 函式內部用go關鍵字啟動了新的
goroutine的話,新的goroutine只有在進入 main.main 函式之後才可能被執行到。

函式

在Go語言中,函式是第一類物件,我們可以將函式保持到變數中。函式主要有具名
和匿名之分,包級函式一般都是具名函式,具名函式是匿名函式的一種特例

// 具名函式
func Add(a, b int) int {
  return a+b
}
// 匿名函式
var Add = func(a, b int) int {
  return a+b
}

Go語言中的函式可以有多個引數和多個返回值,引數和返回值都是以傳值的方式和
被呼叫者交換資料。在語法上,函式還支援可變數量的引數,可變數量的引數必須
是最後出現的引數,可變數量的引數其實是一個切片型別的引數。

// 多個引數和多個返回值
func Swap(a, b int) (int, int) {
  return b, a
}
// 可變數量的引數
// more 對應 []int 切片型別
func Sum(a int, more ...int) int {
  for _, v := range more {
    a += v
  }
  return a
}

 當可變引數是一個空介面型別時,呼叫者是否解包可變引數會導致不同的結果:

package main
 
 
import (
    "fmt"
)
 
 
func main() {
    var a = []interface{}{123, "abc"}
    Print(a...) // 123 abc
    Print(a)    // [123 abc]
}
func Print(a ...interface{}) {
    fmt.Println(a...)
}

第一個 Print 呼叫時傳入的引數是 a... ,等價於直接呼叫 Print(123,
"abc") 。第二個 Print 呼叫傳入的是未解包的 a ,等價於直接調
用 Print([]interface{}{123, "abc"}) 。

‘…’ 其實是go的一種語法糖。 
它的第一個用法主要是用於函式有多個不定引數的情況,可以接受多個不確定數量的引數。 
第二個用法是slice可以被打散進行傳遞。

不僅函式的引數可以有名字,也可以給函式的返回值命名:

func Find(m map[int]int, key int) (value int, ok bool) {
    value, ok = m[key]
    return
}

如果返回值命名了,可以通過名字來修改返回值,也可以通過 defer 語句
在 return 語句之後修改返回值:

func Inc() (v int) {
    defer func(){ v++ } ()
    return 42
}

其中 defer 語句延遲執行了一個匿名函式,因為這個匿名函式捕獲了外部函式的
區域性變數 v ,這種函式我們一般叫閉包。閉包對捕獲的外部變數並不是傳值方式
訪問,而是以引用的方式訪問。

閉包的這種引用方式訪問外部變數的行為可能會導致一些隱含的問題:

package main

import (
	"fmt"
)

func main() {
	for i := 0; i < 3; i++ {
		defer func() { fmt.Println(i) }()
	}
}

// Output:
// 3
// 3
// 3

因為是閉包,在 for 迭代語句中,每個 defer 語句延遲執行的函式引用的都是
同一個 i 迭代變數,在迴圈結束後這個變數的值為3,因此最終輸出的都是3。
修復的思路是在每輪迭代中為每個 defer 函式生成獨有的變數。可以用下面兩種
方式:

package main

import (
	"fmt"
)

func main() {
	for i := 0; i < 3; i++ {
		i := i // 定義一個迴圈體內區域性變數i
		defer func() { fmt.Println(i) }()
	}
}

  

package main

import (
	"fmt"
)

func main() {
	for i := 0; i < 3; i++ {
		// 通過函式傳入i
		// defer 語句會馬上對呼叫引數求值
		defer func(i int) { fmt.Println(i) }(i)
	}
}

  

不過一般來說,在 for 迴圈內部執行 defer 語句並不是一個好
的習慣,此處僅為示例,不建議使用。

每個goroutine剛啟動時只會分配
很小的棧(4或8KB,具體依賴實現),根據需要動態調整棧的大小,棧最大可以
達到GB級(依賴具體實現,在目前的實現中,32位體系結構為250MB,64位體系結構為1GB)

Go語言中指標不再是固定不變的了(因此
不能隨意將指標保持到數值變數中,Go語言的地址也不能隨意儲存到不在GC控制
的環境中,因此使用CGO時不能在C語言中長期持有Go語言物件的地址

 

方法

Go語言的方法關聯到型別的,這樣可以在編譯階段完成方法的靜態繫結。

我們可以給任何自
定義型別新增一個或多個方法。每種型別對應的方法必須和型別的定義在同一個包
中,因此是無法給 int 這類內建型別新增方法的(因為方法的定義和型別的定義
不在一個包中)。對於給定的型別,每個方法的名字必須是唯一的,同時方法和函
數一樣也不支援過載。

方法是由函式演變而來,只是將函式的第一個物件引數移動到了函式名前面了而
已。因此我們依然可以按照原始的過程式思維來使用方法。通過叫方法表示式的特
性可以將方法還原為普通型別的函式: