天天看點

Go語言快速入門(3)--函數、延遲函數defer、錯誤處理panic、recover函數延遲函數錯誤處理機制

函數

  • Go語言的函數文法為,當函數傳回一個為命名的傳回值或者沒有傳回值的時候,傳回清單的圓括号可以省略
func name(paramtre-list) (result-list){
    body
}
           
  • 一個函數能夠傳回不止一個結果,例如下面一個非常簡單的交換swap函數
func swap(a int, b int) (int , int){
	return b, a
}

func main() {
	a := 1
	b := 2
	a ,b = swap(a, b)
	fmt.Println(a)
	fmt.Println(b)
}
           
  • 函數變量也有類型,可以指派給變量或者傳遞或者從其他函數中傳回,函數變量可以像其他函數一樣調用,函數類型的零值是nil空值
func square(n int) int {
	return n * n
}
func negative(n int) int {
	return -n
}
func product(m, n int)int {
	return m * n
}

func main() {
	f := square
	fmt.Println(f(3))

	f = negative
	fmt.Println(f(3))
	fmt.Printf("%T\n", f)

	//f = product	//錯誤 不能把func(int, int) int指派給 func(int) int
}
           
  • 疊代變量在for循環中遇到的問題,這個問題類似于js中的閉包
func main() {
	var slice []func() //定義一個函數變量類型的slice切片

	sli := []int{1, 2, 3, 4, 5}
	for _, v := range sli {
		fmt.Println(&v)
		slice = append(slice, func() {
			fmt.Println(v * v)
		})
	}
	for _, v := range slice {
		 v()
	}
}
//輸出
0xc0000180a8
0xc0000180a8
0xc0000180a8
0xc0000180a8
0xc0000180a8
25
25
25
25
25

//解決
func main() {
	var slice []func() //定義一個函數變量類型的slice切片

	sli := []int{1, 2, 3, 4, 5}
	for _, v := range sli {
		fmt.Println(&v)
        //定義一個局部變量
		b := v
		slice = append(slice, func() {
			fmt.Println(b * b)
		})
	}
	for _, v := range slice {
		 v()
	}
}
           

已經可以從上面列印v的位址看出問題,在循環中建立的所有函數變量共享相同的變量它是一個可通路的存儲位置,而不是固定的值,解決的辦法很簡單,隻需要引入一個局部變量即可

延遲函數

Go中的延遲函數會在目前函數傳回前執行傳入的參數,它經常被用于關閉檔案描述符、關閉資料庫連接配接以及解鎖資源,使用defer有以下幾個注意點

  • 調用順序
import "fmt"

func main() {
	defer fmt.Println("defer")
	fmt.Println("begin")
	fmt.Println("end")
}
//輸出
begin
end
defer
           
  • defer函數即使求值
func g(i int){
	fmt.Println("g i:", i)
}

func f(){
	i := 100
	defer g(i)
	fmt.Println("begin i:", i)
	i = 200
	fmt.Println("end i:", i)
	return
}

func main() {
	f()
}
//輸出結果
begin i: 100
end i: 200
g i: 100
           

g()函數雖然在f()函數傳回時,但是傳遞給g()函數的參數還是100,也就是說defer函數會被延遲調用,但是傳遞給defer函數的參數會在defer語句處就準備好

  • 反序調用
func f() {
	defer fmt.Println("defer01")
	fmt.Println("begin")
	defer fmt.Println("defer02")
	fmt.Println("----")
	defer fmt.Println("defer03")
	fmt.Println("end")
	return
}

func main() {
	f()
}
//輸出
begin
----
end
defer03
defer02
defer01
           

第一個defer函數最後被執行,可以猜測這是一種棧的結構

  • 與return執行的先後順序

defer、return與傳回值三者的執行邏輯應該是:return最先執行,return負責将結果寫入傳回值中,接着defer進行一些收尾的工作,最後函數攜帶目前傳回值退出

下面看兩個例子:

func test() int {	//放回值沒有命名
	var i int
	defer func() {
		i++
		fmt.Println("defer1", i)
	}()
	defer func() {
		i++
		fmt.Println("defer2", i)
	}()
	return i
}

func main() {
	fmt.Println("return:", test())
}
//輸出
defer2 1
defer1 2
return: 0
           
func test() (i int) {	//有名傳回值
	defer func() {
		i++
		fmt.Println("defer1", i)
	}()
	defer func() {
		i++
		fmt.Println("defer2", i)
	}()
	return i
}

func main() {
	fmt.Println("return:", test())
}
//輸出
defer2 1
defer1 2
return: 2
           

因為return的操作不是一個原子操作,分為指派和傳回值的操作,對于例子1,因為傳回值沒有命名,是以return預設指定了一個傳回值假設為s,首先将i指派給s,後續的操作是針對i的自然不會影響s,對于例子2,因為是有名的傳回值,是以每一次的defer的操作都是基于i的

錯誤處理機制

Go語言的錯誤處理機制,多傳回值是前提條件,在Go中内置了一個error接口來處理錯誤

// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
	Error() string
}

//下面是Go中内置的關于error接口的簡單實作
func New(text string) error {        
    return &errorString{text}
}
// errorString is a trivial implementation of error.
翻譯
// 把error轉換成String是錯誤的簡單實作
type errorString struct {        
    s string
}
func (e *errorString) Error() string { 
    return e.s
}

           

Go對錯誤的處理就是通過方法的傳回值告訴開發者需要對錯誤進行判斷和處理,錯誤是可見的

在go中内置了三個關鍵字:

  • panic:運作時發生了異常
  • defer:延遲函數,前面介紹過
  • recover:可以中止panic造成的程式崩潰,是一個隻能在defer中發揮作用的函數,在其他作用域中不會發揮作用
func TestPanic() {
	panic("發生了異常,程式退出")
}
//輸出
goroutine 1 [running]:
main.TestPanic(...)
        E:/GoProject/HelloProject/src/panicTest.go:6
main.main()
        E:/GoProject/HelloProject/src/panicTest.go:19 +0x45
           
func TestDeferAndRecover() {
	defer func() {
		if err := recover(); err != nil {
			fmt.Println("發生了異常,異常的資訊為", err)
		}
	}()
	panic("發生了異常")
}
//輸出
發生了異常,異常的資訊為 發生了異常
           

下面看一個經典的執行個體,除法案例,除數不能為0

func division(x, y int) (int , error){
	if y == 0 {
		return 0, errors.New("y is not zero")
	}
	z := x / y
	return z, nil
}

func main() {
	result, err := division(1, 0)
	if err != nil {
		fmt.Println("發生了異常,并捕獲到了,異常的資訊為", err)
		return
	}
	fmt.Println(result)
}
//輸出
發生了異常,并捕獲到了,異常的資訊為 y is not zero
           

上面一個簡單的例子說明了如何抛出異常,但是上面是可以預測的,如果面對無法預測的異常時,如何捕獲呢?這就要用到recover了

func division2(x, y int) (res int, err error){
	defer func(){
		if e := recover(); e != nil{
			err = e.(error)
		}
	}()
	res = x / y;
	return res, nil
}
func main() {
	result, err := division2(1, 0)
	if err != nil {
		fmt.Println("發生了異常,并捕獲到了,異常的資訊為", err)
		return
	}
	fmt.Println(result)
}
//輸出
發生了異常,并捕獲到了,異常的資訊為 runtime error: integer divide by zero
           

當調用division2(1, 0)時,一定會報除0異常,通過在defer中使用recover函數來捕獲發送的異常,如果不為空,将這個異常複制給傳回結果的變量err

還有一點需要注意的是,在go中一旦某一個協程發生了panic而沒有被recover,那麼整個go程式都會終止,這和Java的多線程不一樣,Java中某一個線程抛出了異常但是沒有被捕獲是不會影響主線程的

go