9 函數

函數

  1. go函數不支持嵌套、重載和默認參數
  2. 但支持一下特性:
  • 無須要申明原型、不定長度變參、多返回值、命名返回值參數和匿名函數、閉包
  1. 定義函數使用關鍵字func,且左大括號不能另起一行
  2. 函數也能夠做爲一種類型使用

如何定義函數

func 函數名(參數類型 參數,參數類型 參數)(返回值類型,返回值類型){ 左花邊號必須和func同行}python

func anet(a int,b string) (int,string) {

}

也能夠同時定義多個一樣類型的返回值和參數,代碼以下所示:算法

func anet(a ,b,c int) (a,b,c int) {  // abc都是int類型的

}

因爲a,b,c是咱們在返回值那個小括號內定義好了,因此咱們在函數內部的代碼裏面不須要再次定義,代碼以下所示:數組

func anet(a, b, c int) (a, b, c int) {
    a, b, c = 1, 2, 3  // 在返回值那個小括號內定義好了,因此不須要定義了
    return a, b, c
}

可變長度參數,咱們在傳入參數的時候有時候只知道這個參數是int型,殊不知道有多少個,因此這個時候須要使用可變長度參數,用三個點來替代,這個可變長度參數等同於python裏面的**kwargs或者*args,都只能放在參數裏面的最後一位閉包

func anet(a ...int) {  // ...表示可變長度參數,參數類型全爲int型
    a, b, c = 1, 2, 3
    return a, b, c
}

func main() {
    anet1("a", 1, 2, 3, 4, 5)
}
func anet1(b string, a ...int) {  // 可變長度參數只能放在最後面使用

    fmt.Println(b, a)
}

函數參數

對於函數參數傳遞方式來講,有兩種:app

  1. 值傳遞
  2. 引用傳遞
    不管是值傳遞,仍是引用傳遞,傳遞給函數的都是變量的副本,不過,值傳遞是值的拷貝。引用傳遞是地址的拷貝,通常來講,地址拷貝更爲高效。而值拷貝取決於拷貝的對象大小,對象越大,則性能越低。函數

  3. map、slice、chan、指針、interface默認以引用的方式傳遞。
  4. 多返回值的時候,咱們並不須要從新定義變量,直接等於號賦值就好了。性能

咱們傳入一個數組到函數裏面,再在這個函數裏面去修改這個數組的值,看會不會對原始數組發生改變:
先看例子:測試

package main

import (
    "fmt"
)

func main() {
    s2 := []int{1, 2, 3, 4, 5}
    fmt.Println(s2)
    anet(s2)
    fmt.Println(s2)
}

func anet(s []int) {
    s[0] = 7
    s[1] = 8
    fmt.Println(s)
}
// 打印結果以下
[1 2 3 4 5]
[7 8 3 4 5]
[7 8 3 4 5]

由打印結果可得知,若是函數的參數是一個數組,那麼傳入到這個函數的數組在函數內部進行了操做,那麼其原始數組也會變更,因傳入參數的時候是直接傳入的一個內存地址進去讓函數使用的,因此可以對原始數組進行更改。指針

那麼看看輸入普通的數字有沒有這樣的效果code

package main

import (
    "fmt"
)

func main() {

    a, b := 1, 2
    fmt.Println(a, b)
    anet(a, b)
    fmt.Println(a, b)

}
func anet(s ...int) {
    s[0] = 7
    s[1] = 8
    fmt.Println(s)
}
// day

多返回值
多返回值的時候不須要從新定義了

func returnName(a, b int) (total, sub int) {
    total = a + b   // 不須要從新定義了,直接使用=賦值便可
    sub = a / b    
    return
}

func main() {
    t, s := returnName(10, 34)
    fmt.Println(t, s)
}

把函數當作參數傳入到另外一個函數
把某一個函數做爲參數傳入到另外一個函數裏面

// func1
package main

import (
    "fmt"
)

type add_func func(int, int) int

func add(a, b int) int {
    return a + b
}

func operator(op add_func, a, b int) int {
    return op(a, b)
}

func main() {
    c := add
    fmt.Println(c)

    su := c(10, 20)
    fmt.Println(su)

    ss := operator(add, 12, 30)   // 把函數當作參數傳入進去
    fmt.Println(ss)
}

不定長參數

// func1
package main

import (
    "fmt"
)



func add2(arg ...int) int {
    s := 0
    for _, v := range arg {
        s += v
    }
    return s
}

func add3(a, b int, args ...int) int {
    s := a + b
    for _, v := range args {
        s += v
    }
    return s
}

func addStr(a string, args ...string) string {
    s := a
    for _, v := range args {
        s += v
    }
    return s
}

func main() {
    fmt.Println(add2(1, 2, 3, 4, 5, 6, 10))  // 多個參數
    fmt.Println(add3(2, 3, 111, 333, 444, 555))   // 傳入2個或者多個參數
    fmt.Println(addStr("asdf", "adfuwe", "123", "siw", "sdc"))  // 傳入一個或者多個參數
}

匿名函數

沒有給這個函數命名的函數叫作匿名函數,代碼例子以下:

func main() {
    a := func() {  // 直接把函數賦值給a,未對這個函數進行命名
        fmt.Println("Func A")
    }
    a()
}

閉包

閉包的做用就是返回一個匿名函數,請看代碼

func main() {
    f := closure(10)
    fmt.Println(f(1))
    fmt.Println(f(2))
}
func closure(x int) func(int) int {    // 其實是這麼寫的func closure(x int) (func(int) int)
//只不過在編譯的時候自動給你轉換了,實際是說返回值是函數,int型的
    return func(y int) int {
        fmt.Printf("%p", &x)
        return x + y
    }
}

defer

  • 它的執行方式相似於其餘語言中的析構函數(python裏面的__del__),在函數執行結束後按照調用順序的相反順序逐個執行
  • 即便函數發生嚴重的錯誤也會執行
  • 支持匿名函數的調用
  • 經常使用語資源清理,文件關閉,解鎖以及記錄時間等操做
  • 經過與匿名函數配合可在return以後修改函數計算結果
  • 若是函數體內某個變量做爲defer時匿名函數的參數,則在定義defer時即已經得到了拷貝,不然則是引用某個變量的地址
  • Go沒有異常機制,可是有panic/recover模式來處理錯誤
  • Panic能夠在任何地方引起,單recover只有在defer調用的函數中有效
  • defer 語句的調用時遵守先進後出的原則,即最後一個defer語句就先被執行。只不過,當你須要爲defer語句到底哪一個先執行這種細節而煩惱的時候,說明你的代碼結構可能須要調整一下了。
  • 當函數返回時,執行defer語句。所以,能夠用來作資源清理
  • 多個defer語句,按先進後出的方式執行
  • defer語句中的變量,在defer聲明時就決定了。

defer代碼例子

咱們看下這個代碼例子

func main() {
    for i := 0; i < 3; i++ {
        defer func() {
            fmt.Println(i)
        }()
    }
}
// 打印結果:
3
3
3

爲何打印結果都是3呢,由於defer是最後執行的,在for循環結束後才執行,然而在for循環結束後i是等於3了,因此打印結果是3

defer與panic與recover的結合使用

  • painc是觸發異常的,當一個函數執行過程當中調用panic()函數的時候,正常的函數執行流程當即終止,但函數中以前使用defer關鍵字延遲執行的語句將正常展開執行,以後該函數將返回到調用函數,並致使逐層向上執行panic流程,直至所屬的goroutine中全部正在執行的函數終止。錯誤信息將被報告,包括在調用panic()函數時傳入的參數,這個過程稱爲錯誤處理流程。
  • recover()函數用於終止錯誤處理流程。通常狀況下,recover()應該在一個使用defer關鍵字的函數中執行以有效截取錯誤處理流程。若是沒有在發生異常的goroutine中明確調用恢復過程(使用recover關鍵字),會致使該goroutine所屬的進程打印異常信息後直接退出

咱們利用defer在panic觸發異常的時候進行糾正,使程序依舊可以正常運行,代碼以下:

// heh1
package main

import (
    "fmt"
)

func main() {
    A()
    B()
    C()
}

func A() {
    fmt.Println("Func B")
}

func B() {

    defer func() {   // 必須在panic以前定義這個,不然會報錯找不到
        if err := recover(); err != nil {
            fmt.Println("recover in B")
        }
    }()
    panic("Func B")
}

func C() {
    fmt.Println("Func C")
}

運行時選擇函數

使用映射和函數引用來製造分支

咱們若是須要對某個目錄下的壓縮包進行解壓,壓縮包的格式有不少種,有zip,gz,rar,tar,tar.gz等等之類的格式,針對這些後綴名,若是咱們使用if條件去判斷類型後再去執行代碼,顯然目前是可行的,代碼效果以下:

if 後綴名=="zip" {
    unzip  文件名
}else if 後綴名=="rar" {
    解壓文件
}else if 後綴名=="tar" {
    解壓文件
}else if 後綴名=="targ.gz" {
    解壓文件
}

顯然針對幾種文件類型使用if判斷仍是能夠的,可是一旦文件類型成千上萬的時候,你以爲if語句寫下來那豈不是N多行了啊,這樣的if語句你受到了嗎?一樣的switch語句也是同樣,在面對不少條件的時候一樣顯得很臃腫,那麼該如何使用?
其實咱們能夠考慮使用map的映射關係來解決,這樣的話咱們只須要考慮在map添加文件類型與對應的解壓方法便可,不須要在寫if/switch條件語句來判斷了。
代碼以下:

package main

import (
    "fmt"
)

// 使用映射和函數引用來製造分支
var ff = map[string]func(string) ([]string, error){
    ".gz": GzipFile, ".zip": ZipFile, ".tar.gz": TarGz}

func GzipFile(file string) ([]string, error) {
    fmt.Println(file)
    xxx := []string{"a", "b"}
    return xxx, nil
}

func TarGz(file string) ([]string, error) {
    fmt.Println(file)
    xxx := []string{"a", "c"}
    return xxx, nil
}

func ZipFile(file string) ([]string, error) {
    fmt.Println(file)
    xxx := []string{"a", "b"}
    return xxx, nil
}

func ss(file string) {   // 總調用函數
    if function, ok := ff[file]; ok {
        function(file)
    }

}

func main() {
    ss(".tar.gz")
}

解釋下:

  1. 首先咱們須要定義一個map,map類型爲 key爲string,value爲func(string) ([]string, error) .
  2. 因爲咱們定義好了map,因此咱們下面全部對應的func函數必須是func(string) ([]string, error)這樣的格式,不然會報錯的。
  3. 定義好文件類型對應解壓方法之後,咱們就能夠寫一個總函數來經過map映射關係來調用對應的方法,這裏 的總函數就是ss。
  4. ss函數裏面左右各一個OK就能夠經過if判斷function是否有值了,function有值就能夠調用對應的解壓方法了。

動態函數建立

在運行時動態地選擇函數的另外一個場景即是,當咱們有兩個或者多個的函數實現了相同的功能時,好比使用了不一樣的算法等,咱們不但願在程序編譯時綁定到其中任何一個函數,例如容許咱們動態地選擇他們來作性能測試或者回歸測試等。
舉個例子:

//
package main

import (
    "os"
)

var IsPalindrome func(string) bool //

func init() {
    if len(os.Args) > 1 && (os.Args[1] == "-a" || os.Args[1] == "--ascii") {
        os.Args = append(os.Args[:1], os.Args[2:]...)
        IsPalindrome = func(s string) bool { // simple ascii version
            if len(s) <= 1 {
                return true
            }
            if s[0] != s[len(s)-1] {
                return false
            }
            return IsPalindrome(s[1 : len(s)-1])
        }
    } else {
        IsPalindrome = func(s string) bool { // UTF8 version
            if len(s) <= 1 {
                return true
            }
            if s[0] != s[len(s)-1] {
                return false
            }
            return IsPalindrome(s[1 : len(s)-1])
        }
    }
}
func main() {
}

代碼解釋下:

  1. 咱們根據輸入的命令來決定IsPalindrome()的實現方式,若是指定了"-a"或者"--ascii"參數,就將它從os.Args切片移除而後建立一個做用於ASCII碼的IsPalindrome的函數
  2. 若是沒有輸入ASCII選項,咱們就建立一個和以前同樣的函數,既能處理ASCII碼,也能夠UTF8碼。
  3. 程序其餘IsPalindrome函數能夠正常被調用,可是實際上什麼代碼會被執行徹底取決於咱們建立的是哪一個版本函數。

高階函數

所謂高階函數就是將一個或者多個其餘函數做爲本身的參數,並在函數裏調用他們。能夠看看下面最簡單的例子:

func SliceIndex(limit int,predicate func(i int) bool) int{
    for i:= 0;i < limit;i++{
        if predicate(i) {
            return i
        }
    }
    return -1
}

上面的函數很簡單,返回predicate()爲真時的索引值。它只知道一個長度信息和它的第二個參數,也是是對於任意給定索引值返回一個布爾值的函數,代表這個索引是否調用者所指望的。下面看看如何調用這個函數

func main() {
    xs := []int{1, 2, 3, 4}
    fmt.Println(SliceIndex(len(xs), func(i int) bool { return xs[i] == 3 }))
}
相關文章
相關標籤/搜索