Golang核心編程(4)-函數以及錯誤處理


更多關於Golang核心編程知識的文章請看:Golang核心編程(0)-目錄頁


可能很多習慣用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所屬的進程打印異常信息後直接退出。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章