【Go語言】錯誤與異常處理機制


①error接口

Go語言中的error類型實際上是抽象了Error()方法的error接口

type error interface {
    Error() string
}

Go語言使用該接口進行標准的錯誤處理。

對於大多數函數,如果要返回錯誤,大致上都可以定義為如下模式,將error作為多種返回
值中的最后一個,但這並非是強制要求:

func Foo(param int)(n int, err error) {
    // ...
}

調用時的代碼建議按如下方式處理錯誤情況:

n, err := Foo(0)
if err != nil {
    // 錯誤處理
} else {
    // 使用返回值n
}

看下面的例子綜合了一下error接口的用法:

package main

import (
    "fmt"
)

//自定義錯誤類型
type ArithmeticError struct {
    error   //實現error接口
}

//重寫Error()方法
func (this *ArithmeticError) Error() string {
    return "自定義的error,error名稱為算數不合法"
}

//定義除法運算函數
func Devide(num1, num2 int) (rs int, err error) {
    if num2 == 0 {
        return 0, &ArithmeticError{}
    } else {
        return num1 / num2, nil
    }
}
func main() {
    var a, b int
    fmt.Scanf("%d %d", &a, &b)

    rs, err := Devide(a, b)
    if err != nil {
        fmt.Println(err)
    } else {
        fmt.Println("結果是:", rs)
    }
}

 

運行,輸入參數5 2(正確的情況):

5 2
結果是: 2

 

若輸入5 0(產生錯誤的情況):

5 0
自定義的error,error名稱為算數不合法

 

通過上面的例子可以看出error類型類似於Java中的Exception類型,不同的是Exception必須搭配throw和catch使用。

②defer--延遲語句

在Go語言中,可以使用關鍵字defer向函數注冊退出調用,即主調函數退出時,defer后的函數才會被調用。
defer語句的作用是不管程序是否出現異常,均在函數退出時自動執行相關代碼。(相當於Java中的finally )

當函數執行到最后時,這些defer語句會按照逆序執行,最后該函數返回。

例如:

package main

import (
    "fmt"
)

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

 

其執行結果為:

4
3
2
1
0

 

defer語句在聲明時被加載到內存(多個defer語句按照FIFO原則) ,加載時記錄變量的值,而在函數返回之后執行,看下面的例子:

例子1:defer語句加載時記錄值

func f1() {
    i := 0
    defer fmt.Println(i) //實際上是將fmt.Println(0)加載到內存
    i++
    return
}
func main() {
    f1()
}

 

其結果顯然是0

例子2:在函數返回后執行

func f2() (i int) {
    var a int = 1
    defer func() {
        a++
        fmt.Println("defer內部", a)
    }()
    return a
}
func main() {
    fmt.Println("main中", f2())
}

 

其結果是

 

defer內部 2
main中 1

 

 

 

例子3:defer語句會讀取主調函數的返回值,並對返回值賦值.(注意和例子2的區別)

func f3() (i int) {
    defer func() {
        i++
    }()
    return 1
}
func main() {
    fmt.Println(f3())
}

 

其結果竟然是2.

通過上面的幾個例子,自然而然會想到用defer語句做清理工作,釋放內存資源(這樣你再也不會為Java中的try-catch-finally層層嵌套而苦惱了)

例如關閉文件句柄:

srcFile,err := os.Open("myFile")
defer srcFile.Close()

 

關閉互斥鎖:

mutex.Lock()
defer mutex.Unlock()

 

上面例子中defer語句的用法有兩個優點:

1.讓設計者永遠也不會忘記關閉文件,有時當函數返回時常常忘記釋放打開的資源變量。

2.將關閉和打開靠在一起,程序的意圖變得清晰很多。

下面看一個文件復制的例子:

package main

import (
    "fmt"
    "io"
    "os"
)

func main() {
    copylen, err := copyFile("dst.txt", "src.txt")
    if err != nil {
        return
    } else {
        fmt.Println(copylen)
    }

}

//函數copyFile的功能是將源文件sec的數據復制給dst
func copyFile(dstName, srcName string) (copylen int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }
    //當return時就會調用src.Close()把源文件關閉
    defer src.Close()
    dst, err := os.Create(dstName)
    if err != nil {
        return
    }
    //當return是就會調用src.Close()把目標文件關閉
    defer dst.Close()
    return io.Copy(dst, src)
}

 

可以看到確實比Java簡潔許多。

③panic-recover運行時異常處理機制

Go語言中沒有Java中那種try-catch-finally結構化異常處理機制,而使用panic()函數答題throw/raise引發錯誤,

然后在defer語句中調用recover()函數捕獲錯誤,這就是Go語言的異常恢復機制——panic-recover機制

兩個函數的原型為:

func panic(interface{})//接受任意類型參數 無返回值
func recover() interface{}//可以返回任意類型 無參數

 

一定要記住,你應當把它作為最后的手段來使用,也就是說,你的代碼中應當沒有,或者很少有panic的東西。這是個強大的工具,請明智地使用
它。那么,我們應該如何使用它呢?

panic()
是一個內建函數,可以中斷原有的控制流程,進入一個令人panic(恐慌即Java中的異常)的流程中。當函數F調用panic,函數F的執行被中
斷,但是F中的延遲函數(必須是在panic之前的已加載的defer)會正常執行,然后F返回到調用它的地方。在調用的地方,F的行為就像調用了panic。這一
過程繼續向上,直到發生panic的goroutine中所有調用的函數返回,此時程序退出。異常可以直接調用panic產
生。也可以由運行時錯誤產生,例如訪問越界的數組。

recover()
是一個內建的函數,可以讓進入令人恐慌的流程中的goroutine恢復過來。recover僅在延遲函數中有效。在正常
的執行過程中,調用recover會返回nil,並且沒有其它任何效果。如果當前的goroutine陷入panic,調用
recover可以捕獲到panic的輸入值,並且恢復正常的執行。

一般情況下,recover()應該在一個使用defer關鍵字的函數中執行以有效截取錯誤處理流程。如果沒有在發生異常的goroutine中明確調用恢復

過程(使用recover關鍵字),會導致該goroutine所屬的進程打印異常信息后直接退出。

這里結合自定義的error類型給出一個使用panic和recover的完整例子:

package main

import (
    "fmt"
)

//自定義錯誤類型
type ArithmeticError struct {
    error
}

//重寫Error()方法
func (this *ArithmeticError) Error() string {
    return "自定義的error,error名稱為算數不合法"
}

//定義除法運算函數***這里與本文中第一幕①error接口的例子不同
func Devide(num1, num2 int) int {
    if num2 == 0 {
        panic(&ArithmeticError{}) //當然也可以使用ArithmeticError{}同時recover等到ArithmeticError類型
    } else {
        return num1 / num2
    }
}
func main() {
    var a, b int
    fmt.Scanf("%d %d", &a, &b)

    defer func() {
        if r := recover(); r != nil {
            fmt.Printf("panic的內容%v\n", r)
            if _, ok := r.(error); ok {
                fmt.Println("panic--recover()得到的是error類型")
            }
            if _, ok := r.(*ArithmeticError); ok {
                fmt.Println("panic--recover()得到的是ArithmeticError類型")
            }
            if _, ok := r.(string); ok {
                fmt.Println("panic--recover()得到的是string類型")
            }
        }
    }()

    rs := Devide(a, b)
    fmt.Println("結果是:", rs)
}

 

其執行的結果為:

使用與上面相同的測試數據,輸入5 2得:

5 2
結果是: 2

 

輸入5 0得:

5 0
panic的內容自定義的error,error名稱為算數不合法
panic--recover()得到的是error類型
panic--recover()得到的是ArithmeticError類型

 

可見已將error示例程序轉換為了Java中的用法,但是在大多數程序中使用error處理的方法較多。

需要注意的是:defer語句定義的位置 如果defer放在了

 rs := Devide(a, b)語句之后,defer將沒有機會執行即下面的程序失效

rs := Devide(a, b)
    defer func() {
        if r := recover(); r != nil {
            fmt.Printf("panic的內容%v\n", r)
            if _, ok := r.(error); ok {
                fmt.Println("panic--recover()得到的是error類型")
            }
            if _, ok := r.(*ArithmeticError); ok {
                fmt.Println("panic--recover()得到的是ArithmeticError類型")
            }
            if _, ok := r.(string); ok {
                fmt.Println("panic--recover()得到的是string類型")
            }
        }
    }()

 

因為在在陷入panic之前defer語句沒有被加載到內存,而在執行panic時程序被中斷,因而無法執行defer語句。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM