1. 程式人生 > >golang學習筆記---函數、方法和接口

golang學習筆記---函數、方法和接口

動態綁定 多個 依賴 find 輸出 ack range 支持 不定

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

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

定到一個具體類型的特殊函數,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 這類內置類型添加方法的(因為方法的定義和類型的定義
不在一個包中)。對於給定的類型,每個方法的名字必須是唯一的,同時方法和函
數一樣也不支持重載。

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

golang學習筆記---函數、方法和接口