Go 函數整理

函數是一塊執行特定任務的代碼。一個函數是在輸入源基礎上,通過執行一系列的算法,生成預期的輸出。

Go 裏面有三種類型的函數:

1. 普通函數

func functionName(parameter_list) (return_value_list) {
   
}

//parameter_list 的形式爲 (param1 type1, param2 type2, …)
//return_value_list 的形式爲 (ret1 type1, ret2 type2, …)

函數的聲明以關鍵詞 func 開始,後面緊跟自定義的函數名 functionname (函數名)。函數能夠接收參數供自己使用,也可以返回零個或多個值(我們通常把返回多個值稱爲返回一組值)。

1.1 函數特性
1.1.1 多返回值

多值返回是 Go 的一大特性,爲我們判斷一個函數是否正常執行提供了方便。

func vals() (int, error) {
    return 3, nil
}

func main() {

    // 獲取函數的兩個返回值
    a, err := vals()
    if err != nil {
        fmt.Println(a)
    }
}
1.1.2 空白符

空白符用來匹配一些不需要的值,然後丟棄掉。

func vals() (int, error) {
    return 3, nil
}

func main() {
    
    // 如果你只對多個返回值裏面的幾個感興趣
    // 可以使用下劃線(_)來忽略其他的返回值
    c, _ := vals()
    fmt.Println(c)       //3
}
1.1.3 變長參數

如果函數的最後一個參數是採用 ...type 的形式,那麼這個函數就可以處理一個變長的參數,這時函數可以接受任意個 type 類型參數作爲最後一個參數。需要注意只有函數的最後一個參數才允許是可變的。

func find(num int, nums ...int) {
    fmt.Printf("type of nums is %T\n", nums)
    found := false
    for i, v := range nums {
        if v == num {
            fmt.Println(num, "found at index", i, "in", nums)
            found = true
        }
    }
    if !found {
        fmt.Println(num, "not found in ", nums)
    }
    fmt.Printf("\n")
}
func main() {
    find(89, 89, 90, 95)  //type of nums is []int    89 found at index 0 in [89 90 95]
    find(78, 38, 56, 98)  //type of nums is []int    78 not found in  [38 56 98]
    find(87)              //type of nums is []int    87 not found in  []
}

可變參數函數的工作原理是把可變參數轉換爲一個新的切片。

func find(num int, nums ...int) {
    fmt.Printf("type of nums is %T\n", nums)
    found := false
    for i, v := range nums {
        if v == num {
            fmt.Println(num, "found at index", i, "in", nums)
            found = true
        }
    }
    if !found {
        fmt.Println(num, "not found in ", nums)
    }
    fmt.Printf("\n")
}
func main() {
    nums := []int{89, 90, 95}
    find(89, nums)
}

上面的例子中我們將一個切片傳給一個可變參數函數。這種情況下無法通過編譯,編譯器報出錯誤 cannot use nums (type []int) as type int in argument to find 。原因是在這裏 nums 已經是一個 int 類型切片,編譯器試圖在 nums 基礎上再創建一個切片,所以失敗。

有一個可以直接將切片傳入可變參數函數的語法糖,你可以在在切片後加上 ... 後綴。如果這樣做,切片將直接傳入函數,不再創建新的切片

func main() {
    nums := []int{89, 90, 95}
    find(89, nums...)     //type of nums is []int    89 found at index 0 in [89 90 95]
}

如果一個變長參數的類型沒有被指定,則可以使用默認的空接口 interface{},這樣就可以接受任何類型的參數。該方法不僅可以用於長度未知的參數,還可以用於任何不確定類型的參數。一般而言我們會使用一個 for-range 循環以及 switch 結構對每個參數的類型進行判斷:

 func typecheck(..,..,values ...interface{}) {
  for _, value := range values {
   switch v := value.(type) {
    case int: 
    case float64: 
    case string: 
    case bool: 
    default: 
   }
  }
 }
1.1.4 函數重載

函數重載(function overloading)指的是可以編寫多個同名函數,只要它們擁有不同的形參與 / 或者不同的返回值,在 Go 裏面函數重載是不被允許的。這將導致一個編譯錯誤:

funcName redeclared in this book, previous declaration at lineno
1.1.5 函數參數傳遞

Go 語言中函數的傳參都是值傳遞(傳值),都是一個副本,一個拷貝。因爲拷貝的內容有時候是非引用類型(int、string、struct 等這些),這樣就在函數中就無法修改原內容數據;有的是引用類型(指針、map、slice、chan 等這些),這樣就可以修改原內容數據。

func main()  {
 var args int64= 1
 fmt.Printf("實際參數的地址 %p\n", &args)    //實際參數的地址 0xc00006e090
 modifiedNumber(args)                     // args就是實際參數
 fmt.Printf("改動後的值是  %d\n",args)      //改動後的值是  1
}

func modifiedNumber(args int64)  {           //這裏定義的args就是形式參數
 fmt.Printf("形參地址 %p \n",&args)        //形參地址 0xc00006e098 
 args = 10
}
func main()  {
 var args =  []int64{1,2,3}
 fmt.Printf("切片args的地址: %p \n",args)               //切片args的地址: 0xc00006c120 
 fmt.Printf("切片args第一個元素的地址: %p \n",&args[0])   //切片args第一個元素的地址: 0xc00006c120 
 fmt.Printf("直接對切片args取地址:%p \n",&args)          //直接對切片args取地址:0xc000064440 
 modifiedNumber(args)
 fmt.Println(args)      //[10 2 3]
}

func modifiedNumber(args []int64)  {
 fmt.Printf("形參切片的地址 %p \n",args)                    //形參切片的地址 0xc00006c120 
 fmt.Printf("形參切片args第一個元素的地址: %p \n",&args[0])  //形參切片args第一個元素的地址: 0xc00006c120 
 fmt.Printf("直接對形參切片args取地址:%p \n",&args)         //直接對形參切片args取地址:0xc0000644a0 
 args[0] = 10
}
1.1.6 defer

關鍵字 defer 允許我們推遲到函數返回之前(或任意位置執行 return 語句之後)一刻才執行某個語句或函數。

func a() {
 i := 0
 defer fmt.Println(i)
 i++
 return
}

func main() {
 a()   //0
}

當一個函數內多次調用 defer 時,Go 會把 defer 調用放入到一個棧中,隨後按照後進先出(Last In First Out, LIFO)的順序執行。

func f() {
 for i := 0; i < 5; i++ {
  defer fmt.Printf("%d ", i)
 }
}

func main() {
 f()   //4 3 2 1 0
}

defer 的作用:

1.關閉文件流
// open a file  
defer file.Close()

2.解鎖一個加鎖的資源
mu.Lock()  
defer mu.Unlock() 

3.關閉數據庫鏈接
// open a database connection  
defer disconnectFromDB()

4.panic異常捕獲
func f() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered in f", r)
        }
    }()
    fmt.Println("Calling g.")
    g()
    fmt.Println("Returned normally from g.")
}

5.對函數return的返回值進行操作
func func1(s string) (n int, err error) {
    defer func() {
        log.Printf("func1(%q) = %d, %v", s, n, err)
    }()
    return 7, io.EOF
}

func main() {
    func1("Go")   //2021/04/13 15:15:24 func1("Go") = 7, EOF
}
1.2 init 函數

init() 函數會在每個包完成初始化後自動執行,並且執行優先級比 main 函數高。init 函數通常被用來:

package main

import "fmt"

var _ int64=s()

func init(){
    fmt.Println("init function --->")
}

func s() int64{
    fmt.Println("function s() --->")
    return 1
}

func main(){
    fmt.Println("main --->")
}

執行結果如下:

function s() --->
init function --->
main --->

init() 函數特性:

func init(){
    fmt.Println("init 1")
}

func init(){
    fmt.Println("init2")
}

func main(){
    fmt.Println("main")
}

/*執行結果:
init1
init2
main */
1.3 內置函數

Go 語言擁有一些不需要進行導入操作就可以使用的內置函數。

ksJZaw

2. 匿名函數

當我們不希望給函數起名字的時候,可以使用匿名函數。匿名函數即沒有名稱的函數。Go 語言支持匿名函數是因爲 Go 語言支持頭等函數的機制。支持頭等函數(First Class Function)的編程語言,可以把函數賦值給變量,也可以把函數作爲其它函數的參數或者返回值

func main() {  
    a := func() {
        fmt.Println("hello world first class function")
    }
    a()
    fmt.Printf("%T", a)
}

在上面的程序中,我們將一個函數賦值給了變量 a。這是把函數賦值給變量的語法。可以看到賦值給 a 的函數沒有名稱,由於沒有名稱,這類函數稱爲匿名函數(Anonymous Function)

2.1 匿名函數特性

要調用一個匿名函數,可以不用賦值給變量。就像其它函數一樣,還可以向匿名函數傳遞參數。

func main() {  
    func(n string) {
        fmt.Println("Welcome", n)     //Welcome Gophers
    }("Gophers")
}
2.2 自定義的函數類型

正如我們定義自己的結構體類型一樣,我們可以定義自己的函數類型。

type add func(a int, b int) int

func main() {  
    var a add = func(a int, b int) int {
        return a + b
    }
    s := a(5, 6)
    fmt.Println("Sum", s)   //Sum 11
}

以上代碼片段創建了一個新的函數類型 add,它接收兩個整型參數,並返回一個整型。

然後向它賦值了一個符合 add 類型簽名的函數。

2.3 將函數作爲參數

函數可以作爲其它函數的參數進行傳遞,然後在其它函數內調用執行

func simple(a func(a, b int) int) {  
    fmt.Println(a(60, 7))
}

func main() {  
    f := func(a, b int) int {
        return a + b
    }
    simple(f)   //67
}
2.4 將函數作爲返回值

函數可以作爲其它函數的返回值進行傳遞

func simple() func(a, b int) int {  
    f := func(a, b int) int {
        return a + b
    }
    return f
}

func main() {  
    s := simple()
    fmt.Println(s(60, 7))  //67
}

在上面程序中, simple 函數返回了一個函數,並接受兩個 int 參數,返回一個 int

我們調用了 simple 函數,並把 simple 的返回值賦值給了 s。現在 s 包含了 simple 函數返回的函數。我們調用了 s,並向它傳遞了兩個 int 參數,程序輸出 67。

2.5 閉包

閉包是由函數及其相關的引用環境組合而成的實體 (即:閉包 = 函數 + 引用環境)。在函數式語言中,當內嵌函數體內引用到體外的變量時,將會把定義時涉及到的引用環境和函數體打包成一個整體(閉包)。

在 Go 語言中,匿名函數就是一個閉包,它可以直接引用外部函數的局部變量。

func main() {  
    a := 5
    func() {
        fmt.Println("a =", a)
    }()
}

在上面的程序中,匿名函數訪問了變量 a,而 a 存在於函數體的外部。因此這個匿名函數就是閉包。

每一個閉包都會綁定它自己的外圍變量(Surrounding Variable)。

func appendStr() func(string) string {  
    t := "Hello"
    c := func(b string) string {
        t = t + " " + b
        return t
    }
    return c
}

func main() {  
    a := appendStr()
    b := appendStr()
    fmt.Println(a("World"))     //Hello World  
    fmt.Println(b("Everyone"))  //Hello Everyone

    fmt.Println(a("Gopher"))    //Hello World Gopher
    fmt.Println(b("!"))         //Hello Everyone !
}

在上面程序中,函數 appendStr 返回了一個閉包。這個閉包綁定了變量 t

我們首先用參數 World 調用了 a。現在 at 值變爲了 Hello World。然後我們又用參數 Everyone 調用了 b。由於 b 綁定了自己的變量 t,因此 b 中的 t 還是等於初始值 Hello。於是該函數調用之後,b 中的 t 變爲了 Hello Everyone

閉包的使用

package main

import (  
    "fmt"
)

type student struct {  
    firstName string
    lastName  string
    grade     string
    country   string
}

func filter(s []student, f func(student) bool) []student {  
    var r []student
    for _, v := range s {
        if f(v) == true {
            r = append(r, v)
        }
    }
    return r
}

func main() {  
    s1 := student{
        firstName: "Naveen",
        lastName:  "Ramanathan",
        grade:     "A",
        country:   "India",
    }
    s2 := student{
        firstName: "Samuel",
        lastName:  "Johnson",
        grade:     "B",
        country:   "USA",
    }
    s := []student{s1, s2}
    f := filter(s, func(s student) bool {
        if s.grade == "B" {
            return true
        }
        return false
    })
    fmt.Println(f)     //[{Samuel Johnson B USA}]
}

在上面的代碼中,filter 的第二個參數是一個函數。這個函數接收 student 參數,返回一個 bool 值。這個函數計算了某一學生是否滿足篩選條件。我們遍歷了 student 切片,將每個學生作爲參數傳遞給了函數 f。如果該函數返回 true,就表示該學生通過了篩選條件,接着將該學生添加到了結果切片 r 中。

main 函數中,我們首先創建了兩個學生 s1s2,並將他們添加到了切片 s。現在假設我們想要查詢所有成績爲 B 的學生。爲了實現這樣的功能,我們傳遞了一個檢查學生成績是否爲 B 的函數,如果是,該函數會返回 true。我們把這個函數作爲參數傳遞給了 filter 函數。上述程序會輸出:

[{Samuel Johnson B USA}]

假設我們想要查找所有來自印度的學生。通過修改傳遞給 filter 的函數參數,就很容易地實現了。

c := filter(s, func(s student) bool {  
    if s.country == "India" {
        return true
    }
    return false
})
fmt.Println(c)      //[{Naveen Ramanathan A India}]

3. 方法

func 這個關鍵字和方法名中間加入了一個特殊的接收器類型。接收器可以是結構體類型或者是非結構體類型。接收器是可以在方法的內部訪問的。

func (t Type) methodName(parameter_list) (return_value_list) {
}
3.1 使用示例
package main

import (
 "fmt"
)

type Employee struct {
 name     string
 salary   int
 currency string
}

/*
  displaySalary() 方法將 Employee 做爲接收器類型
*/
func (e Employee) displaySalary() {
 fmt.Printf("Salary of %s is %s%d \n", e.name, e.currency, e.salary)
}

/*
displaySalary()方法被轉化爲一個函數,把 Employee 當做參數傳入。
*/
func displaySalary(e Employee) {
 fmt.Printf("Salary of %s is %s%d \n", e.name, e.currency, e.salary)
}

func main() {
 emp1 := Employee {
  name:     "Sam Adolf",
  salary:   5000,
  currency: "$",
 }
 emp1.displaySalary() //調用方法    Salary of Sam Adolf is $5000 

 displaySalary(emp1)  //調用函數    Salary of Sam Adolf is $5000 
}
3.2 使用原因

Go 不是純粹的面向對象編程語言,而且 Go 不支持類。因此,基於類型的方法是一種實現和類相似行爲的途徑。

相同的名字的方法可以定義在不同的類型上,而相同名字的函數是不被允許的。

package main

import (
    "fmt"
    "math"
)

type Rectangle struct {
    length int
    width  int
}

type Circle struct {
    radius float64
}

func (r Rectangle) Area() int {
    return r.length * r.width
}

func (c Circle) Area() float64 {
    return math.Pi * c.radius * c.radius
}

func main() {
    r := Rectangle{
        length: 10,
        width:  5,
    }
    fmt.Printf("Area of rectangle %d\n", r.Area())
    c := Circle{
        radius: 12,
    }
    fmt.Printf("Area of circle %f", c.Area())
}
3.3 指針接收器與值接收器
package main

import (
 "fmt"
)

type Employee struct {
 name string
 age  int
}

/*
使用值接收器的方法。
*/
func (e Employee) changeName(newName string) {
 e.name = newName
}

/*
使用指針接收器的方法。
*/
func (e *Employee) changeAge(newAge int) {
 e.age = newAge
}

func main() {
 e := Employee{
  name: "Mark Andrew",
  age:  50,
 }
 fmt.Printf("Employee name before change: %s", e.name)   //Employee name before change: Mark Andrew
 e.changeName("Michael Andrew")
 fmt.Printf("\nEmployee name after change: %s", e.name)  //Employee name after change: Mark Andrew

 fmt.Printf("\n\nEmployee age before change: %d", e.age) //Employee age before change: 50
 (&e).changeAge(51)
 fmt.Printf("\nEmployee age after change: %d", e.age)    //Employee age after change: 51
 e.changeAge(52)
 fmt.Printf("\nEmployee age after change: %d", e.age)    //Employee age after change: 52
}

在上面的程序中,changeName 方法有一個值接收器 (e Employee),而 changeAge 方法有一個指針接收器 (e *Employee)。在 changeName 方法中對 Employee 結構體的字段 name 所做的改變對調用者是不可見的,因此程序在調用 e.changeName("Michael Andrew") 這個方法的前後打印出相同的名字。由於 changeAge 方法是使用指針 (e *Employee) 接收器的,所以在調用 (&e).changeAge(51) 方法對 age 字段做出的改變對調用者將是可見的。我們使用 (&e).changeAge(51) 來調用 changeAge 方法。由於 changeAge 方法有一個指針接收器,所以我們使用 (&e) 來調用這個方法。其實沒有這個必要,Go 語言讓我們可以直接使用 e.changeAge(51)e.changeAge(51) 會自動被 Go 語言解釋爲 (&e).changeAge(51)

一般來說,指針接收器可以使用在:

3.4 非結構體上的方法

爲了在一個類型上定義一個方法,方法的接收器類型定義和方法的定義應該在同一個包中。

package main

func (a int) add(b int) {
}

func main() {

}

在上面程序中,我們嘗試把一個 add 方法添加到內置的類型 int。這是不允許的,因爲 add 方法的定義和 int 類型的定義不在同一個包中。該程序會拋出編譯錯誤 cannot define new methods on non-local type int

我們可以爲內置類型 int 創建一個類型別名,然後創建一個以該類型別名爲接收器的方法。

package main

import "fmt"

type myInt int

func (a myInt) add(b myInt) myInt {
 return a + b
}

func main() {
 num1 := myInt(5)
 num2 := myInt(10)
 sum := num1.add(num2)
 fmt.Println("Sum is", sum)
}
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/lkVbZ1-ps4BzloveQb2ceA