閱讀577 返回首頁    go 阿裏雲 go 技術社區[雲棲]


Go語言與數據庫開發:01-05

Go語言與數據庫開發:01-05

接下來,我們討論一下Go語言中的函數。

函數可以讓我們將一個語句序列打包為一個單元,然後可以從程序中其它地方多次調用。函
數的機製可以讓我們將一個大的工作分解為小的任務,這樣的小任務可以讓不同程序員在不
同時間、不同地方獨立完成。一個函數同時對用戶隱藏了其實現細節。由於這些因素,對於
任何編程語言來說,函數都是一個至關重要的部分。

函數聲明包括函數名、形式參數列表、返回值列表(可省略)以及函數體。
func name(parameter-list) (result-list) {
body
}

形式參數列表描述了函數的參數名以及參數類型。這些參數作為局部變量,其值由參數調用
者提供。返回值列表描述了函數返回值的變量名以及類型。如果函數返回一個無名變量或者
沒有返回值,返回值列表的括號是可以省略的。如果一個函數聲明不包括返回值列表,那麼
函數體執行完畢後,不會返回任何值。

如果一組形參或返回值有相同的類型,我們不必為每個形參都寫出參數類
型。下麵2個聲明是等價的:
func f(i, j, k int, s, t string) { /* ... / }
func f(i int, j int, k int, s string, t string) { /
... */ }

。在函數調用時,Go語
言沒有默認參數值,也沒有任何方法可以通過參數名指定形參,因此形參和返回值的變量名
對於函數調用者而言沒有意義。
在函數體中,函數的形參作為局部變量,被初始化為調用者提供的值。函數的形參和有名返
回值作為函數最外層的局部變量,被存儲在相同的詞法塊中。
實參通過值的方式傳遞,因此函數的形參是實參的拷貝。對形參進行修改不會影響實參。但
是,如果實參包括引用類型,如指針,slice(切片)、map、function、channel等類型,實參可
能會由於函數的簡介引用被修改。


多返回值

在Go中,一個函數可以返回多個值。我們已經在之前例子中看到,許多標準庫中的函數返回2
個值,一個是期望得到的返回值,另一個是函數出錯時的錯誤信息。

雖然良好的命名很重要,但你也不必為每一個返回值都取一個適當的名字。比如,按照慣
例,函數的最後一個bool類型的返回值表示函數是否運行成功,error類型的返回值代表函數
的錯誤信息,對於這些類似的慣例,我們不必思考合適的命名,它們都無需解釋。

如果一個函數將所有的返回值都顯示的變量名,那麼該函數的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 可以減少代碼的重複,但是使得
代碼難以被理解。不宜過度使用bare return。


錯誤

panic是來自被調函數的信號,表示發生了某個已知的bug。一個良好的程序
永遠不應該發生panic異常。

對於大部分函數而言,永遠無法確保能否成功運行。這是因為錯誤的原因超出了程序員的控
製。舉個例子,任何進行I/O操作的函數都會麵臨出現錯誤的可能,隻有沒有經驗的程序員才
會相信讀寫操作不會失敗,即時是簡單的讀寫。因此,當本該可信的操作出乎意料的失敗
後,我們必須弄清楚導致失敗的原因。

在Go的錯誤處理中,錯誤是軟件包API和應用程序用戶界麵的一個重要組成部分,程序運行
失敗僅被認為是幾個預期的結果之一。

對於那些將運行失敗看作是預期結果的函數,它們會返回一個額外的返回值,通常是最後一
個,來傳遞錯誤信息。如果導致失敗的原因隻有一個,額外的返回值可以是一個布爾值,通
常被命名為ok。比如,cache.Lookup失敗的唯一原因是key不存在,那麼代碼可以按照下麵的
方式組織:
value, ok := cache.Lookup(key)
if !ok {
// ...cache[key] does not exist…
}

通常,導致失敗的原因不止一種,尤其是對I/O操作而言,用戶需要了解更多的錯誤信息。因
此,額外的返回值不再是簡單的布爾類型,而是error類型。

內置的error是接口類型。

現在我們隻需要明白error類型可能是nil或者non-nil。nil意味著函數運行成功,non-nil表示失
敗。對於non-nil的error類型,我們可以通過調用error的Error函數或者輸出函數獲得字符串類型
的錯誤信息。
fmt.Println(err)
fmt.Printf("%v", err)

通常,當函數返回non-nil的error時,其他的返回值是未定義的(undefined),這些未定義的返回
值應該被忽略。然而,有少部分函數在發生錯誤時,仍然會返回一些有用的返回值。比如,
當讀取文件發生錯誤時,Read函數會返回可以讀取的字節數以及錯誤信息。對於這種情況,
正確的處理方式應該是先處理這些不完整的數據,再處理錯誤。因此對函數的返回值要有清
晰的說明,以便於其他人使用。

在Go中,函數運行失敗時會返回錯誤信息,這些錯誤信息被認為是一種預期的值而非異常
(exception),這使得Go有別於那些將函數運行失敗看作是異常的語言。
雖然Go有各種異常機製,但這些機製僅被使用在處理那些未被預料到的錯誤,即bug,而不是那些在健壯程序
中應該被避免的程序錯誤。


錯誤處理策略

當一次函數調用返回錯誤時,調用者有應該選擇何時的方式處理錯誤。根據情況的不同,有
很多處理方式,讓我們來看看常用的五種方式。

首先,也是最常用的方式是傳播錯誤。
這意味著函數中某個子程序的失敗,會變成該函數的失敗。

fmt.Errorf函數使用fmt.Sprintf格式化錯誤信息並返回。

當錯誤最終由main函數處理時,錯誤信息應提供清晰的從原因到後果
的因果鏈,就像美國宇航局事故調查時做的那樣:
genesis: crashed: no parachute: G-switch failed: bad relay orientation

由於錯誤信息經常是以鏈式組合在一起的,所以錯誤信息中應避免大寫和換行符。最終的錯
誤信息可能很長,我們可以通過類似grep的工具處理錯誤信息;

編寫錯誤信息時,我們要確保錯誤信息對問題細節的描述是詳盡的。

一般而言,被調函數f(x)會將調用信息和參數信息作為發生錯誤時的上下文放在錯誤信息中並
返回給調用者,調用者需要添加一些錯誤信息中不包含的信息,比如添加url到html.Parse返回
的錯誤中

處理錯誤的第二種策略。如果錯誤的發生是偶然性的,或由不可預知的問題導
致的。一個明智的選擇是重新嚐試失敗的操作。在重試時,我們需要限製重試的時間間隔或
重試的次數,防止無限製的重試。

如果錯誤發生後,程序無法繼續運行,我們就可以采用第三種策略:輸出錯誤信息並結束程
序。需要注意的是,這種策略隻應在main中執行。對庫函數而言,應僅向上傳播錯誤,除非
該錯誤意味著程序內部包含不一致性,即遇到了bug,才能在庫函數中結束程序。
// (In function main.)
if err := WaitForServer(url); err != nil {
fmt.Fprintf(os.Stderr, "Site is down: %v\n", err)
os.Exit(1)
}

調用log.Fatalf可以更簡潔的代碼達到與上文相同的效果。log中的所有函數,都默認會在錯誤
信息之前輸出時間信息。
if err := WaitForServer(url); err != nil {
log.Fatalf("Site is down: %v\n", err)
}

長時間運行的服務器常采用默認的時間格式,而交互式工具很少采用包含如此多信息的格
式。

我們可以設置log的前綴信息屏蔽時間信息,一般而言,前綴信息會被設置成命令名。
log.SetPrefix("wait: ")
log.SetFlags(0)

第四種策略:有時,我們隻需要輸出錯誤信息就足夠了,不需要中斷程序的運行。我們可以
通過log包提供函數
if err := Ping(); err != nil {
log.Printf("ping failed: %v; networking disabled",err)
}

或者標準錯誤流輸出錯誤信息。
if err := Ping(); err != nil {
fmt.Fprintf(os.Stderr, "ping failed: %v; networking disabled\n", err)
}

log包中的所有函數會為沒有換行符的字符串增加換行符。

第五種,也是最後一種策略:我們可以直接忽略掉錯誤。
dir, err := ioutil.TempDir("", "scratch")
if err != nil {
return fmt.Errorf("failed to create temp dir: %v",err)
}
// ...use temp dir…
os.RemoveAll(dir) // ignore errors; $TMPDIR is cleaned periodically

盡管os.RemoveAll會失敗,但上麵的例子並沒有做錯誤處理。這是因為操作係統會定期的清
理臨時目錄。正因如此,雖然程序沒有處理錯誤,但程序的邏輯不會因此受到影響。我們應
該在每次函數調用後,都養成考慮錯誤處理的習慣,當你決定忽略某個錯誤時,你應該在清
晰的記錄下你的意圖。

在Go中,錯誤處理有一套獨特的編碼風格。檢查某個子函數是否失敗後,我們通常將處理失
敗的邏輯代碼放在處理成功的代碼之前。如果某個錯誤會導致函數返回,那麼成功時的邏輯
代碼不應放在else語句塊中,而應直接放在函數體中。Go中大部分函數的代碼結構幾乎相
同,首先是一係列的初始檢查,防止錯誤發生,之後是函數的實際邏輯。


函數值

在Go中,函數被看作第一類值(first-class values):函數像其他值一樣,擁有類型,可以被
賦值給其他變量,傳遞給函數,從函數返回。對函數值(function value)的調用類似函數調用。

函數類型的零值是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。

函數值使得我們不僅僅可以通過數據來參數化函數,亦可通過行為。

strings.Map對字符串中的每個字符調用add1
函數,並將每個add1函數的返回值組成一個新的字符串返回給調用者。
func add1(r rune) rune { return r + 1 }
fmt.Println(strings.Map(add1, "HAL-9000")) // "IBM.:111"
fmt.Println(strings.Map(add1, "VMS")) // "WNT"
fmt.Println(strings.Map(add1, "Admix")) // "Benjy"


匿名函數

擁有函數名的函數隻能在包級語法塊中被聲明,通過函數字麵量(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
}
}
func main() {
f := squares()
fmt.Println(f()) // "1"
fmt.Println(f()) // "4"
fmt.Println(f()) // "9"
fmt.Println(f()) // "16"
}

函數squares返回另一個類型為 func() int 的函數。對squares的一次調用會生成一個局部變量
x並返回一個匿名函數。每次調用時匿名函數時,該函數都會先使x的值加1,再返回x的平
方。第二次調用squares時,會生成第二個x變量,並返回一個新的匿名函數。新匿名函數操
作的是第二個x變量。

squares的例子證明,函數值不僅僅是一串代碼,還記錄了狀態。在squares中定義的匿名內
部函數可以訪問和更新squares中的局部變量,這意味著匿名函數和squares中,存在變量引
用。這就是函數值屬於引用類型和函數值不可比較的原因。Go使用閉包(closures)技術實
現函數值,Go程序員也把函數值叫做閉包。

通過這個例子,我們看到變量的生命周期不由它的作用域決定:squares返回後,變量x仍然
隱式的存在於f中。


可變參數

參數數量可變的函數稱為為可變參數函數。

在聲明可變參數函數時,需要在參數列表的最後一個參數類型之前加上省略符號“...”,這表示
該函數會接收任意數量的該類型參數。

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"

在上麵的代碼中,調用者隱式的創建一個數組,並將原始參數複製到數組中,再把數組的一
個切片作為參數傳給被調函數。

下麵的代碼功能與上個例子中最後一條語句相同。
values := []int{1, 2, 3, 4}
fmt.Println(sum(values...)) // "10"

雖然在可變參數函數內部,...int 型參數的行為看起來很像切片類型,但實際上,可變參數函
數和以切片作為參數的函數是不同的。
func f(...int) {}
func g([]int) {}
fmt.Printf("%T\n", f) // "func(...int)"
fmt.Printf("%T\n", g) // "func([]int)"

可變參數函數經常被用於格式化字符串。


Deferred函數

隨著函數變得複雜,需要處理的錯誤也變多,維護清理邏輯變得越來越困
難。而Go語言獨有的defer機製可以讓事情變得簡單。

你隻需要在調用普通函數或方法前加上關鍵字defer,就完成了defer所需要的語法。當defer語
句被執行時,跟在defer後麵的函數會被延遲執行。直到包含該defer語句的函數執行完畢時,
defer後的函數才會被執行,不論包含defer語句的函數是通過return正常結束,還是由於panic
導致的異常結束。你可以在一個函數中執行多條defer語句,它們的執行順序與聲明順序相反。

defer語句經常被用於處理成對的操作,如打開、關閉、連接、斷開連接、加鎖、釋放鎖。通
過defer機製,不論函數邏輯多複雜,都能保證在任何執行路徑下,資源被釋放。釋放資源的
defer應該直接跟在請求資源的語句後。

調試複雜程序時,defer機製也常被用於記錄何時進入和退出函數。
。通過這種方式, 我們可以隻通過一條語句控製函數的入口和所有的出口,甚至可以記錄函數的
運行時間,如例子中的start。需要注意一點:不要忘記defer語句後的圓括號,否則本該在進入時
執行的操作會在退出時執行,而本該在退出時執行的,永遠不會被執行。

defer語句中的函數會在return語句更新返回值變量後再執行,又因為在函數中定義
的匿名函數可以訪問該函數包括返回值變量在內的所有變量,所以,對匿名函數采用defer機
製,可以使其觀察函數的返回值。

在循環體中的defer語句需要特別注意,因為隻有在函數執行完畢後,這些被延遲的函數才會執行。


Panic異常

Go的類型係統會在編譯時捕獲很多錯誤,但有些錯誤隻能在運行時檢查,如數組訪問越界、
空指針引用等。這些運行時錯誤會引起painc異常。

一般而言,當panic異常發生時,程序會中斷運行,並立即執行在該goroutine中被延遲的函數(defer 機製)。
隨後,程序崩潰並輸出日誌信息。日誌信息包括panic value和函數調用的堆棧跟蹤信息。panic value通常是某
種錯誤信息。對於每個goroutine,日誌信息中都會有與之相對的,發生panic時的函數調用堆棧跟蹤信息。通常,
我們不需要再次運行程序去定位問題,日誌信息已經提供了足夠的診斷依據。因此,在我們填寫問題報告時,一
般會將panic異常和日誌信息一並記錄。

不是所有的panic異常都來自運行時,直接調用內置的panic函數也會引發panic異常;panic函
數接受任何值作為參數。當某些不應該發生的場景發生時,我們就應該調用panic。比如,當
程序到達了某條邏輯上不可能到達的路徑。

斷言函數必須滿足的前置條件是明智的做法,但這很容易被濫用。除非你能提供更多的錯誤
信息,或者能更快速的發現錯誤,否則不需要使用斷言,編譯器在運行時會幫你檢查代碼。

func Reset(x *Buffer) {
if x == nil {
panic("x is nil") // unnecessary!
}
x.elements = nil
}

雖然Go的panic機製類似於其他語言的異常,但panic的適用場景有一些不同。由於panic會引
起程序的崩潰,因此panic一般用於嚴重錯誤,如程序內部的邏輯不一致。勤奮的程序員認為
任何崩潰都表明代碼中存在漏洞,所以對於大部分漏洞,我們應該使用Go提供的錯誤機製,
而不是panic,盡量避免程序的崩潰。在健壯的程序中,任何可以預料到的錯誤,如不正確的
輸入、錯誤的配置或是失敗的I/O操作都應該被優雅的處理,最好的處理方式,就是使用Go的
錯誤機製。

將panic機製類比其他語言異常機製的讀者可能會驚訝,runtime.Stack為何能輸出已經被釋放
函數的信息?在Go的panic機製中,延遲函數的調用在釋放堆棧信息之前。


Recover捕獲異常

通常來說,不應該對panic異常做任何處理,但有時,也許我們可以從異常中恢複,至少我們
可以在程序崩潰前,做一些操作。舉個例子,當web服務器遇到不可預料的嚴重問題時,在崩
潰前應該將所有的連接關閉;如果不做任何處理,會使得客戶端一直處於等待狀態。如果web
服務器還在開發階段,服務器甚至可以將異常信息反饋到客戶端,幫助調試。

如果在deferred函數中調用了內置函數recover,並且定義該defer語句的函數發生了panic異
常,recover會使程序從panic中恢複,並返回panic value。導致panic異常的函數不會繼續運
行,但能正常返回。在未發生panic時調用recover,recover會返回nil。

當某個異常出現時,我們不會選擇讓解析器崩潰,而是會將panic異常當作普通的解析錯誤,並
附加額外信息提醒用戶報告此錯誤。

func Parse(input string) (s *Syntax, err error) {
defer func() {
if p := recover(); p != nil {
err = fmt.Errorf("internal error: %v", p)
}
}()
// ...parser...
}

deferred函數幫助Parse從panic中恢複。在deferred函數內部,panic value被附加到錯誤信息
中;並用err變量接收錯誤信息,返回給調用者。我們也可以通過調用runtime.Stack往錯誤信
息中添加完整的堆棧調用信息。

安全的做法是有選擇性的recover。換句話說,隻恢複應該被恢複的panic異
常,此外,這些異常所占的比例應該盡可能的低。為了標識某個panic是否應該被恢複,我們
可以將panic value設置成特殊類型。在recover時對panic value進行檢查,如果發現panic
value是特殊類型,就將這個panic作為errror處理,如果不是,則按照正常的panic進行處理。

有些情況下,我們無法恢複。某些致命錯誤會導致Go在運行時終止程序,如內存不足。

最後更新:2017-09-14 19:32:48

  上一篇:go  我國傳感器細分領域躋身世界領先水平 整體水平落後
  下一篇:go  安卓O內核的加固