1. 程式人生 > >golang 系列 (四) defer、error、panic, go語句

golang 系列 (四) defer、error、panic, go語句

defer語句

defer 語句僅能被放置在函式或方法中。defer 不能呼叫內建函式以及標準庫程式碼包 unsafe 的函式.

// 讀取檔案, 轉換為 byte 陣列
func readFile(path string) ([]byte, error) {
	file,err := os.Open(path)
	if err != nil {
		return nil,err
	}
	// 定義延遲語句, 無論本函式正常返回還是發生了異常其中的file.Close()都會在本函式即將退出那一刻被執行。
	defer file.Close()
	return ioutil.ReadAll(file)
}

多個 defer 語句

函數出現多個defer語句時,會按出現順序的倒序執行。

func deferPrint() {
    defer func(){
        fmt.Print(1)
    }()
    defer func() {
        fmt.Print(2)
    }()
    defer func() {
        fmt.Print(3)
    }()
    fmt.Print(4)
}

得到的結果是: 4321

如果 defer 呼叫的函式有引數傳遞的話, 要注意在程式執行到該語句時就已經傳遞進去並執行了.

func deferPrint2() {
	// 定義函式物件
	f := func(i int) int {
		fmt.Printf("%d ", i)
		return i * 10
	}
	for i := 1; i < 5; i++ {
		// 呼叫函式物件時傳遞到函式中的引數會被直接取值並且函式物件會直接執行. 但 defer 呼叫的語句卻會按照壓棧的順序先進後出. 
		defer fmt.Printf("%d ", f(i))
	}
}

得到的結果是: 1 2 3 4 40 30 20 10

如果 defer 呼叫匿名函式時引用了外部變數的話, 結果會在函式呼叫完畢最後一刻統一取該外部變數的值. 如下函式因為defer 語句在最後執行時, for迴圈已經全部執行完了, 此時變數 i = 5 , 所以最終打印出來的結果就都是 5 .

func deferPrint3() {
	for i := 1; i < 5; i++ {
		// defer 呼叫匿名函式時引用了外部變數
		defer func() {
			fmt.Printf("%d ", i)
		}()
	}
}

得到的結果是: 5 5 5 5

若想得到理想的結果應該把變數作為引數傳遞到匿名函式中. 如下:

func deferPrint4() {
	for i := 1; i < 5; i++ {
		defer func(n int) {
			fmt.Printf("%d ", n)
		}(i)
	}
}

得到的結果是: 4 3 2 1

error 語句

error是Go語言內建的一個介面型別. 如果一個類中包含 Error() string 這個函式就相當於實現了 error 介面.

產生並判斷 error 的方式如下:

// 讀取檔案, 轉換為 byte 陣列
func readFile(path string) ([]byte, error) {	
	// 如果 path 為空的話返回異常.
	if path == ""{
		// 建立異常物件返回
		return nil, errors.New("The path is empty!")
	}
	file, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	// 定義延遲語句, 無論本函式正常返回還是發生了異常其中的file.Close()都會在本函式即將退出那一刻被執行。
	defer file.Close()
	return ioutil.ReadAll(file)
}

error 的型別

當產生某種異常時, 我們可以使用 Golang 定義的錯誤型別來判斷該錯誤屬於哪種型別. 比如:

// 代表讀取方已無更多資料可讀
if err == io.EOF{
	// 關閉流	
	file.Close()
}

panic 語句

理解:
error(錯誤) 指的是可預見的錯誤. 例如在只接收日期格式的輸入函式中接收到其他格式.
panic(異常) 指的是在不可能出現的地方出現錯誤. 例如使用日期格式接收了其他格式的輸入.

總結:
當錯誤出現時如果沒有處理或者處理不當時就會演變成異常.

使用:

func main() {
    fmt.Println("Starting the program")
    panic("A severe error occurred: stopping the program!")
    fmt.Println("Ending the program")
}

列印結果:

Starting the program
panic: A severe error occurred: stopping the program!

goroutine 1 [running]:
main.main()

panic 函式可接受一個 interface{} 型別的值作為其引數,interface{} 代表空介面。Go中的任何型別都是它的實現型別。(類似於 java 中的 Object 概念) 即我們可以傳任何型別的引數給 panic。如果不處理異常, 會造成程式崩潰的後果.

recover 語句

處理 panic 可以使用內部函式 recover

使用:

// 定義x, y變數
var x, y int

func main() {
	fmt.Println("啟動程式")
	initNum()
	step1()
	fmt.Printf("結束程式. x = %d, y = %d", x, y)
}

// 初始化變數值
func initNum()  {
	x = 1
	y = 2
}

func step1() {
	// 定義處理異常的defer函式, 要注意defer recover函式必須定義在panic之前
	defer func() {
		if e := recover(); e != nil {
			fmt.Printf("出現異常: %s\n", e)
			// 恢復資料
			initNum()
		}
	}()
	// 調換x, y的值
	x ,y = 2, 1
	// 使用panic建立異常
	if x == 2{
		panic("x == 2")
	}
}	

列印結果:

啟動程式
出現異常: x == 2
結束程式. x = 1, y = 2

recover 函式會返回一個 interface{} 型別的值,如果 e 不為 nil 那麼就說明有異常產生。這時要根據情況做相應處理。一旦defer語句中的 recover 函式被呼叫,異常就會被恢復.

go 語句

go語句和通道型別是Go語言的併發程式設計理念的最終體現。首先, go 語句是非同步並且不即時執行的. 例如:

func main() {
    go fmt.Println("Go!")
}

這個程式不會列印結果. 因為在 go 語句非同步執行之前 main 函式已經執行完畢並且退出程式了. 由於 go 語句執行時間的不確定性, 所以 Golang 中提供了幾種方式來協調執行.

方式1 : time.Sleep

func main() {
    go fmt.Println("Go!")
	// 主執行緒延遲結束100毫秒
    time.Sleep(100 * time.Millisecond)
}

列印結果 : Go!

方式2 : runtime.Gosched

func main() {
    go fmt.Println("Go!!")
    runtime.Gosched()
}

列印結果 : Go!!

runtime.Gosched 函式的作用是讓當前正在執行的 Goroutine (這裡是執行 main 函式的那個Goroutine)暫停,而讓系統轉去執行其他的 Goroutine (對應 go fmt.Println("Go!!") 的那個Goroutine)。

方式3 : sync.WaitGroup

func main() {
	var wg sync.WaitGroup
	wg.Add(3)
	go func () {
		fmt.Println("Go!1")
		wg.Done()
	}()

	go func() {
		fmt.Println("Go!2")
		wg.Done()
	}()

	go func() {
		fmt.Println("Go!3")
		wg.Done()
	}()

	wg.Wait()
}

列印結果:

Go!3
Go!1
Go!2

sync.WaitGroup 有三個方法, Add()Done()Wait()
sync.WaitGroup 內部有個值代表非同步任務的數量, 初始化為 0 , 每次呼叫 Add 函式會增加指定的數量。
Done 會使任務數量減 1 。
wait 方法會使當前 Goroutine 阻塞直到任務數量為 0。
上例中我們在 main 函式啟用了三個 Goroutine 來封裝三個Go函式。每個匿名函式的最後都呼叫了wg.Done 方法。當這三個 go 函式都呼叫過 wg.Done 函式之後,處於main函式最後的那條 wg.Wait() 語句就不會阻塞,程式執行結束。