《Go 語言程式設計》線上閱讀地址:https://yar999.gitbooks.io/gopl-zh/content...
函式
函式宣告
- 函式宣告包括函式名、形式引數列表、返回值列表(可省略)以及函式體。
func name(parameter-list) (result-list) {
body
}
形式引數列表描述了函式的引數名以及引數型別。這些引數作為區域性變數,其值由引數呼叫者提供。返回值也可以像形式引數一樣被命名,在這種情況下,每個返回值被宣告成一個區域性變數,並初始化為其型別的零值。
- 用 _ 符號作為形參名可以強調某個引數未被使用。
func first(x int, _ int) int { return x }
- 函式的型別被稱為函式的識別符號。如果兩個函式形式引數列表和返回值列表中的變數型別一一對應,那麼這兩個函式被認為有相同的型別和識別符號。
- 在函式呼叫時,Go語言沒有預設引數值,也沒有任何方法可以透過引數名指定形參,因此形參和返回值的變數名對於函式呼叫者而言沒有意義。
- 實參透過值的方式傳遞,因此函式的形參是實參的複製。對形參進行修改不會影響實參。但是,如果實參包括引用型別,如指標,slice(切片)、map、function、channel等型別,實參可能會由於函式的引用而被修改。
- golang.org/x/… 目錄下儲存了一些由Go團隊設計、維護,對網路程式設計、國際化檔案處理、移動平臺、影像處理、加密解密、開發者工具提供支援的擴充套件包。未將這些擴充套件包加入到標準庫原因有二,一是部分包仍在開發中,二是對大多數Go語言的開發者而言,擴充套件包提供的功能很少被使用。
遞迴呼叫
- 大部分程式語言使用固定大小的函式呼叫棧,常見的大小從64KB到2MB不等。固定大小棧會限制遞迴的深度,當你用遞迴處理大量資料時,需要避免棧溢位;除此之外,還會導致安全性問題。與相反,Go語言使用可變棧,棧的大小按需增加(初始時很小)。這使得我們使用遞迴時不必考慮溢位和安全問題
- 雖然Go的垃圾回收機制會回收不被使用的記憶體,但是這不包括作業系統層面的資源,比如開啟的檔案、網路連線。因此我們必須顯式的釋放這些資源。
多返回值函式
- 呼叫多返回值函式時,返回給呼叫者的是一組值,呼叫者必須顯式的將這些值分配給變數:
links, err := findLinks(url)
如果某個值不被使用,可以將其分配給blank identifier:
links, _ := findLinks(url) // errors ignored
- 如果一個函式將所有的返回值都顯示的變數名,那麼該函式的return語句可以省略運算元。這稱之為bare return。
// CountWordsAndImages does an HTTP GET request for the HTML
// document url and returns the number of words and images in it.
func CountWordsAndImages(url string) (words, images int, err error) {
resp, err := http.Get(url)
if err != nil {
return
}
doc, err := html.Parse(resp.Body)
resp.Body.Close()
if err != nil {
err = fmt.Errorf("parsing HTML: %s", err)
return
}
words, images = countWordsAndImages(doc)
return
}
func countWordsAndImages(n *html.Node) (words, images int) { /* ... */ }
按照函式宣告中返回值列表的次序,返回所有的返回值,在上面的例子中,每一個return語句等價於:
return words, images, err
- 當一個函式有多處return語句以及許多返回值時,bare return 可以減少程式碼的重複,但是使得程式碼難以被理解。如果你沒有仔細的審查上面的程式碼,很難發現前2處return等價於
return 0,0,err
(Go會將返回值 words和images在函式體的開始處,根據它們的型別,將其初始化為0),最後一處return等價於return words,image,nil
。基於以上原因,不宜過度使用bare return。
錯誤
在Go的錯誤處理中,錯誤是軟體包API和應用程式使用者介面的一個重要組成部分,程式執行失敗僅被認為是幾個預期的結果之一。
對於那些將執行失敗看作是預期結果的函式,它們會返回一個額外的返回值,通常是最後一個,來傳遞錯誤資訊。
resp, err := http.Get(url)
- 內建的error是介面型別。nil意味著函式執行成功,non-nil表示失敗。對於non-nil的error型別,我們可以透過呼叫error的
Error
函式或者輸出函式獲得字串型別的錯誤資訊。
fmt.Println(err)
fmt.Printf("%v", err)
函式值
- 在Go中,函式被看作第一類值(first-class values):函式像其他值一樣,擁有型別,可以被賦值給其他變數,傳遞給函式,從函式返回。對函式值(function value)的呼叫類似函式呼叫。例子如下:
func square(n int) int { return n * n }
func negative(n int) int { return -n }
func product(m, n int) int { return m * n }
f := square
fmt.Println(f(3)) // "9"
f = negative
fmt.Println(f(3)) // "-3"
fmt.Printf("%T\n", f) // "func(int) int"
f = product // compile error: can't assign func(int, int) int to func(int) int
- 函式型別的零值是nil。呼叫值為nil的函式值會引起panic錯誤:
var f func(int) int
f(3) // 此處f的值為nil, 會引起panic錯誤
- 函式值可以與nil比較:
var f func(int) int
if f != nil {
f(3)
}
但是函式值之間是不可比較的,也不能用函式值作為map的key。
匿名函式
擁有函式名的函式只能在包級語法塊中被宣告,透過函式字面量(function literal),我們可繞過這一限制,在任何表示式中表示一個函式值。函式字面量的語法和函式宣告相似,區別在於func關鍵字後沒有函式名。函式值字面量是一種表示式,它的值被稱為匿名函式(anonymous function)。
函式字面量允許我們在使用函式時,再定義它。透過這種技巧,我們可以改寫之前對strings.Map的呼叫:
strings.Map(func(r rune) rune { return r + 1 }, "HAL-9000")
更為重要的是,透過這種方式定義的函式可以訪問完整的詞法環境(lexical environment),這意味著在函式中定義的內部函式可以引用該函式的變數。
// squares返回一個匿名函式。
// 該匿名函式每次被呼叫時都會返回下一個數的平方。
func squares() func() int {
var x int
return func() int {
x++
return x * x
}
}
透過這個例子,我們看到變數的生命週期不由它的作用域決定:squares返回後,變數x仍然隱式的存在於f中。
- 當匿名函式需要被遞迴呼叫時,我們必須首先宣告一個變數,再將匿名函式賦值給這個變數。如果不分成兩步,函式字面量無法與變數繫結,我們也無法遞迴呼叫該匿名函式,比如:
var visitAll func(items []string)
visitAll = func(items []string) {
......
visitAll(m[item])
......
}
否則會出現編譯錯誤
visitAll := func(items []string) {
// ...
visitAll(m[item]) // compile error: undefined: visitAll
// ...
}
可變引數
引數數量可變的函式稱為為可變引數函式。典型的例子就是fmt.Printf和類似函式。Printf首先接收一個的必備引數,之後接收任意個數的後續引數。
在宣告可變引數函式時,需要在引數列表的最後一個引數型別之前加上省略符號“…”,這表示該函式會接收任意數量的該型別引數。
func sum(vals...int) int {
total := 0
for _, val := range vals {
total += val
}
return total
}
sum函式返回任意個int型引數的和。在函式體中,vals被看作是型別為[] int
的切片。sum可以接收任意數量的int型引數:
fmt.Println(sum()) // "0"
fmt.Println(sum(3)) // "3"
fmt.Println(sum(1, 2, 3, 4)) // "10"
- 在上面的程式碼中,呼叫者隱式的建立一個陣列,並將原始引數複製到陣列中,再把陣列的一個切片作為引數傳給被調函式。如果原始引數已經是切片型別,我們該如何傳遞給sum?只需在最後一個引數後加上省略符。下面的程式碼功能與上個例子中最後一條語句相同。
values := []int{1, 2, 3, 4}
fmt.Println(sum(values...)) // "10"
// fmt.Println(sum(1, 2, 3, 4))
- 雖然在可變引數函式內部,
...int
型引數的行為看起來很像切片型別,但實際上,可變引數函式和以切片作為引數的函式是不同的。
func f(...int) {}
func g([]int) {}
fmt.Printf("%T\n", f) // "func(...int)"
fmt.Printf("%T\n", g) // "func([]int)"
- 可變引數函式經常被用於格式化字串。下面的errorf函式構造了一個以行號開頭的,經過格式化的錯誤資訊。函式名的字尾f是一種通用的命名規範,代表該可變引數函式可以接收Printf風格的格式化字串。
func errorf(linenum int, format string, args ...interface{}) {
fmt.Fprintf(os.Stderr, "Line %d: ", linenum)
fmt.Fprintf(os.Stderr, format, args...)
fmt.Fprintln(os.Stderr)
}
linenum, name := 12, "count"
errorf(linenum, "undefined: %s", name) // "Line 12: undefined: count"
...interfac{}
表示函式在format
引數後可以接收任意個任意型別的引數。interface{}
會在後面介紹。
Deferred 函式
你只需要在呼叫普通函式或方法前加上關鍵字defer,就完成了defer所需要的語法。當defer語句被執行時,跟在defer後面的函式會被延遲執行。直到包含該defer語句的函式執行完畢時,defer後的函式才會被執行,不論包含defer語句的函式是透過return正常結束,還是由於panic導致的異常結束。你可以在一個函式中執行多條defer語句,它們的執行順序與宣告順序相反。
defer語句經常被用於處理成對的操作,如開啟、關閉、連線、斷開連線、加鎖、釋放鎖。透過defer機制,不論函式邏輯多複雜,都能保證在任何執行路徑下,資源被釋放。釋放資源的defer應該直接跟在請求資源的語句後。
對檔案的操作
package ioutil
func ReadFile(filename string) ([]byte, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
return ReadAll(f)
}
- 處理互斥鎖
var mu sync.Mutex
var m = make(map[string]int)
func lookup(key string) int {
mu.Lock()
defer mu.Unlock()
return m[key]
}
- 除錯複雜程式時,defer機制也常被用於記錄何時進入和退出函式。下例中的bigSlowOperation函式,直接呼叫trace記錄函式的被調情況。bigSlowOperation被調時,trace會返回一個函式值,該函式值會在bigSlowOperation退出時被呼叫。透過這種方式, 我們可以只透過一條語句控制函式的入口和所有的出口,甚至可以記錄函式的執行時間,如例子中的start。需要注意一點:不要忘記defer語句後的圓括號,否則本該在進入時執行的操作會在退出時執行,而本該在退出時執行的,永遠不會被執行。
func bigSlowOperation() {
defer trace("bigSlowOperation")() // don't forget the extra parentheses
// ...lots of work…
time.Sleep(10 * time.Second) // simulate slow
operation by sleeping
}
func trace(msg string) func() {
start := time.Now()
log.Printf("enter %s", msg)
return func() {
log.Printf("exit %s (%s)", msg,time.Since(start))
}
}
每一次bigSlowOperation被呼叫,程式都會記錄函式的進入,退出,持續時間。(我們用time.Sleep模擬一個耗時的操作)
$ go build gopl.io/ch5/trace
$ ./trace
2015/11/18 09:53:26 enter bigSlowOperation
2015/11/18 09:53:36 exit bigSlowOperation (10.000589217s)
- 用 defer 函式記錄返回值(需要是命名返回值才能記錄)
func double(x int) (result int) {
defer func() { fmt.Printf("double(%d) = %d\n", x,result) }()
return x + x
}
_ = double(4)
// Output:
// "double(4) = 8"
- 被延遲執行的匿名函式甚至可以修改函式返回給呼叫者的返回值:
func triple(x int) (result int) {
defer func() { result += x }()
return double(x)
}
fmt.Println(triple(4)) // "12"
- 在迴圈體中的defer語句需要特別注意,因為只有在函式執行完畢後,這些被延遲的函式才會執行。下面的程式碼會導致系統的檔案描述符耗盡,因為在所有檔案都被處理之前,沒有檔案會被關閉。
for _, filename := range filenames {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close() // NOTE: risky; could run out of file
descriptors
// ...process f…
}
一種解決方法是將迴圈體中的檔案操作和defer語句移至另外一個函式。在每次迴圈時,呼叫這個函式。
for _, filename := range filenames {
if err := doFile(filename); err != nil {
return err
}
}
func doFile(filename string) error {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close()
// ...process f…
}
Panic 和 Recover
Go的型別系統會在編譯時捕獲很多錯誤,但有些錯誤只能在執行時檢查,如陣列訪問越界、空指標引用等。這些執行時錯誤會引起painc異常。
當panic異常發生時,程式會中斷執行,並立即執行在該goroutine(可以先理解成執行緒,在第8章會詳細介紹)中被延遲的函式(defer 機制)。隨後,程式崩潰並輸出日誌資訊。日誌資訊包括panic value和函式呼叫的堆疊跟蹤資訊。
雖然Go的panic機制類似於其他語言的異常,但panic的適用場景有一些不同。由於panic會引起程式的崩潰,因此panic一般用於嚴重錯誤,如程式內部的邏輯不一致。
通常來說,不應該對panic異常做任何處理,但有時,也許我們可以從異常中恢復,至少我們可以在程式崩潰前,做一些操作。舉個例子,當web伺服器遇到不可預料的嚴重問題時,在崩潰前應該將所有的連線關閉;如果不做任何處理,會使得客戶端一直處於等待狀態。
如果在deferred函式中呼叫了內建函式recover,並且定義該defer語句的函式發生了panic異常,recover會使程式從panic中恢復,並返回panic value。導致panic異常的函式不會繼續執行,但能正常返回。在未發生panic時呼叫recover,recover會返回nil。
例子中deferred函式幫助Parse從panic中恢復。在deferred函式內部,panic value被附加到錯誤資訊中;並用err變數接收錯誤資訊,返回給呼叫者。
func Parse(input string) (s *Syntax, err error) {
defer func() {
if p := recover(); p != nil {
err = fmt.Errorf("internal error: %v", p)
}
}()
// ...parser...
}
不加區分的恢復所有的panic異常,不是可取的做法。
只恢復應該被恢復的panic異常,此外,這些異常所佔的比例應該儘可能的低。為了標識某個panic是否應該被恢復,我們可以將panic value設定成特殊型別。在recover時對panic value進行檢查,如果發現panic value是特殊型別,就將這個panic作為errror處理,如果不是,則按照正常的panic進行處理
func soleTitle(doc *html.Node) (title string, err error) {
type bailout struct{}
defer func() {
switch p := recover(); p {
case nil: // no panic
case bailout{}: // "expected" panic
err = fmt.Errorf("multiple title elements")
default:
panic(p)
}
}()
forEachNode(doc, func(n *html.Node) {
if n.Type == html.ElementNode && n.Data == "title" &&
n.FirstChild != nil {
if title != "" {
panic(bailout{}) // multiple titleelements
}
title = n.FirstChild.Data
}
}, nil)
if title == "" {
return "", fmt.Errorf("no title element")
}
return title, nil
}
本作品採用《CC 協議》,轉載必須註明作者和本文連結