1. 程式人生 > 其它 >Golang學習-CH5 Go語言函式

Golang學習-CH5 Go語言函式

目錄

Go 語言支援普通函式匿名函式閉包,從設計上對函式進行了優化和改進,讓函式使用起來更加方便。

Go 語言的函式屬於“一等公民”(first-class),也就是說:

  • 函式本身可以作為值進行傳遞。
  • 支援匿名函式和閉包(closure)。
  • 函式可以滿足介面。

5.1 Go語言函式宣告和定義

  • 在Go語言中,函式的基本組成為:關鍵字 func、函式名、引數列表、返回值、函式體和返回語句。
  • Go語言裡面擁三種類型的函式:
    • 普通的帶有名字的函式
    • 匿名函式或者 lambda 函式
    • 方法

普通函式宣告(定義)

func 函式名(形式引數列表)(返回值列表){
    函式體
}

//如果一組形參或返回值有相同的型別,不必為每個形參都寫出引數型別
func f(i, j, k int, s, t string) { /* ... */ }
func f(i int, j int, k int, s string, t string) { /* ... */ }

//示例
func add(x int, y int) int {return x + y}
func sub(x, y int) (z int) { z = x - y; return}
func first(x int, _ int) int { return x }
func zero(int, int) int { return 0 }
fmt.Printf("%T\n", add) // "func(int, int) int"
fmt.Printf("%T\n", sub) // "func(int, int) int"
fmt.Printf("%T\n", first) // "func(int, int) int"
fmt.Printf("%T\n", zero) // "func(int, int) int"
  • Go語言沒有預設引數值,也沒有任何方法可以通過引數名指定形參,因此形參和返回值的變數名對於函式呼叫者而言沒有意義。

  • 在函式中,實參通過值傳遞的方式進行傳遞,因此函式的形參是實參的拷貝,對形參進行修改不會影響實參。但是,如果實參包括引用型別,如指標、slice(切片)、map、function、channel 等型別,實參可能會由於函式的間接引用被修改。

  • Go語言中函式沒有聲明後定義函式體的操作,必須直接完成宣告和函式體內容

返回值

Go語言支援多返回值,多返回值能方便地獲得函式執行後的多個返回引數。

  • 一般來說,多返回值的最後一個返回引數返回函式執行中可能出現的錯誤。
  • Go語言既支援安全指標,也支援多返回值,因此在使用函式進行邏輯編寫時更為方便。

多個返回值:

  • 用括號將多個返回值型別括起來,用逗號分隔每個返回值的型別。
  • 使用 return 語句返回時,值列表的順序需要與函式宣告的返回值型別一致。
func typedTwoValues() (int, int) {
    return 1, 2
}
func main() {
    a, b := typedTwoValues()
    fmt.Println(a, b)
}

帶有變類名的返回值:

  • 純型別的返回值對於程式碼可讀性不是很友好
  • Go語言支援對返回值進行命名,這樣返回值就和引數一樣擁有引數變數名和型別
  • 命名的返回值變數的預設值為型別的預設值
//當函式使用命名返回值時,可以在 return 中不填寫返回值列表
func namedRetValues() (a, b int) {
    a = 1
    b = 2
    return
}

注意:同一種類型返回值和命名返回值兩種形式只能二選一,混用時將會發生編譯錯誤

func namedRetValues() (a, b int, int)

5.2 Go語言函式中引數傳遞影響

  • 常規傳遞:使用普通數值變數時,傳遞引數型別為值傳遞。函式內部對引數的處理不會影響外部。
  • 指標傳遞:使用指標變數傳遞地址,會影響到外部實參
  • 陣列名作為引數傳遞:不同於其他語言,go語言在將陣列名作為函式引數的時候,引數傳遞即是對陣列的複製。在形參中對陣列元素的修改都不會影響到陣列元素原來的值。
  • 切片作為引數傳遞:在使用slice作為函式引數時,進行引數傳遞將是一個地址拷貝,即將底層陣列的記憶體地址複製給引數slice。
  • 函式作為引數:在go語言中,函式也作為一種資料型別,所以函式也可以作為函式的引數來使用。
func function(a, b int, sum func(int, int) int) {

    fmt.Println(sum(a, b))

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

func main() {
    var a, b int = 5, 6
    f := sum
    function(a, b, f)
}

5.3 Go語言函式變數

在Go語言中,函式也是一種型別,可以和其他型別一樣儲存在變數中。

示例見5.2

5.4 Go語言字串的鏈式處理

使用 SQL 語言從資料庫中獲取資料時,可以對原始資料進行排序(sort by)、分組(group by)和去重(distinct)等操作,SQL 將資料的操作與遍歷過程作為兩個部分進行隔離,這樣操作和遍歷過程就可以各自獨立地進行設計,這就是常見的資料與操作分離的設計

對資料的操作進行多步驟的處理被稱為鏈式處理,本例中使用多個字串作為資料集合,然後對每個字串進行一系列的處理,使用者可以通過系統函式或者自定義函式對鏈式處理中的每個環節進行自定義。

https://mip.yht7.com/golang/5458.html

package main

import (
    "fmt"
    "strings"
)
// 字串處理函式,傳入字串切片和處理鏈
func StringProccess(list []string, chain []func(string) string) {
    // 遍歷每一個字串
    for index, str := range list {
        // 第一個需要處理的字串
        result := str
        // 遍歷每一個處理鏈
        for _, proc := range chain {
            // 輸入一個字串進行處理,返回資料作為下一個處理鏈的輸入。
            result = proc(result)
        }
        // 將結果放回切片
        list[index] = result
    }
}

// 自定義的移除字首的處理函式
func removePrefix(str string) string {
    return strings.TrimPrefix(str, "go")
}

func main() {
    // 待處理的字串列表
    list := []string{
        "go scanner",
        "go parser",
        "go compiler",
        "go printer",
        "go formater",
    }

    // 處理函式鏈
    chain := []func(string) string{
        removePrefix,
        strings.TrimSpace,
        strings.ToUpper,
    }

    // 處理字串
    StringProccess(list, chain)
    // 輸出處理好的字串
    for _, str := range list {
        fmt.Println(str)
    }
}

鏈式處理器是一種常見的程式設計設計,Netty 是使用 Java 語言編寫的一款非同步事件驅動的網路應用程式框架,支援快速開發可維護的高效能的面向協議的伺服器和客戶端,Netty 中就有類似的鏈式處理器的設計。

5.5 Go語言匿名函式

Go語言支援匿名函式,即在需要使用函式時再定義函式,匿名函式沒有函式名只有函式體,函式可以作為一種型別被賦值給函式型別的變數,匿名函式也往往以變數方式傳遞。

//定義格式
func(引數列表)(返回引數列表){
    函式體
}

定義時呼叫匿名函式:直接定義並使用

func(data int) {
    fmt.Println("hello", data)
}(100)

將匿名函式賦值給變數:

// 將匿名函式體儲存到f()中
f := func(data int) {
    fmt.Println("hello", data)
}
// 使用f()呼叫
f(100)

匿名函式的用途非常廣泛,它本身就是一種值,可以方便地儲存在各種容器中實現回撥函式和操作封裝。

匿名函式用作回撥函式:

使用者傳入不同的匿名函式體可以實現對元素不同的遍歷操作

// 遍歷切片的每個元素, 通過給定函式進行元素訪問
func visit(list []int, f func(int)) {
    for _, v := range list {
        f(v)
    }
}
func main() {
    // 使用匿名函式列印切片內容
    visit([]int{1, 2, 3, 4}, func(v int) {
        fmt.Println(v)
    })
}

使用匿名函式實現操作封裝:

package main
import (
    "flag"
    "fmt"
)
//定義命令列引數 skill,將結果傳入skillParam指標
var skillParam = flag.String("skill", "", "skill to perform")
func main() {
    flag.Parse()//解析命令列引數
    //字串對映到函式的map
    //使用匿名函式作為value
    var skill = map[string]func(){
        "fire": func() {
            fmt.Println("chicken fire")
        },
        "run": func() {
            fmt.Println("soldier run")
        },
        "fly": func() {
            fmt.Println("angel fly")
        },
    }
    //在map查詢是否有對應命令
    if f, ok := skill[*skillParam]; ok {
        f()
    } else {
        fmt.Println("skill not found")
    }
}

5.6 Go語言函式型別實現介面*

先跳,等介面結束

5.7 Go語言閉包*

Go語言中閉包是引用了自由變數的函式,被引用的自由變數和函式一同存在,即使已經離開了自由變數的環境也不會被釋放或者刪除,在閉包中可以繼續使用這個自由變數,因此,簡單的說:

函式 + 引用環境 = 閉包

同一個函式與不同引用環境組合,可以形成不同的例項。引用環境可以指外部變數的引入。

拓展:https://zhuanlan.zhihu.com/p/92634505

在閉包內部修改引用的變數

閉包對它作用域上部的變數可以進行修改,修改引用的變數會對變數進行實際修改。

// 準備一個字串
str := "hello world"
// 建立一個匿名函式
foo := func() {
    // 匿名函式中訪問str
    str = "hello dude"
}
// 呼叫匿名函式
foo()  //str會被修改

閉包的記憶效應

被捕獲到閉包中的變數讓閉包本身擁有了記憶效應,閉包中的邏輯可以修改閉包捕獲的變數,變數會跟隨閉包生命期一直存在,閉包本身就如同變數一樣擁有了記憶效應。可以理解為閉包接著變數一直存在下去。

//累加器示例

// 提供一個值, 每次呼叫函式會指定對值進行累加
func Accumulate(value int) func() int {
    // 返回一個閉包
    return func() int {
        // 累加
        value++
        // 返回一個累加值
        return value
    }
}
func main() {
    // 建立一個累加器, 初始值為1
    accumulator := Accumulate(1)
    // 累加1並列印
    fmt.Println(accumulator())
    fmt.Println(accumulator())
    // 列印累加器的函式地址
    fmt.Printf("%p\n", &accumulator)
    // 建立一個累加器, 初始值為1
    accumulator2 := Accumulate(10)
    // 累加1並列印
    fmt.Println(accumulator2())
    // 列印累加器的函式地址
    fmt.Printf("%p\n", &accumulator2)
}
  • accumulator 與 accumulator2 輸出的函式地址不同,因此它們是兩個不同的閉包例項。
  • 每呼叫一次 accumulator 都會自動對引用的變數進行累加。

閉包實現生成器

閉包的記憶效應被用於實現類似於設計模式中工廠模式的生成器,下面的例子展示了建立一個玩家生成器的過程。

// 建立一個玩家生成器, 輸入名稱, 輸出生成器
func playerGen(name string) func() (string, int) {
    // 血量一直為150
    hp := 150
    // 返回建立的閉包
    return func() (string, int) {
        // 將變數引用到閉包中
        return name, hp
    }
}
func main() {
    // 建立一個玩家生成器
    generator := playerGen("high noon")
    // 返回玩家的名字和血量
    name, hp := generator()
    // 列印值
    fmt.Println(name, hp)
}

5.8 Go語言可變引數

類似C語言的printf()函式,Go語言標準庫中的 fmt.Println() 等函式的實現也依賴於語言的可變引數功能。

單一型別、數量可變

func myfunc(args ...int) {
    for _, arg := range args {
        fmt.Println(arg)
    }
}

//呼叫示例:
myfunc(2, 3, 4)
myfunc(1, 3, 7, 13)
  • 函式 myfunc() 接受不定數量的引數,這些引數的型別全部是 int
  • 從內部實現機制來說,型別...type本質上是一個數組切片,也就是[]type,所以可以直接使用range進行遍歷。
  • 作為一個語法糖,如果不採用這種方式,完全可以使用陣列切片替代。但是這種缺陷使得呼叫方也得使用切片作為引數傳入。
func myfunc2(args []int) {
    for _, arg := range args {
        fmt.Println(arg)
    }
}
//功能完全等價,呼叫示例
myfunc2([]int{1, 3, 7, 13})

語法糖(syntactic sugar),即這種語法對語言的功能並沒有影響,但是更方便程式設計師使用,通常來說,使用語法糖能夠增加程式的可讀性,從而減少程式出錯的可能。

如果要獲取可變引數的數量,可以使用 len() 函式對可變引數變數對應的切片進行求長度操作,以獲得可變引數數量。

可變型別、可變數量

如果希望傳任意型別,可以指定型別為 interface{}。用 interface{} 傳遞任意型別資料是Go語言的慣例用法,使用 interface{} 仍然是型別安全的

func MyPrintf(args ...interface{}) {
    for _, arg := range args {
        switch arg.(type) {
            case int:
                fmt.Println(arg, "is an int value.")
            case string:
                fmt.Println(arg, "is a string value.")
            case int64:
                fmt.Println(arg, "is an int64 value.")
            default:
                fmt.Println(arg, "is an unknown type.")
        }
    }
}
func main() {
    var v1 int = 1
    var v2 int64 = 234
    var v3 string = "hello"
    var v4 float32 = 1.234
    MyPrintf(v1, v2, v3, v4)
}

在多個可變引數函式中傳遞引數

可變引數變數是一個包含所有引數的切片,如果要將這個含有可變引數的變數傳遞給下一個可變引數函式,可以在傳遞時給可變引數變數後面新增...,這樣就可以將切片中的元素進行傳遞,而不是傳遞可變引數變數本身。

// 實際列印的函式
func rawPrint(rawList ...interface{}) {
    // 遍歷可變引數切片
    for _, a := range rawList {
        // 列印引數
        fmt.Println(a)
    }
}
// 列印函式封裝
func print(slist ...interface{}) {
    // 將slist可變引數切片完整傳遞給下一個函式
    rawPrint(slist...)	//傳遞了可變引數
}
func main() {
    print(1, 2, 3)	
}
  • 如果程式碼修改為rawPrint(slist),則傳遞進的是切片整體,列印的結果為[1,2,3]

5.9 Go語言defer(延遲執行語句)

Go語言的 defer 語句會將其後面跟隨的語句進行延遲處理,在 defer 歸屬的函式即將返回時,將延遲處理的語句按 defer 的逆序進行執行,也就是說,先被 defer 的語句最後被執行,最後被 defer 的語句,最先被執行。

關鍵字 defer 的用法類似於面向物件程式語言 Java 和 C# 的 finally 語句塊,它一般用於釋放某些已分配的資源,典型的例子就是對一個互斥解鎖,或者關閉一個檔案。

多個延遲執行語句的處理順序

以逆序執行,棧順序。

func main() {
    fmt.Println("defer begin")
    // 將defer放入延遲呼叫棧
    defer fmt.Println(1)
    defer fmt.Println(2)
    // 最後一個放入, 位於棧頂, 最先呼叫
    defer fmt.Println(3)
    fmt.Println("defer end")
}
//結果:
//defer begin
//defer end
//3
//2
//1
  • 程式碼的延遲順序與最終的執行順序是反向的。
  • 延遲呼叫是在 defer 所在函式結束時進行,函式結束可以是正常返回時,也可以是發生宕機時。

使用延遲執行語句在函式退出時釋放資源

處理業務或邏輯中涉及成對的操作是一件比較煩瑣的事情,比如開啟和關閉檔案、接收請求和回覆請求、加鎖和解鎖等。在這些操作中,最容易忽略的就是在每個函式退出處正確地釋放和關閉資源。

解決了C中函式末尾經常忘記關閉和釋放。使用 defer 能非常方便地處理資源釋放問題。

  • 使用延遲併發解鎖
//未使用defer的情況:
var (
    // 一個演示用的對映
    valueByKey      = make(map[string]int)
    // 保證使用對映時的併發安全的互斥鎖,map預設不是併發安全,使用互斥量保護
    valueByKeyGuard sync.Mutex
)
// 根據鍵讀取值
func readValue(key string) int {
    // 對共享資源加鎖
    valueByKeyGuard.Lock()
    // 取值
    v := valueByKey[key]
    // 對共享資源解鎖
    valueByKeyGuard.Unlock()
    return v
}

//使用defer簡化的情況:
func readValue(key string) int {
    valueByKeyGuard.Lock()
    // defer後面的語句不會馬上呼叫, 而是延遲到函式結束時呼叫
    defer valueByKeyGuard.Unlock()
    return valueByKey[key]
}
  • 使用延遲釋放檔案控制代碼
func fileSize(filename string) int64 {
    f, err := os.Open(filename)
    if err != nil {
        return 0
    }
    // 延遲呼叫Close, 此時Close不會被呼叫
    defer f.Close()
    info, err := f.Stat()
    if err != nil {
        // defer機制觸發, 呼叫Close關閉檔案
        return 0
    }
    size := info.Size()
    // defer機制觸發, 呼叫Close關閉檔案
    return size
}
  • defer在函式結束後會自動呼叫,免去了不同錯誤情況下可能出現多個檔案關閉函式。

5.10 Go語言遞迴函式

Go語言也支援遞迴函式,遞迴函式能解決分而治之的問題。

構成遞迴需要具備以下條件:

  • 一個問題可以被拆分成多個子問題;
  • 拆分前的原問題與拆分後的子問題除了資料規模不同,但處理問題的思路是一樣的;
  • 不能無限制的呼叫本身,子問題需要有退出遞迴狀態的條件。
//斐波那契數列
func main() {
    result := 0
    for i := 1; i <= 10; i++ {
        result = fibonacci(i)
        fmt.Printf("fibonacci(%d) is: %d\n", i, result)
    }
}
func fibonacci(n int) (res int) {
    if n <= 2 {
        res = 1
    } else {
        res = fibonacci(n-1) + fibonacci(n-2)
    }
    return
}

5.11 Go語言處理執行時錯誤*

Go語言的錯誤處理思想及設計包含以下特徵:

  • 對於開發者:一個可能造成錯誤的函式,需要返回值中返回一個錯誤介面(error),如果呼叫是成功的,錯誤介面將返回 nil,否則返回錯誤。
  • 對於使用者:在函式呼叫後需要檢查錯誤,如果發生錯誤,則進行必要的錯誤處理。

Go語言沒有類似 Java 或 .NET 中的異常處理機制,雖然可以使用 defer、panic、recover 模擬,但官方並不主張這樣做,Go語言的設計者認為其他語言的異常機制已被過度使用,上層邏輯需要為函式發生的異常付出太多的資源,同時,如果函式使用者覺得錯誤處理很麻煩而忽略錯誤,那麼程式將在不可預知的時刻崩潰。

Go語言希望開發者將錯誤處理視為正常開發必須實現的環節,正確地處理每一個可能發生錯誤的函式,同時,Go語言使用返回值返回錯誤的機制,也能大幅降低編譯器、執行時處理錯誤的複雜度,讓開發者真正地掌握錯誤的處理。

//net包中的例子
func Dial(network, address string) (Conn, error) {
    var d Dialer
    return d.Dial(network, address)
}
//錯誤介面的定義格式
type error interface {
    Error() string
}

自定義一個錯誤

errors包:

在Go語言中,使用 errors 包進行錯誤的定義,格式如下:

var err = errors.New("this is an error")


// 建立錯誤物件
func New(text string) error {
    return &errorString{text}
}
// 錯誤字串
type errorString struct {
    s string
}
// 返回發生何種錯誤
func (e *errorString) Error() string {
    return e.s
}

錯誤字串由於相對固定,一般在包作用域宣告,應儘量減少在使用時直接使用 errors.New 返回

使用示例:

package main
import (
    "errors"
    "fmt"
)
// 定義除數為0的錯誤
var errDivisionByZero = errors.New("division by zero")
func div(dividend, divisor int) (int, error) {
    // 判斷除數為0的情況並返回
    if divisor == 0 {
        return 0, errDivisionByZero
    }
    // 正常計算,返回空錯誤
    return dividend / divisor, nil
}
func main() {
    fmt.Println(div(1, 0))
}

藉助自定義結構體實現錯誤介面*

使用 errors.New 定義的錯誤字串的錯誤型別是無法提供豐富的錯誤資訊的,那麼,如果需要攜帶錯誤資訊返回,就需要藉助自定義結構體實現錯誤介面。

示例:返回錯誤描述時,帶回檔名和行號資訊。

package main
import (
    "fmt"
)
// 宣告一個解析錯誤
type ParseError struct {
    Filename string // 檔名
    Line     int    // 行號
}
// 實現error介面,返回錯誤描述
func (e *ParseError) Error() string {
    return fmt.Sprintf("%s:%d", e.Filename, e.Line)
}
// 建立一些解析錯誤
func newParseError(filename string, line int) error {
    return &ParseError{filename, line}
}
func main() {
    var e error
    // 建立一個錯誤例項,包含檔名和行號
    e = newParseError("main.go", 1)
    // 通過error介面檢視錯誤描述
    fmt.Println(e.Error())
    // 根據錯誤介面具體的型別,獲取詳細錯誤資訊
    switch detail := e.(type) {
    case *ParseError: // 這是一個解析錯誤
        fmt.Printf("Filename: %s Line: %d\n", detail.Filename, detail.Line)
    default: // 其他型別的錯誤
        fmt.Println("other error")
    }
}

http://c.biancheng.net/view/62.html

5.12 Go語言宕機(panic)

Go語言的型別系統會在編譯時捕獲很多錯誤,但有些錯誤只能在執行時檢查,如陣列訪問越界、空指標引用等,這些執行時錯誤會引起宕機。宕機不是一件很好的事情,可能造成體驗停止、服務中斷,但是宕機同時也是一種合理的止損方法。

一般而言,當宕機發生時,程式會中斷執行,並立即執行在該 goroutine(可以先理解成執行緒)中被延遲的函式(defer 機制)。隨後,程式崩潰並輸出日誌資訊,日誌資訊包括 panic value 和函式呼叫的堆疊跟蹤資訊,panic value 通常是某種錯誤資訊。

對於每個 goroutine,日誌資訊中都會有與之相對的,發生 panic 時的函式呼叫堆疊跟蹤資訊,通常,我們不需要再次執行程式去定位問題,日誌資訊已經提供了足夠的診斷依據,因此,在我們填寫問題報告時,一般會將宕機和日誌資訊一併記錄。

雖然Go語言的 panic 機制類似於其他語言的異常,但 panic 的適用場景有一些不同,由於 panic 會引起程式的崩潰,因此 panic 一般用於嚴重錯誤,如程式內部的邏輯不一致。任何崩潰都表明了我們的程式碼中可能存在漏洞,所以對於大部分漏洞,我們應該使用Go語言提供的錯誤機制,而不是 panic。

手動觸發宕機

Go語言可以在程式中手動觸發宕機,讓程式崩潰,這樣開發者可以及時地發現錯誤,同時減少可能的損失。

Go語言程式在宕機時,會將堆疊和 goroutine 資訊輸出到控制檯,所以宕機也可以方便地知曉發生錯誤的位置。

package main
func main() {
    panic("crash")
}

//func panic(v interface{})    //panic() 的引數可以是任意型別

在執行依賴的必備資源缺失時主動觸發宕機

func MustCompile(str string) *Regexp {
    regexp, error := Compile(str)
    if error != nil {
        panic(`regexp: Compile(` + quote(str) + `): ` + error.Error())
    }
    return regexp
}

手動宕機進行報錯的方式不是一種偷懶的方式,反而能迅速報錯,終止程式繼續執行,防止更大的錯誤產生,不過,如果任何錯誤都使用宕機處理,也不是一種良好的設計習慣,因此應根據需要來決定是否使用宕機進行報錯。

宕機時觸發延遲執行語句

當 panic() 觸發的宕機發生時,panic() 後面的程式碼將不會被執行,但是在 panic() 函式前面已經執行過的 defer 語句依然會在宕機發生時發生作用

package main
import "fmt"
func main() {
    defer fmt.Println("宕機後要做的事情1")
    defer fmt.Println("宕機後要做的事情2")
    panic("宕機")
}

這個特性可以用來在宕機發生前進行宕機資訊處理。

5.13 Go語言宕機恢復(recover)

Recover 是一個Go語言的內建函式,可以讓進入宕機流程中的 goroutine 恢復過來,recover 僅在延遲函式 defer 中有效

在正常的執行過程中,呼叫 recover 會返回 nil 並且沒有其他任何效果,如果當前的 goroutine 陷入恐慌,呼叫 recover 可以捕獲到 panic 的輸入值,並且恢復正常的執行。

通常來說,不應該對進入 panic 宕機的程式做任何處理,但有時,需要我們可以從宕機中恢復,至少我們可以在程式崩潰前,做一些操作,舉個例子,當 web 伺服器遇到不可預料的嚴重問題時,在崩潰前應該將所有的連線關閉,如果不做任何處理,會使得客戶端一直處於等待狀態,如果 web 伺服器還在開發階段,伺服器甚至可以將異常資訊反饋到客戶端,幫助除錯。
在其他語言裡,宕機往往以異常的形式存在,底層丟擲異常,上層邏輯通過 try/catch 機制捕獲異常,沒有被捕獲的嚴重異常會導致宕機,捕獲的異常可以被忽略,讓程式碼繼續執行。
Go語言沒有異常系統,其使用 panic 觸發宕機類似於其他語言的丟擲異常,recover 的宕機恢復機制就對應其他語言中的 try/catch 機制。

讓程式在崩潰時繼續執行*

http://c.biancheng.net/view/64.html

package main
import (
    "fmt"
    "runtime"
)
// 崩潰時需要傳遞的上下文資訊
type panicContext struct {
    function string // 所在函式
}
// 保護方式允許一個函式
func ProtectRun(entry func()) {
    // 延遲處理的函式
    defer func() {
        // 發生宕機時,獲取panic傳遞的上下文並列印
        err := recover()
        switch err.(type) {
        case runtime.Error: // 執行時錯誤
            fmt.Println("runtime error:", err)
        default: // 非執行時錯誤
            fmt.Println("error:", err)
        }
    }()
    entry()
}
func main() {
    fmt.Println("執行前")
    // 允許一段手動觸發的錯誤
    ProtectRun(func() {
        fmt.Println("手動宕機前")
        // 使用panic傳遞上下文
        panic(&panicContext{
            "手動觸發panic",
        })
        fmt.Println("手動宕機後")
    })
    // 故意造成空指標訪問錯誤
    ProtectRun(func() {
        fmt.Println("賦值宕機前")
        var a *int
        *a = 1
        fmt.Println("賦值宕機後")
    })
    fmt.Println("執行後")
}

程式碼輸出結果:

執行前
手動宕機前
error: &{手動觸發panic}
賦值宕機前
runtime error: runtime error: invalid memory address or nil pointer dereference
執行後

對程式碼的說明:

  • 第 9 行宣告描述錯誤的結構體,儲存執行錯誤的函式。
  • 第 17 行使用 defer 將閉包延遲執行,當 panic 觸發崩潰時,ProtectRun() 函式將結束執行,此時 defer 後的閉包將會發生呼叫。
  • 第 20 行,recover() 獲取到 panic 傳入的引數。
  • 第 22 行,使用 switch 對 err 變數進行型別斷言。
  • 第 23 行,如果錯誤是有 Runtime 層丟擲的執行時錯誤,如空指標訪問、除數為 0 等情況,列印執行時錯誤。
  • 第 25 行,其他錯誤,列印傳遞過來的錯誤資料。
  • 第 44 行,使用 panic 手動觸發一個錯誤,並將一個結構體附帶資訊傳遞過去,此時,recover 就會獲取到這個結構體資訊,並打印出來。
  • 第 57 行,模擬程式碼中空指標賦值造成的錯誤,此時會由 Runtime 層丟擲錯誤,被 ProtectRun() 函式的 recover() 函式捕獲到。

panic和recover的關係

panic 和 recover 的組合有如下特性:

  • 有 panic 沒 recover,程式宕機。
  • 有 panic 也有 recover,程式不會宕機,執行完對應的 defer 後,從宕機點退出當前函式後繼續執行。

雖然 panic/recover 能模擬其他語言的異常機制,但並不建議在編寫普通函式時也經常性使用這種特性。

在 panic 觸發的 defer 函式內,可以繼續呼叫 panic,進一步將錯誤外拋,直到程式整體崩潰。

如果想在捕獲錯誤時設定當前函式的返回值,可以對返回值使用命名返回值方式直接進行設定。

5.14 Go語言一些函式應用

Go語言計算函式執行時間

函式的執行時間的長短是衡量這個函式效能的重要指標。在Go語言中我們可以使用 time 包中的 Since() 函式來獲取函式的執行時間。

package main
import (
    "fmt"
    "time"
)
func test() {
    start := time.Now() // 獲取當前時間
    sum := 0
    for i := 0; i < 100000000; i++ {
        sum++
    }
    elapsed := time.Since(start)
    fmt.Println("該函式執行完成耗時:", elapsed)
}
func main() {
    test()
}
  • Since() 函式返回從 t 到現在經過的時間,等價於time.Now().Sub(t)
func test() {
    start := time.Now() // 獲取當前時間
    sum := 0
    for i := 0; i < 100000000; i++ {
        sum++
    }
    elapsed := time.Now().Sub(start)
    fmt.Println("該函式執行完成耗時:", elapsed)
}

Go語言通過記憶體快取來提升效能

當在進行大量計算的時候,提升效能最直接有效的一種方式是避免重複計算,通過在記憶體中快取並重複利用快取從而避免重複執行相同計算的方式稱為記憶體快取。

其實就是動態規劃過程中的資訊記憶

Go語言雜湊函式

go語言中提供了MD5、SHA-1等幾種雜湊函式

package main
import (
    "crypto/md5"
    "crypto/sha1"
    "fmt"
)

func main() {
    TestString := "Hi, pandaman!"
    Md5Inst := md5.New()
    Md5Inst.Write([]byte(TestString))
    Result := Md5Inst.Sum([]byte(""))
    fmt.Printf("%x\n\n", Result)

    Sha1Inst := sha1.New()
    Sha1Inst.Write([]byte(TestString))
    Result = Sha1Inst.Sum([]byte(""))
    fmt.Printf("%x\n\n", Result)
}

5.15 Go語言函式的底層實現*

Go語言函式使用的是 caller-save 的模式,即由呼叫者負責儲存暫存器,所以在函式的頭尾不會出現push ebp; mov esp ebp這樣的程式碼,相反其是在主調函式呼叫被調函式的前後有一個儲存現場和恢復現場的動作。

http://c.biancheng.net/view/4784.html

5.16 Go語言Test功能測試函式

完善的測試體系,能夠提高開發的效率,當專案足夠複雜的時候,想要保證儘可能的減少 bug,有兩種有效的方式分別是程式碼稽核測試,Go語言中提供了 testing 包來實現單元測試功能。

Go語言自帶了 testing 測試包,可以進行自動化的單元測試,輸出結果驗證,並且可以測試效能。

測試規則

func TestXxx( t *testing.T ){
    //......
}
  • 測試用例檔案不會參與正常原始碼的編譯,不會被包含到可執行檔案中;
  • 測試用例的檔名必須以_test.go結尾;
  • 需要使用 import 匯入 testing 包;
  • 測試函式的名稱要以TestBenchmark開頭,後面可以跟任意字母組成的字串,但第一個字母必須大寫,例如 TestAbc(),一個測試用例檔案中可以包含多個測試函式;
  • 單元測試則以(t *testing.T)作為引數,效能測試以(t *testing.B)做為引數;
  • 測試用例檔案使用go test命令來執行,原始碼中不需要 main() 函式作為入口,所有以_test.go結尾的原始碼檔案內以Test開頭的函式都會自動執行。

單元測試

在同一資料夾下建立兩個Go語言檔案,分別命名為 demo.go 和 demt_test.go:

//demo.go
package demo
// 根據長寬獲取面積
func GetArea(weight int, height int) int {
    return weight * height
}
//demo_test.go
package demo
import "testing"
func TestGetArea(t *testing.T) {
    area := GetArea(40, 50)
    if area != 2000 {
        t.Error("測試失敗")
    }
}

執行測試命令:

go test -v

單元測試下最好沒有main.go

壓力測試

//改造demo_test.go
package demo
import "testing"
func BenchmarkGetArea(t *testing.B) {
    for i := 0; i < t.N; i++ {
        GetArea(40, 50)
    }
}

執行測試命令:

go test -bench="."

覆蓋率測試

覆蓋率測試能知道測試程式總共覆蓋了多少業務程式碼(也就是 demo_test.go 中測試了多少 demo.go 中的程式碼),可以的話最好是覆蓋100%。

//改造demo_test.go
package demo
import "testing"
func TestGetArea(t *testing.T) {
    area := GetArea(40, 50)
    if area != 2000 {
        t.Error("測試失敗")
    }
}
func BenchmarkGetArea(t *testing.B) {
    for i := 0; i < t.N; i++ {
        GetArea(40, 50)
    }
}