1. 程式人生 > >Golang核心程式設計(4)-函式以及錯誤處理

Golang核心程式設計(4)-函式以及錯誤處理

可能很多習慣用C或Java的朋友發現,Add方法是以大寫開頭的,這並不符合駝峰式方法命名的規範,但在Go語言中,**以名字以大寫開頭的函式表示可被包之外的程式碼去呼叫,而以小寫開頭的函式則表明只能被本包呼叫,相當於Java中的private關鍵字的作用。**錯誤處理是學習任何程式語言都需要考慮的一個重要話題。在早期的語言中,錯誤處理不是語言規範的一部分,通常只作為一種程式設計正規化存在,比如C語言中的 errno 。但自C++語言以來,語言層面上會增加錯誤處理的支援,比如異常(exception)的概念和 try-catch 關鍵字的引入。`Go語言在此功能上考慮得更為深遠。漂亮的錯誤處理規範是Go語言最大的亮點之一。

一、函式

1.1、函式定義

前面我們已經大概介紹過函式,這裡我們用一個最簡單的加法函式來進行詳細說明:

package mymath
import "errors"
func Add(a int, b int) (ret int, err error) {
	if a < 0 || b < 0 { // 假設這個函式只支援兩個非負數字的加法
	err= errors.New("Should be non-negative numbers!")
	return
}
	return a + b, nil  // 支援多重返回值
}

如果引數列表中若干個相鄰的引數型別的相同,比如上面例子中的 a 和 b ,則可以在引數列表中省略前面變數的型別宣告,如下所示:

func Add(a, b int)(ret int, err error) {
	// ...
}

如果返回值列表中多個返回值的型別相同,也可以用同樣的方式合併。
如果函式只有一個返回值,也可以這麼寫:

func Add(a, b int) int {
	// ...
}

1.2、函式呼叫

函式呼叫非常方便,只要事先匯入了該函式所在的包,就可以直接按照如下所示的方式呼叫函式:

import "mymath"// 假設Add被放在一個叫mymath的包中
	// ...
c := mymath.Add(1, 2)

可能很多習慣用C或Java的朋友發現,Add方法是以大寫開頭的,這並不符合駝峰式方法命名的規範,但在Go語言中,以名字以大寫開頭的函式表示可被包之外的程式碼去呼叫,而以小寫開頭的函式則表明只能被本包呼叫,相當於Java中的private關鍵字的作用。

1.3、不定引數

接觸過Java的朋友應該知道Java中有的方法可以使用不定引數,而在Go語言中,也同樣提供了這項機制。

public static void fun1(int ...numbers){
        for (int number : numbers) {
            System.out.println(number);
        }
    }

 public static void main(String[] args) {
        fun1(1,2,3,4,5,6,7);
    }
1.3.1、不定引數型別

不定引數是指函式傳入的引數個數為不定數量,為了做到這點,首先需要將函式定義為接受不定引數型別:

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

這段程式碼的意思是,函式 myfunc() 接受不定數量的引數,這些引數的型別全部是 int ,所以它可以用如下方式呼叫:

myfunc(2, 3, 4)
myfunc(1, 3, 7, 13)

形如 …type 格式的型別只能作為函式的引數型別存在,並且必須是最後一個引數。它是一個語法糖(syntactic sugar),即這種語法對語言的功能並沒有影響,但是更方便程式設計師使用。通常來說,使用語法糖能夠增加程式的可讀性,從而減少程式出錯的機會。
從內部實現機理上來說,型別...type本質上是一個數組切片,也就是 []type,這也是為什麼上面的引數args可以用for迴圈來獲得每個傳入的引數。

假如沒有 ...type這樣的語法糖,開發者將不得不這麼寫:

func myfunc2(args []int) {
	for _, arg := range args {
	fmt.Println(arg)
}
}
1.3.2、任意型別的不定引數

之前的例子中將不定引數型別約束為 int ,如果你希望傳任意型別,可以指定型別為
interface{}

func function(args ...interface{}){
	// ...
}

在Java中,這相當於:

public static void function(int ...Object){
       //...
    }

1.4、多返回值

與C、C++和Java等開發語言的一個極大不同在於,Go語言的函式或者成員的方法可以有多個返回值,這個特效能夠使我們寫出比其他語言更優雅、更簡潔的程式碼,比如 File.Read() 函式就可以同時返回讀取的位元組數和錯誤資訊。如果讀取檔案成功,則返回值中的 n 為讀取的位元組數, err 為 nil ,否則 err 為具體的出錯資訊
func (file *File) Read(b []byte) (n int, err Error)

同樣,從上面的方法原型可以看到,我們還可以給返回值命名,就像函式的輸入引數一樣。返回值被命名之後,它們的值在函式開始的時候被自動初始化為空。在函式中執行不帶任何引數的 return 語句時,會返回對應的返回值變數的值。

如果呼叫方呼叫了一個具有多返回值的方法,但是卻不想關心其中的某個返回值,可以簡單地用一個下劃線“ _ ”來跳過這個返回值,比如下面的程式碼表示呼叫者在讀檔案的時候不想關心Read() 函式返回的錯誤碼:
n, _ := f.Read(buf)

二、錯誤處理

錯誤處理是學習任何程式語言都需要考慮的一個重要話題。在早期的語言中,錯誤處理不是語言規範的一部分,通常只作為一種程式設計正規化存在,比如C語言中的 errno 。但自C++語言以來,語言層面上會增加錯誤處理的支援,比如異常(exception)的概念和 try-catch 關鍵字的引入。`Go語言在此功能上考慮得更為深遠。漂亮的錯誤處理規範是Go語言最大的亮點之一。

2.1、error 介面

Go語言引入了一個關於錯誤處理的標準模式,即error 介面,該介面的定義如下:

type error interface {
	Error() string
}

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

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

//呼叫時的程式碼建議按如下方式處理錯誤情況:
n, err := Foo(0)
if err != nil {
// 錯誤處理
} else {
// 使用返回值n
}

2.2、自定義的錯誤型別

首先,定義一個用於承載錯誤資訊的型別。因為Go語言中介面的靈活性,你根本不需要從error 介面繼承或者像Java一樣需要使用 implements來明確指定型別和介面之間的關係,具體程式碼如下:

type PathError struct {
	Op string
	Path string
	Err error
}

如果這樣的話,編譯器又怎能知道 PathError 可以當一個 error 來傳遞呢?關鍵在於下面的程式碼實現了 Error() 方法:

func (e *PathError) Error() string {
	return e.Op + " " + e.Path + ": " + e.Err.Error()
}

syscall.Stat()失敗返回 err 時,將該 err 包裝到一個 PathError 物件中返回:

    func Stat(name string) (fi FileInfo, err error) {
        var stat syscall.Stat_t
                err = syscall.Stat(name, &stat)
        if err != nil {
            return nil, &PathError {"stat", name, err}
        }
        return fileInfoFromStat(&stat, name), nil
    }

2.3、defer

關鍵字 defer是Go語言引入的一個非常有意思的特性,defer關鍵字宣告的程式碼或者方法無論是否有錯誤出現都會繼續執行下去,比如這些關閉資源的方法,可以用defer宣告。

    func CopyFile(dst, src string) (w int64, err error) {
        srcFile, err := os.Open(src)
        if err != nil {
            return
        }
        defer srcFile.Close()
        dstFile, err := os.Create(dstName)
        if err != nil {
            return
        }
        defer dstFile.Close()
        return io.Copy(dstFile, srcFile)
    }

即使其中的 Copy() 函式丟擲異常,Go仍然會保證 dstFile 和 srcFile 會被正常關閉。
如果覺得一句話幹不完清理的工作,也可以使用在 defer 後加一個匿名函式的做法:

defer func() {
	// 做你複雜的清理工作
} ()

另外,一個函式中可以存在多個 defer 語句,因此需要注意的是, defer 語句的呼叫是類似於堆疊(Stack)遵照先進後出的原則,即最後一個 defer 語句將最先被執行。

2.4、panic()和recover()

Go語言引入了兩個內建函式 panic()recover()以報告和處理執行時錯誤和程式中的錯誤場景:

func panic(interface{})
func recover() interface{}

當在一個函式執行過程中呼叫panic()函式時,正常的函式執行流程將立即終止,但函式中之前使用defer關鍵字延遲執行的語句將正常展開執行,之後該函式將返回到呼叫函式,並導致逐層向上執行 panic流程,直至所屬的goroutine中所有正在執行的函式被終止。錯誤資訊將被報告,包括在呼叫panic() 函式時傳入的引數,這個過程稱為錯誤處理流程。

panic()的引數型別interface{}我們可以得知,該函式接收任意型別的資料,比如整
型、字串、物件等
。呼叫方法很簡單,下面為幾個例子:

panic(404)
panic("network broken")
panic(Error("file not exists"))

recover()函式用於終止錯誤處理流程。一般情況下, recover()應該在一個使用 defer
關鍵字的函式中執行以有效擷取錯誤處理流程。如果沒有在發生異常的goroutine中明確呼叫恢復過程(使用 recover 關鍵字),會導致該goroutine所屬的程序列印異常資訊後直接退出。