mirror of
https://github.com/gopl-zh/gopl-zh.github.com.git
synced 2025-08-04 15:01:46 +00:00
回到简体
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
## 5.1. 函數聲明
|
||||
## 5.1. 函数声明
|
||||
|
||||
函數聲明包括函數名、形式參數列表、返迴值列表(可省略)以及函數體。
|
||||
函数声明包括函数名、形式参数列表、返回值列表(可省略)以及函数体。
|
||||
|
||||
```Go
|
||||
func name(parameter-list) (result-list) {
|
||||
@@ -8,8 +8,8 @@ func name(parameter-list) (result-list) {
|
||||
}
|
||||
```
|
||||
|
||||
形式參數列表描述了函數的參數名以及參數類型。這些參數作爲局部變量,其值由參數調用者提供。返迴值列表描述了函數返迴值的變量名以及類型。如果函數返迴一個無名變量或者沒有返迴值,返迴值列表的括號是可以省略的。如果一個函數聲明不包括返迴值列表,那麽函數體執行完畢後,不會返迴任何值。
|
||||
在hypot函數中,
|
||||
形式参数列表描述了函数的参数名以及参数类型。这些参数作为局部变量,其值由参数调用者提供。返回值列表描述了函数返回值的变量名以及类型。如果函数返回一个无名变量或者没有返回值,返回值列表的括号是可以省略的。如果一个函数声明不包括返回值列表,那么函数体执行完毕后,不会返回任何值。
|
||||
在hypot函数中,
|
||||
|
||||
```Go
|
||||
func hypot(x, y float64) float64 {
|
||||
@@ -18,18 +18,18 @@ func hypot(x, y float64) float64 {
|
||||
fmt.Println(hypot(3,4)) // "5"
|
||||
```
|
||||
|
||||
x和y是形參名,3和4是調用時的傳入的實數,函數返迴了一個float64類型的值。
|
||||
返迴值也可以像形式參數一樣被命名。在這種情況下,每個返迴值被聲明成一個局部變量,併根據該返迴值的類型,將其初始化爲0。
|
||||
如果一個函數在聲明時,包含返迴值列表,該函數必須以 return語句結尾,除非函數明顯無法運行到結尾處。例如函數在結尾時調用了panic異常或函數中存在無限循環。
|
||||
x和y是形参名,3和4是调用时的传入的实数,函数返回了一个float64类型的值。
|
||||
返回值也可以像形式参数一样被命名。在这种情况下,每个返回值被声明成一个局部变量,并根据该返回值的类型,将其初始化为0。
|
||||
如果一个函数在声明时,包含返回值列表,该函数必须以 return语句结尾,除非函数明显无法运行到结尾处。例如函数在结尾时调用了panic异常或函数中存在无限循环。
|
||||
|
||||
正如hypot一樣,如果一組形參或返迴值有相同的類型,我們不必爲每個形參都寫出參數類型。下面2個聲明是等價的:
|
||||
正如hypot一样,如果一组形参或返回值有相同的类型,我们不必为每个形参都写出参数类型。下面2个声明是等价的:
|
||||
|
||||
```Go
|
||||
func f(i, j, k int, s, t string) { /* ... */ }
|
||||
func f(i int, j int, k int, s string, t string) { /* ... */ }
|
||||
```
|
||||
|
||||
下面,我們給出4種方法聲明擁有2個int型參數和1個int型返迴值的函數.blank identifier(譯者註:卽下文的_符號)可以強調某個參數未被使用。
|
||||
下面,我们给出4种方法声明拥有2个int型参数和1个int型返回值的函数.blank identifier(译者注:即下文的_符号)可以强调某个参数未被使用。
|
||||
|
||||
```Go
|
||||
func add(x int, y int) int {return x + y}
|
||||
@@ -43,15 +43,15 @@ fmt.Printf("%T\n", first) // "func(int, int) int"
|
||||
fmt.Printf("%T\n", zero) // "func(int, int) int"
|
||||
```
|
||||
|
||||
函數的類型被稱爲函數的標識符。如果兩個函數形式參數列表和返迴值列表中的變量類型一一對應,那麽這兩個函數被認爲有相同的類型和標識符。形參和返迴值的變量名不影響函數標識符也不影響它們是否可以以省略參數類型的形式表示。
|
||||
函数的类型被称为函数的标识符。如果两个函数形式参数列表和返回值列表中的变量类型一一对应,那么这两个函数被认为有相同的类型和标识符。形参和返回值的变量名不影响函数标识符也不影响它们是否可以以省略参数类型的形式表示。
|
||||
|
||||
每一次函數調用都必須按照聲明順序爲所有參數提供實參(參數值)。在函數調用時,Go語言沒有默認參數值,也沒有任何方法可以通過參數名指定形參,因此形參和返迴值的變量名對於函數調用者而言沒有意義。
|
||||
每一次函数调用都必须按照声明顺序为所有参数提供实参(参数值)。在函数调用时,Go语言没有默认参数值,也没有任何方法可以通过参数名指定形参,因此形参和返回值的变量名对于函数调用者而言没有意义。
|
||||
|
||||
在函數體中,函數的形參作爲局部變量,被初始化爲調用者提供的值。函數的形參和有名返迴值作爲函數最外層的局部變量,被存儲在相同的詞法塊中。
|
||||
在函数体中,函数的形参作为局部变量,被初始化为调用者提供的值。函数的形参和有名返回值作为函数最外层的局部变量,被存储在相同的词法块中。
|
||||
|
||||
實參通過值的方式傳遞,因此函數的形參是實參的拷貝。對形參進行脩改不會影響實參。但是,如果實參包括引用類型,如指針,slice(切片)、map、function、channel等類型,實參可能會由於函數的簡介引用被脩改。
|
||||
实参通过值的方式传递,因此函数的形参是实参的拷贝。对形参进行修改不会影响实参。但是,如果实参包括引用类型,如指针,slice(切片)、map、function、channel等类型,实参可能会由于函数的简介引用被修改。
|
||||
|
||||
你可能會偶爾遇到沒有函數體的函數聲明,這表示該函數不是以Go實現的。這樣的聲明定義了函數標識符。
|
||||
你可能会偶尔遇到没有函数体的函数声明,这表示该函数不是以Go实现的。这样的声明定义了函数标识符。
|
||||
|
||||
```Go
|
||||
package math
|
||||
|
@@ -1,10 +1,10 @@
|
||||
## 5.2. 遞歸
|
||||
## 5.2. 递归
|
||||
|
||||
函數可以是遞歸的,這意味着函數可以直接或間接的調用自身。對許多問題而言,遞歸是一種強有力的技術,例如處理遞歸的數據結構。在4.4節,我們通過遍歷二叉樹來實現簡單的插入排序,在本章節,我們再次使用它來處理HTML文件。
|
||||
函数可以是递归的,这意味着函数可以直接或间接的调用自身。对许多问题而言,递归是一种强有力的技术,例如处理递归的数据结构。在4.4节,我们通过遍历二叉树来实现简单的插入排序,在本章节,我们再次使用它来处理HTML文件。
|
||||
|
||||
下文的示例代碼使用了非標準包 golang.org/x/net/html ,解析HTML。golang.org/x/... 目録下存儲了一些由Go糰隊設計、維護,對網絡編程、国際化文件處理、移動平台、圖像處理、加密解密、開發者工具提供支持的擴展包。未將這些擴展包加入到標準庫原因有二,一是部分包仍在開發中,二是對大多數Go語言的開發者而言,擴展包提供的功能很少被使用。
|
||||
下文的示例代码使用了非标准包 golang.org/x/net/html ,解析HTML。golang.org/x/... 目录下存储了一些由Go团队设计、维护,对网络编程、国际化文件处理、移动平台、图像处理、加密解密、开发者工具提供支持的扩展包。未将这些扩展包加入到标准库原因有二,一是部分包仍在开发中,二是对大多数Go语言的开发者而言,扩展包提供的功能很少被使用。
|
||||
|
||||
例子中調用golang.org/x/net/html的部分api如下所示。html.Parse函數讀入一組bytes.解析後,返迴html.node類型的HTML頁面樹狀結構根節點。HTML擁有很多類型的結點如text(文本),commnets(註釋)類型,在下面的例子中,我們 隻關註< name key='value' >形式的結點。
|
||||
例子中调用golang.org/x/net/html的部分api如下所示。html.Parse函数读入一组bytes.解析后,返回html.node类型的HTML页面树状结构根节点。HTML拥有很多类型的结点如text(文本),commnets(注释)类型,在下面的例子中,我们 只关注< name key='value' >形式的结点。
|
||||
|
||||
<u><i>golang.org/x/net/html</i></u>
|
||||
```Go
|
||||
@@ -35,7 +35,7 @@ type Attribute struct {
|
||||
func Parse(r io.Reader) (*Node, error)
|
||||
```
|
||||
|
||||
main函數解析HTML標準輸入,通過遞歸函數visit獲得links(鏈接),併打印出這些links:
|
||||
main函数解析HTML标准输入,通过递归函数visit获得links(链接),并打印出这些links:
|
||||
|
||||
<u></i>gopl.io/ch5/findlinks1</i></u>
|
||||
```Go
|
||||
@@ -61,7 +61,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
visit函數遍歷HTML的節點樹,從每一個anchor元素的href屬性獲得link,將這些links存入字符串數組中,併返迴這個字符串數組。
|
||||
visit函数遍历HTML的节点树,从每一个anchor元素的href属性获得link,将这些links存入字符串数组中,并返回这个字符串数组。
|
||||
|
||||
```Go
|
||||
// visit appends to links each link found in n and returns the result.
|
||||
@@ -80,9 +80,9 @@ func visit(links []string, n *html.Node) []string {
|
||||
}
|
||||
```
|
||||
|
||||
爲了遍歷結點n的所有後代結點,每次遇到n的孩子結點時,visit遞歸的調用自身。這些孩子結點存放在FirstChild鏈表中。
|
||||
为了遍历结点n的所有后代结点,每次遇到n的孩子结点时,visit递归的调用自身。这些孩子结点存放在FirstChild链表中。
|
||||
|
||||
讓我們以Go的主頁(golang.org)作爲目標,運行findlinks。我們以fetch(1.5章)的輸出作爲findlinks的輸入。下面的輸出做了簡化處理。
|
||||
让我们以Go的主页(golang.org)作为目标,运行findlinks。我们以fetch(1.5章)的输出作为findlinks的输入。下面的输出做了简化处理。
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch1/fetch
|
||||
@@ -102,9 +102,9 @@ https://golang.org/dl/
|
||||
http://www.google.com/intl/en/policies/privacy/
|
||||
```
|
||||
|
||||
註意在頁面中出現的鏈接格式,在之後我們會介紹如何將這些鏈接,根據根路徑( https://golang.org )生成可以直接訪問的url。
|
||||
注意在页面中出现的链接格式,在之后我们会介绍如何将这些链接,根据根路径( https://golang.org )生成可以直接访问的url。
|
||||
|
||||
在函數outline中,我們通過遞歸的方式遍歷整個HTML結點樹,併輸出樹的結構。在outline內部,每遇到一個HTML元素標籤,就將其入棧,併輸出。
|
||||
在函数outline中,我们通过递归的方式遍历整个HTML结点树,并输出树的结构。在outline内部,每遇到一个HTML元素标签,就将其入栈,并输出。
|
||||
|
||||
<u><i>gopl.io/ch5/outline</i></u>
|
||||
```Go
|
||||
@@ -127,9 +127,9 @@ func outline(stack []string, n *html.Node) {
|
||||
}
|
||||
```
|
||||
|
||||
有一點值得註意:outline有入棧操作,但沒有相對應的出棧操作。當outline調用自身時,被調用者接收的是stack的拷貝。被調用者的入棧操作,脩改的是stack的拷貝,而不是調用者的stack,因對當函數返迴時,調用者的stack併未被脩改。
|
||||
有一点值得注意:outline有入栈操作,但没有相对应的出栈操作。当outline调用自身时,被调用者接收的是stack的拷贝。被调用者的入栈操作,修改的是stack的拷贝,而不是调用者的stack,因对当函数返回时,调用者的stack并未被修改。
|
||||
|
||||
下面是 https://golang.org 頁面的簡要結構:
|
||||
下面是 https://golang.org 页面的简要结构:
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch5/outline
|
||||
@@ -149,14 +149,14 @@ $ ./fetch https://golang.org | ./outline
|
||||
...
|
||||
```
|
||||
|
||||
正如你在上面實驗中所見,大部分HTML頁面隻需幾層遞歸就能被處理,但仍然有些頁面需要深層次的遞歸。
|
||||
正如你在上面实验中所见,大部分HTML页面只需几层递归就能被处理,但仍然有些页面需要深层次的递归。
|
||||
|
||||
大部分編程語言使用固定大小的函數調用棧,常見的大小從64KB到2MB不等。固定大小棧會限製遞歸的深度,當你用遞歸處理大量數據時,需要避免棧溢出;除此之外,還會導致安全性問題。與相反,Go語言使用可變棧,棧的大小按需增加(初始時很小)。這使得我們使用遞歸時不必考慮溢出和安全問題。
|
||||
大部分编程语言使用固定大小的函数调用栈,常见的大小从64KB到2MB不等。固定大小栈会限制递归的深度,当你用递归处理大量数据时,需要避免栈溢出;除此之外,还会导致安全性问题。与相反,Go语言使用可变栈,栈的大小按需增加(初始时很小)。这使得我们使用递归时不必考虑溢出和安全问题。
|
||||
|
||||
**練習 5.1:** 脩改findlinks代碼中遍歷n.FirstChild鏈表的部分,將循環調用visit,改成遞歸調用。
|
||||
**练习 5.1:** 修改findlinks代码中遍历n.FirstChild链表的部分,将循环调用visit,改成递归调用。
|
||||
|
||||
**練習 5.2:** 編寫函數,記録在HTML樹中出現的同名元素的次數。
|
||||
**练习 5.2:** 编写函数,记录在HTML树中出现的同名元素的次数。
|
||||
|
||||
**練習 5.3:** 編寫函數輸出所有text結點的內容。註意不要訪問`<script>`和`<style>`元素,因爲這些元素對瀏覽者是不可見的。
|
||||
**练习 5.3:** 编写函数输出所有text结点的内容。注意不要访问`<script>`和`<style>`元素,因为这些元素对浏览者是不可见的。
|
||||
|
||||
**練習 5.4:** 擴展vist函數,使其能夠處理其他類型的結點,如images、scripts和style sheets。
|
||||
**练习 5.4:** 扩展vist函数,使其能够处理其他类型的结点,如images、scripts和style sheets。
|
||||
|
@@ -1,8 +1,8 @@
|
||||
## 5.3. 多返迴值
|
||||
## 5.3. 多返回值
|
||||
|
||||
在Go中,一個函數可以返迴多個值。我們已經在之前例子中看到,許多標準庫中的函數返迴2個值,一個是期望得到的返迴值,另一個是函數出錯時的錯誤信息。下面的例子會展示如何編寫多返迴值的函數。
|
||||
在Go中,一个函数可以返回多个值。我们已经在之前例子中看到,许多标准库中的函数返回2个值,一个是期望得到的返回值,另一个是函数出错时的错误信息。下面的例子会展示如何编写多返回值的函数。
|
||||
|
||||
下面的程序是findlinks的改進版本。脩改後的findlinks可以自己發起HTTP請求,這樣我們就不必再運行fetch。因爲HTTP請求和解析操作可能會失敗,因此findlinks聲明了2個返迴值:鏈接列表和錯誤信息。一般而言,HTML的解析器可以處理HTML頁面的錯誤結點,構造出HTML頁面結構,所以解析HTML很少失敗。這意味着如果findlinks函數失敗了,很可能是由於I/O的錯誤導致的。
|
||||
下面的程序是findlinks的改进版本。修改后的findlinks可以自己发起HTTP请求,这样我们就不必再运行fetch。因为HTTP请求和解析操作可能会失败,因此findlinks声明了2个返回值:链接列表和错误信息。一般而言,HTML的解析器可以处理HTML页面的错误结点,构造出HTML页面结构,所以解析HTML很少失败。这意味着如果findlinks函数失败了,很可能是由于I/O的错误导致的。
|
||||
|
||||
<u><i>gopl.io/ch5/findlinks2</i></u>
|
||||
```Go
|
||||
@@ -39,23 +39,23 @@ func findLinks(url string) ([]string, error) {
|
||||
}
|
||||
```
|
||||
|
||||
在findlinks中,有4處return語句,每一處return都返迴了一組值。前三處return,將http和html包中的錯誤信息傳遞給findlinks的調用者。第一處return直接返迴錯誤信息,其他兩處通過fmt.Errorf(§7.8)輸出詳細的錯誤信息。如果findlinks成功結束,最後的return語句將一組解析獲得的連接返迴給用戶。
|
||||
在findlinks中,有4处return语句,每一处return都返回了一组值。前三处return,将http和html包中的错误信息传递给findlinks的调用者。第一处return直接返回错误信息,其他两处通过fmt.Errorf(§7.8)输出详细的错误信息。如果findlinks成功结束,最后的return语句将一组解析获得的连接返回给用户。
|
||||
|
||||
在finallinks中,我們必須確保resp.Body被關閉,釋放網絡資源。雖然Go的垃圾迴收機製會迴收不被使用的內存,但是這不包括操作繫統層面的資源,比如打開的文件、網絡連接。因此我們必須顯式的釋放這些資源。
|
||||
在finallinks中,我们必须确保resp.Body被关闭,释放网络资源。虽然Go的垃圾回收机制会回收不被使用的内存,但是这不包括操作系统层面的资源,比如打开的文件、网络连接。因此我们必须显式的释放这些资源。
|
||||
|
||||
調用多返迴值函數時,返迴給調用者的是一組值,調用者必須顯式的將這些值分配給變量:
|
||||
调用多返回值函数时,返回给调用者的是一组值,调用者必须显式的将这些值分配给变量:
|
||||
|
||||
```Go
|
||||
links, err := findLinks(url)
|
||||
```
|
||||
|
||||
如果某個值不被使用,可以將其分配給blank identifier:
|
||||
如果某个值不被使用,可以将其分配给blank identifier:
|
||||
|
||||
```Go
|
||||
links, _ := findLinks(url) // errors ignored
|
||||
```
|
||||
|
||||
一個函數內部可以將另一個有多返迴值的函數作爲返迴值,下面的例子展示了與findLinks有相同功能的函數,兩者的區别在於下面的例子先輸出參數:
|
||||
一个函数内部可以将另一个有多返回值的函数作为返回值,下面的例子展示了与findLinks有相同功能的函数,两者的区别在于下面的例子先输出参数:
|
||||
|
||||
```Go
|
||||
func findLinksLog(url string) ([]string, error) {
|
||||
@@ -64,7 +64,7 @@ func findLinksLog(url string) ([]string, error) {
|
||||
}
|
||||
```
|
||||
|
||||
當你調用接受多參數的函數時,可以將一個返迴多參數的函數作爲該函數的參數。雖然這很少出現在實際生産代碼中,但這個特性在debug時很方便,我們隻需要一條語句就可以輸出所有的返迴值。下面的代碼是等價的:
|
||||
当你调用接受多参数的函数时,可以将一个返回多参数的函数作为该函数的参数。虽然这很少出现在实际生产代码中,但这个特性在debug时很方便,我们只需要一条语句就可以输出所有的返回值。下面的代码是等价的:
|
||||
|
||||
```Go
|
||||
log.Println(findLinks(url))
|
||||
@@ -72,7 +72,7 @@ links, err := findLinks(url)
|
||||
log.Println(links, err)
|
||||
```
|
||||
|
||||
準確的變量名可以傳達函數返迴值的含義。尤其在返迴值的類型都相同時,就像下面這樣:
|
||||
准确的变量名可以传达函数返回值的含义。尤其在返回值的类型都相同时,就像下面这样:
|
||||
|
||||
```Go
|
||||
func Size(rect image.Rectangle) (width, height int)
|
||||
@@ -80,9 +80,9 @@ func Split(path string) (dir, file string)
|
||||
func HourMinSec(t time.Time) (hour, minute, second int)
|
||||
```
|
||||
|
||||
雖然良好的命名很重要,但你也不必爲每一個返迴值都取一個適當的名字。比如,按照慣例,函數的最後一個bool類型的返迴值表示函數是否運行成功,error類型的返迴值代表函數的錯誤信息,對於這些類似的慣例,我們不必思考合適的命名,它們都無需解釋。
|
||||
虽然良好的命名很重要,但你也不必为每一个返回值都取一个适当的名字。比如,按照惯例,函数的最后一个bool类型的返回值表示函数是否运行成功,error类型的返回值代表函数的错误信息,对于这些类似的惯例,我们不必思考合适的命名,它们都无需解释。
|
||||
|
||||
如果一個函數將所有的返迴值都顯示的變量名,那麽該函數的return語句可以省略操作數。這稱之爲bare return。
|
||||
如果一个函数将所有的返回值都显示的变量名,那么该函数的return语句可以省略操作数。这称之为bare return。
|
||||
|
||||
```Go
|
||||
// CountWordsAndImages does an HTTP GET request for the HTML
|
||||
@@ -104,14 +104,14 @@ func CountWordsAndImages(url string) (words, images int, err error) {
|
||||
func countWordsAndImages(n *html.Node) (words, images int) { /* ... */ }
|
||||
```
|
||||
|
||||
按照返迴值列表的次序,返迴所有的返迴值,在上面的例子中,每一個return語句等價於:
|
||||
按照返回值列表的次序,返回所有的返回值,在上面的例子中,每一个return语句等价于:
|
||||
|
||||
```Go
|
||||
return words, images, err
|
||||
```
|
||||
|
||||
當一個函數有多處return語句以及許多返迴值時,bare return 可以減少代碼的重複,但是使得代碼難以被理解。舉個例子,如果你沒有仔細的審査代碼,很難發現前2處return等價於 return 0,0,err(Go會將返迴值 words和images在函數體的開始處,根據它們的類型,將其初始化爲0),最後一處return等價於 return words,image,nil。基於以上原因,不宜過度使用bare return。
|
||||
当一个函数有多处return语句以及许多返回值时,bare return 可以减少代码的重复,但是使得代码难以被理解。举个例子,如果你没有仔细的审查代码,很难发现前2处return等价于 return 0,0,err(Go会将返回值 words和images在函数体的开始处,根据它们的类型,将其初始化为0),最后一处return等价于 return words,image,nil。基于以上原因,不宜过度使用bare return。
|
||||
|
||||
**練習 5.5:** 實現countWordsAndImages。(參考練習4.9如何分詞)
|
||||
**练习 5.5:** 实现countWordsAndImages。(参考练习4.9如何分词)
|
||||
|
||||
**練習 5.6:** 脩改gopl.io/ch3/surface (§3.2) 中的corner函數,將返迴值命名,併使用bare return。
|
||||
**练习 5.6:** 修改gopl.io/ch3/surface (§3.2) 中的corner函数,将返回值命名,并使用bare return。
|
||||
|
@@ -1,8 +1,8 @@
|
||||
### 5.4.1. 錯誤處理策略
|
||||
### 5.4.1. 错误处理策略
|
||||
|
||||
當一次函數調用返迴錯誤時,調用者有應該選擇何時的方式處理錯誤。根據情況的不同,有很多處理方式,讓我們來看看常用的五種方式。
|
||||
当一次函数调用返回错误时,调用者有应该选择何时的方式处理错误。根据情况的不同,有很多处理方式,让我们来看看常用的五种方式。
|
||||
|
||||
首先,也是最常用的方式是傳播錯誤。這意味着函數中某個子程序的失敗,會變成該函數的失敗。下面,我們以5.3節的findLinks函數作爲例子。如果findLinks對http.Get的調用失敗,findLinks會直接將這個HTTP錯誤返迴給調用者:
|
||||
首先,也是最常用的方式是传播错误。这意味着函数中某个子程序的失败,会变成该函数的失败。下面,我们以5.3节的findLinks函数作为例子。如果findLinks对http.Get的调用失败,findLinks会直接将这个HTTP错误返回给调用者:
|
||||
|
||||
```Go
|
||||
resp, err := http.Get(url)
|
||||
@@ -11,7 +11,7 @@ if err != nil{
|
||||
}
|
||||
```
|
||||
|
||||
當對html.Parse的調用失敗時,findLinks不會直接返迴html.Parse的錯誤,因爲缺少兩條重要信息:1、錯誤發生在解析器;2、url已經被解析。這些信息有助於錯誤的處理,findLinks會構造新的錯誤信息返迴給調用者:
|
||||
当对html.Parse的调用失败时,findLinks不会直接返回html.Parse的错误,因为缺少两条重要信息:1、错误发生在解析器;2、url已经被解析。这些信息有助于错误的处理,findLinks会构造新的错误信息返回给调用者:
|
||||
|
||||
```Go
|
||||
doc, err := html.Parse(resp.Body)
|
||||
@@ -21,21 +21,21 @@ if err != nil {
|
||||
}
|
||||
```
|
||||
|
||||
fmt.Errorf函數使用fmt.Sprintf格式化錯誤信息併返迴。我們使用該函數前綴添加額外的上下文信息到原始錯誤信息。當錯誤最終由main函數處理時,錯誤信息應提供清晰的從原因到後果的因果鏈,就像美国宇航局事故調査時做的那樣:
|
||||
fmt.Errorf函数使用fmt.Sprintf格式化错误信息并返回。我们使用该函数前缀添加额外的上下文信息到原始错误信息。当错误最终由main函数处理时,错误信息应提供清晰的从原因到后果的因果链,就像美国宇航局事故调查时做的那样:
|
||||
|
||||
```
|
||||
genesis: crashed: no parachute: G-switch failed: bad relay orientation
|
||||
```
|
||||
|
||||
由於錯誤信息經常是以鏈式組合在一起的,所以錯誤信息中應避免大寫和換行符。最終的錯誤信息可能很長,我們可以通過類似grep的工具處理錯誤信息(譯者註:grep是一種文本蒐索工具)。
|
||||
由于错误信息经常是以链式组合在一起的,所以错误信息中应避免大写和换行符。最终的错误信息可能很长,我们可以通过类似grep的工具处理错误信息(译者注:grep是一种文本搜索工具)。
|
||||
|
||||
編寫錯誤信息時,我們要確保錯誤信息對問題細節的描述是詳盡的。尤其是要註意錯誤信息表達的一致性,卽相同的函數或同包內的同一組函數返迴的錯誤在構成和處理方式上是相似的。
|
||||
编写错误信息时,我们要确保错误信息对问题细节的描述是详尽的。尤其是要注意错误信息表达的一致性,即相同的函数或同包内的同一组函数返回的错误在构成和处理方式上是相似的。
|
||||
|
||||
以OS包爲例,OS包確保文件操作(如os.Open、Read、Write、Close)返迴的每個錯誤的描述不僅僅包含錯誤的原因(如無權限,文件目録不存在)也包含文件名,這樣調用者在構造新的錯誤信息時無需再添加這些信息。
|
||||
以OS包为例,OS包确保文件操作(如os.Open、Read、Write、Close)返回的每个错误的描述不仅仅包含错误的原因(如无权限,文件目录不存在)也包含文件名,这样调用者在构造新的错误信息时无需再添加这些信息。
|
||||
|
||||
一般而言,被調函數f(x)會將調用信息和參數信息作爲發生錯誤時的上下文放在錯誤信息中併返迴給調用者,調用者需要添加一些錯誤信息中不包含的信息,比如添加url到html.Parse返迴的錯誤中。
|
||||
一般而言,被调函数f(x)会将调用信息和参数信息作为发生错误时的上下文放在错误信息中并返回给调用者,调用者需要添加一些错误信息中不包含的信息,比如添加url到html.Parse返回的错误中。
|
||||
|
||||
讓我們來看看處理錯誤的第二種策略。如果錯誤的發生是偶然性的,或由不可預知的問題導致的。一個明智的選擇是重新嚐試失敗的操作。在重試時,我們需要限製重試的時間間隔或重試的次數,防止無限製的重試。
|
||||
让我们来看看处理错误的第二种策略。如果错误的发生是偶然性的,或由不可预知的问题导致的。一个明智的选择是重新尝试失败的操作。在重试时,我们需要限制重试的时间间隔或重试的次数,防止无限制的重试。
|
||||
|
||||
<u><i>gopl.io/ch5/wait</i></u>
|
||||
```Go
|
||||
@@ -57,7 +57,7 @@ func WaitForServer(url string) error {
|
||||
}
|
||||
```
|
||||
|
||||
如果錯誤發生後,程序無法繼續運行,我們就可以采用第三種策略:輸出錯誤信息併結束程序。需要註意的是,這種策略隻應在main中執行。對庫函數而言,應僅向上傳播錯誤,除非該錯誤意味着程序內部包含不一致性,卽遇到了bug,才能在庫函數中結束程序。
|
||||
如果错误发生后,程序无法继续运行,我们就可以采用第三种策略:输出错误信息并结束程序。需要注意的是,这种策略只应在main中执行。对库函数而言,应仅向上传播错误,除非该错误意味着程序内部包含不一致性,即遇到了bug,才能在库函数中结束程序。
|
||||
|
||||
```Go
|
||||
// (In function main.)
|
||||
@@ -67,7 +67,7 @@ if err := WaitForServer(url); err != nil {
|
||||
}
|
||||
```
|
||||
|
||||
調用log.Fatalf可以更簡潔的代碼達到與上文相同的效果。log中的所有函數,都默認會在錯誤信息之前輸出時間信息。
|
||||
调用log.Fatalf可以更简洁的代码达到与上文相同的效果。log中的所有函数,都默认会在错误信息之前输出时间信息。
|
||||
|
||||
```Go
|
||||
if err := WaitForServer(url); err != nil {
|
||||
@@ -75,21 +75,21 @@ if err := WaitForServer(url); err != nil {
|
||||
}
|
||||
```
|
||||
|
||||
長時間運行的服務器常采用默認的時間格式,而交互式工具很少采用包含如此多信息的格式。
|
||||
长时间运行的服务器常采用默认的时间格式,而交互式工具很少采用包含如此多信息的格式。
|
||||
|
||||
```
|
||||
2006/01/02 15:04:05 Site is down: no such domain:
|
||||
bad.gopl.io
|
||||
```
|
||||
|
||||
我們可以設置log的前綴信息屏蔽時間信息,一般而言,前綴信息會被設置成命令名。
|
||||
我们可以设置log的前缀信息屏蔽时间信息,一般而言,前缀信息会被设置成命令名。
|
||||
|
||||
```Go
|
||||
log.SetPrefix("wait: ")
|
||||
log.SetFlags(0)
|
||||
```
|
||||
|
||||
第四種策略:有時,我們隻需要輸出錯誤信息就足夠了,不需要中斷程序的運行。我們可以通過log包提供函數
|
||||
第四种策略:有时,我们只需要输出错误信息就足够了,不需要中断程序的运行。我们可以通过log包提供函数
|
||||
|
||||
```Go
|
||||
if err := Ping(); err != nil {
|
||||
@@ -97,7 +97,7 @@ if err := Ping(); err != nil {
|
||||
}
|
||||
```
|
||||
|
||||
或者標準錯誤流輸出錯誤信息。
|
||||
或者标准错误流输出错误信息。
|
||||
|
||||
```Go
|
||||
if err := Ping(); err != nil {
|
||||
@@ -105,9 +105,9 @@ if err := Ping(); err != nil {
|
||||
}
|
||||
```
|
||||
|
||||
log包中的所有函數會爲沒有換行符的字符串增加換行符。
|
||||
log包中的所有函数会为没有换行符的字符串增加换行符。
|
||||
|
||||
第五種,也是最後一種策略:我們可以直接忽略掉錯誤。
|
||||
第五种,也是最后一种策略:我们可以直接忽略掉错误。
|
||||
|
||||
```Go
|
||||
dir, err := ioutil.TempDir("", "scratch")
|
||||
@@ -118,6 +118,6 @@ if err != nil {
|
||||
os.RemoveAll(dir) // ignore errors; $TMPDIR is cleaned periodically
|
||||
```
|
||||
|
||||
盡管os.RemoveAll會失敗,但上面的例子併沒有做錯誤處理。這是因爲操作繫統會定期的清理臨時目録。正因如此,雖然程序沒有處理錯誤,但程序的邏輯不會因此受到影響。我們應該在每次函數調用後,都養成考慮錯誤處理的習慣,當你決定忽略某個錯誤時,你應該在清晰的記録下你的意圖。
|
||||
尽管os.RemoveAll会失败,但上面的例子并没有做错误处理。这是因为操作系统会定期的清理临时目录。正因如此,虽然程序没有处理错误,但程序的逻辑不会因此受到影响。我们应该在每次函数调用后,都养成考虑错误处理的习惯,当你决定忽略某个错误时,你应该在清晰的记录下你的意图。
|
||||
|
||||
在Go中,錯誤處理有一套獨特的編碼風格。檢査某個子函數是否失敗後,我們通常將處理失敗的邏輯代碼放在處理成功的代碼之前。如果某個錯誤會導致函數返迴,那麽成功時的邏輯代碼不應放在else語句塊中,而應直接放在函數體中。Go中大部分函數的代碼結構幾乎相同,首先是一繫列的初始檢査,防止錯誤發生,之後是函數的實際邏輯。
|
||||
在Go中,错误处理有一套独特的编码风格。检查某个子函数是否失败后,我们通常将处理失败的逻辑代码放在处理成功的代码之前。如果某个错误会导致函数返回,那么成功时的逻辑代码不应放在else语句块中,而应直接放在函数体中。Go中大部分函数的代码结构几乎相同,首先是一系列的初始检查,防止错误发生,之后是函数的实际逻辑。
|
||||
|
@@ -1,6 +1,6 @@
|
||||
### 5.4.2. 文件結尾錯誤(EOF)
|
||||
### 5.4.2. 文件结尾错误(EOF)
|
||||
|
||||
函數經常會返迴多種錯誤,這對終端用戶來説可能會很有趣,但對程序而言,這使得情況變得複雜。很多時候,程序必須根據錯誤類型,作出不同的響應。讓我們考慮這樣一個例子:從文件中讀取n個字節。如果n等於文件的長度,讀取過程的任何錯誤都表示失敗。如果n小於文件的長度,調用者會重複的讀取固定大小的數據直到文件結束。這會導致調用者必須分别處理由文件結束引起的各種錯誤。基於這樣的原因,io包保證任何由文件結束引起的讀取失敗都返迴同一個錯誤——io.EOF,該錯誤在io包中定義:
|
||||
函数经常会返回多种错误,这对终端用户来说可能会很有趣,但对程序而言,这使得情况变得复杂。很多时候,程序必须根据错误类型,作出不同的响应。让我们考虑这样一个例子:从文件中读取n个字节。如果n等于文件的长度,读取过程的任何错误都表示失败。如果n小于文件的长度,调用者会重复的读取固定大小的数据直到文件结束。这会导致调用者必须分别处理由文件结束引起的各种错误。基于这样的原因,io包保证任何由文件结束引起的读取失败都返回同一个错误——io.EOF,该错误在io包中定义:
|
||||
|
||||
```Go
|
||||
package io
|
||||
@@ -11,7 +11,7 @@ import "errors"
|
||||
var EOF = errors.New("EOF")
|
||||
```
|
||||
|
||||
調用者隻需通過簡單的比較,就可以檢測出這個錯誤。下面的例子展示了如何從標準輸入中讀取字符,以及判斷文件結束。(4.3的chartcount程序展示了更加複雜的代碼)
|
||||
调用者只需通过简单的比较,就可以检测出这个错误。下面的例子展示了如何从标准输入中读取字符,以及判断文件结束。(4.3的chartcount程序展示了更加复杂的代码)
|
||||
|
||||
```Go
|
||||
in := bufio.NewReader(os.Stdin)
|
||||
@@ -27,4 +27,4 @@ for {
|
||||
}
|
||||
```
|
||||
|
||||
因爲文件結束這種錯誤不需要更多的描述,所以io.EOF有固定的錯誤信息——“EOF”。對於其他錯誤,我們可能需要在錯誤信息中描述錯誤的類型和數量,這使得我們不能像io.EOF一樣采用固定的錯誤信息。在7.11節中,我們會提出更繫統的方法區分某些固定的錯誤值。
|
||||
因为文件结束这种错误不需要更多的描述,所以io.EOF有固定的错误信息——“EOF”。对于其他错误,我们可能需要在错误信息中描述错误的类型和数量,这使得我们不能像io.EOF一样采用固定的错误信息。在7.11节中,我们会提出更系统的方法区分某些固定的错误值。
|
||||
|
@@ -1,15 +1,15 @@
|
||||
## 5.4. 錯誤
|
||||
## 5.4. 错误
|
||||
|
||||
|
||||
在Go中有一部分函數總是能成功的運行。比如strings.Contains和strconv.FormatBool函數,對各種可能的輸入都做了良好的處理,使得運行時幾乎不會失敗,除非遇到災難性的、不可預料的情況,比如運行時的內存溢出。導致這種錯誤的原因很複雜,難以處理,從錯誤中恢複的可能性也很低。
|
||||
在Go中有一部分函数总是能成功的运行。比如strings.Contains和strconv.FormatBool函数,对各种可能的输入都做了良好的处理,使得运行时几乎不会失败,除非遇到灾难性的、不可预料的情况,比如运行时的内存溢出。导致这种错误的原因很复杂,难以处理,从错误中恢复的可能性也很低。
|
||||
|
||||
還有一部分函數隻要輸入的參數滿足一定條件,也能保證運行成功。比如time.Date函數,該函數將年月日等參數構造成time.Time對象,除非最後一個參數(時區)是nil。這種情況下會引發panic異常。panic是來自被調函數的信號,表示發生了某個已知的bug。一個良好的程序永遠不應該發生panic異常。
|
||||
还有一部分函数只要输入的参数满足一定条件,也能保证运行成功。比如time.Date函数,该函数将年月日等参数构造成time.Time对象,除非最后一个参数(时区)是nil。这种情况下会引发panic异常。panic是来自被调函数的信号,表示发生了某个已知的bug。一个良好的程序永远不应该发生panic异常。
|
||||
|
||||
對於大部分函數而言,永遠無法確保能否成功運行。這是因爲錯誤的原因超出了程序員的控製。舉個例子,任何進行I/O操作的函數都會面臨出現錯誤的可能,隻有沒有經驗的程序員才會相信讀寫操作不會失敗,卽時是簡單的讀寫。因此,當本該可信的操作出乎意料的失敗後,我們必須弄清楚導致失敗的原因。
|
||||
对于大部分函数而言,永远无法确保能否成功运行。这是因为错误的原因超出了程序员的控制。举个例子,任何进行I/O操作的函数都会面临出现错误的可能,只有没有经验的程序员才会相信读写操作不会失败,即时是简单的读写。因此,当本该可信的操作出乎意料的失败后,我们必须弄清楚导致失败的原因。
|
||||
|
||||
在Go的錯誤處理中,錯誤是軟件包API和應用程序用戶界面的一個重要組成部分,程序運行失敗僅被認爲是幾個預期的結果之一。
|
||||
在Go的错误处理中,错误是软件包API和应用程序用户界面的一个重要组成部分,程序运行失败仅被认为是几个预期的结果之一。
|
||||
|
||||
對於那些將運行失敗看作是預期結果的函數,它們會返迴一個額外的返迴值,通常是最後一個,來傳遞錯誤信息。如果導致失敗的原因隻有一個,額外的返迴值可以是一個布爾值,通常被命名爲ok。比如,cache.Lookup失敗的唯一原因是key不存在,那麽代碼可以按照下面的方式組織:
|
||||
对于那些将运行失败看作是预期结果的函数,它们会返回一个额外的返回值,通常是最后一个,来传递错误信息。如果导致失败的原因只有一个,额外的返回值可以是一个布尔值,通常被命名为ok。比如,cache.Lookup失败的唯一原因是key不存在,那么代码可以按照下面的方式组织:
|
||||
|
||||
```Go
|
||||
value, ok := cache.Lookup(key)
|
||||
@@ -18,22 +18,22 @@ if !ok {
|
||||
}
|
||||
```
|
||||
|
||||
通常,導致失敗的原因不止一種,尤其是對I/O操作而言,用戶需要了解更多的錯誤信息。因此,額外的返迴值不再是簡單的布爾類型,而是error類型。
|
||||
通常,导致失败的原因不止一种,尤其是对I/O操作而言,用户需要了解更多的错误信息。因此,额外的返回值不再是简单的布尔类型,而是error类型。
|
||||
|
||||
內置的error是接口類型。我們將在第七章了解接口類型的含義,以及它對錯誤處理的影響。現在我們隻需要明白error類型可能是nil或者non-nil。nil意味着函數運行成功,non-nil表示失敗。對於non-nil的error類型,我們可以通過調用error的Error函數或者輸出函數獲得字符串類型的錯誤信息。
|
||||
内置的error是接口类型。我们将在第七章了解接口类型的含义,以及它对错误处理的影响。现在我们只需要明白error类型可能是nil或者non-nil。nil意味着函数运行成功,non-nil表示失败。对于non-nil的error类型,我们可以通过调用error的Error函数或者输出函数获得字符串类型的错误信息。
|
||||
|
||||
```Go
|
||||
fmt.Println(err)
|
||||
fmt.Printf("%v", err)
|
||||
```
|
||||
|
||||
通常,當函數返迴non-nil的error時,其他的返迴值是未定義的(undefined),這些未定義的返迴值應該被忽略。然而,有少部分函數在發生錯誤時,仍然會返迴一些有用的返迴值。比如,當讀取文件發生錯誤時,Read函數會返迴可以讀取的字節數以及錯誤信息。對於這種情況,正確的處理方式應該是先處理這些不完整的數據,再處理錯誤。因此對函數的返迴值要有清晰的説明,以便於其他人使用。
|
||||
通常,当函数返回non-nil的error时,其他的返回值是未定义的(undefined),这些未定义的返回值应该被忽略。然而,有少部分函数在发生错误时,仍然会返回一些有用的返回值。比如,当读取文件发生错误时,Read函数会返回可以读取的字节数以及错误信息。对于这种情况,正确的处理方式应该是先处理这些不完整的数据,再处理错误。因此对函数的返回值要有清晰的说明,以便于其他人使用。
|
||||
|
||||
在Go中,函數運行失敗時會返迴錯誤信息,這些錯誤信息被認爲是一種預期的值而非異常(exception),這使得Go有别於那些將函數運行失敗看作是異常的語言。雖然Go有各種異常機製,但這些機製僅被使用在處理那些未被預料到的錯誤,卽bug,而不是那些在健壯程序中應該被避免的程序錯誤。對於Go的異常機製我們將在5.9介紹。
|
||||
在Go中,函数运行失败时会返回错误信息,这些错误信息被认为是一种预期的值而非异常(exception),这使得Go有别于那些将函数运行失败看作是异常的语言。虽然Go有各种异常机制,但这些机制仅被使用在处理那些未被预料到的错误,即bug,而不是那些在健壮程序中应该被避免的程序错误。对于Go的异常机制我们将在5.9介绍。
|
||||
|
||||
Go這樣設計的原因是由於對於某個應該在控製流程中處理的錯誤而言,將這個錯誤以異常的形式拋出會混亂對錯誤的描述,這通常會導致一些糟糕的後果。當某個程序錯誤被當作異常處理後,這個錯誤會將堆棧根據信息返迴給終端用戶,這些信息複雜且無用,無法幫助定位錯誤。
|
||||
Go这样设计的原因是由于对于某个应该在控制流程中处理的错误而言,将这个错误以异常的形式抛出会混乱对错误的描述,这通常会导致一些糟糕的后果。当某个程序错误被当作异常处理后,这个错误会将堆栈根据信息返回给终端用户,这些信息复杂且无用,无法帮助定位错误。
|
||||
|
||||
正因此,Go使用控製流機製(如if和return)處理異常,這使得編碼人員能更多的關註錯誤處理。
|
||||
正因此,Go使用控制流机制(如if和return)处理异常,这使得编码人员能更多的关注错误处理。
|
||||
|
||||
{% include "./ch5-04-1.md" %}
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
## 5.5. 函數值
|
||||
## 5.5. 函数值
|
||||
|
||||
在Go中,函數被看作第一類值(first-class values):函數像其他值一樣,擁有類型,可以被賦值給其他變量,傳遞給函數,從函數返迴。對函數值(function value)的調用類似函數調用。例子如下:
|
||||
在Go中,函数被看作第一类值(first-class values):函数像其他值一样,拥有类型,可以被赋值给其他变量,传递给函数,从函数返回。对函数值(function value)的调用类似函数调用。例子如下:
|
||||
|
||||
```Go
|
||||
func square(n int) int { return n * n }
|
||||
@@ -17,14 +17,14 @@
|
||||
f = product // compile error: can't assign func(int, int) int to func(int) int
|
||||
```
|
||||
|
||||
函數類型的零值是nil。調用值爲nil的函數值會引起panic錯誤:
|
||||
函数类型的零值是nil。调用值为nil的函数值会引起panic错误:
|
||||
|
||||
```Go
|
||||
var f func(int) int
|
||||
f(3) // 此處f的值爲nil, 會引起panic錯誤
|
||||
f(3) // 此处f的值为nil, 会引起panic错误
|
||||
```
|
||||
|
||||
函數值可以與nil比較:
|
||||
函数值可以与nil比较:
|
||||
|
||||
```Go
|
||||
var f func(int) int
|
||||
@@ -33,9 +33,9 @@
|
||||
}
|
||||
```
|
||||
|
||||
但是函數值之間是不可比較的,也不能用函數值作爲map的key。
|
||||
但是函数值之间是不可比较的,也不能用函数值作为map的key。
|
||||
|
||||
函數值使得我們不僅僅可以通過數據來參數化函數,亦可通過行爲。標準庫中包含許多這樣的例子。下面的代碼展示了如何使用這個技巧。strings.Map對字符串中的每個字符調用add1函數,併將每個add1函數的返迴值組成一個新的字符串返迴給調用者。
|
||||
函数值使得我们不仅仅可以通过数据来参数化函数,亦可通过行为。标准库中包含许多这样的例子。下面的代码展示了如何使用这个技巧。strings.Map对字符串中的每个字符调用add1函数,并将每个add1函数的返回值组成一个新的字符串返回给调用者。
|
||||
|
||||
```Go
|
||||
func add1(r rune) rune { return r + 1 }
|
||||
@@ -45,14 +45,14 @@
|
||||
fmt.Println(strings.Map(add1, "Admix")) // "Benjy"
|
||||
```
|
||||
|
||||
5.2節的findLinks函數使用了輔助函數visit,遍歷和操作了HTML頁面的所有結點。使用函數值,我們可以將遍歷結點的邏輯和操作結點的邏輯分離,使得我們可以複用遍歷的邏輯,從而對結點進行不同的操作。
|
||||
5.2节的findLinks函数使用了辅助函数visit,遍历和操作了HTML页面的所有结点。使用函数值,我们可以将遍历结点的逻辑和操作结点的逻辑分离,使得我们可以复用遍历的逻辑,从而对结点进行不同的操作。
|
||||
|
||||
<u><i>gopl.io/ch5/outline2</i></u>
|
||||
```Go
|
||||
// forEachNode針對每個結點x,都會調用pre(x)和post(x)。
|
||||
// pre和post都是可選的。
|
||||
// 遍歷孩子結點之前,pre被調用
|
||||
// 遍歷孩子結點之後,post被調用
|
||||
// forEachNode针对每个结点x,都会调用pre(x)和post(x)。
|
||||
// pre和post都是可选的。
|
||||
// 遍历孩子结点之前,pre被调用
|
||||
// 遍历孩子结点之后,post被调用
|
||||
func forEachNode(n *html.Node, pre, post func(n *html.Node)) {
|
||||
if pre != nil {
|
||||
pre(n)
|
||||
@@ -66,7 +66,7 @@ func forEachNode(n *html.Node, pre, post func(n *html.Node)) {
|
||||
}
|
||||
```
|
||||
|
||||
該函數接收2個函數作爲參數,分别在結點的孩子被訪問前和訪問後調用。這樣的設計給調用者更大的靈活性。舉個例子,現在我們有startElemen和endElement兩個函數用於輸出HTML元素的開始標籤和結束標籤`<b>...</b>`:
|
||||
该函数接收2个函数作为参数,分别在结点的孩子被访问前和访问后调用。这样的设计给调用者更大的灵活性。举个例子,现在我们有startElemen和endElement两个函数用于输出HTML元素的开始标签和结束标签`<b>...</b>`:
|
||||
|
||||
```Go
|
||||
var depth int
|
||||
@@ -84,15 +84,15 @@ func endElement(n *html.Node) {
|
||||
}
|
||||
```
|
||||
|
||||
上面的代碼利用fmt.Printf的一個小技巧控製輸出的縮進。`%*s`中的`*`會在字符串之前填充一些空格。在例子中,每次輸出會先填充`depth*2`數量的空格,再輸出"",最後再輸出HTML標籤。
|
||||
上面的代码利用fmt.Printf的一个小技巧控制输出的缩进。`%*s`中的`*`会在字符串之前填充一些空格。在例子中,每次输出会先填充`depth*2`数量的空格,再输出"",最后再输出HTML标签。
|
||||
|
||||
如果我們像下面這樣調用forEachNode:
|
||||
如果我们像下面这样调用forEachNode:
|
||||
|
||||
```Go
|
||||
forEachNode(doc, startElement, endElement)
|
||||
```
|
||||
|
||||
與之前的outline程序相比,我們得到了更加詳細的頁面結構:
|
||||
与之前的outline程序相比,我们得到了更加详细的页面结构:
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch5/outline2
|
||||
@@ -117,15 +117,15 @@ $ ./outline2 http://gopl.io
|
||||
...
|
||||
```
|
||||
|
||||
**練習 5.7:** 完善startElement和endElement函數,使其成爲通用的HTML輸出器。要求:輸出註釋結點,文本結點以及每個元素的屬性(< a href='...'>)。使用簡略格式輸出沒有孩子結點的元素(卽用`<img/>`代替`<img></img>`)。編寫測試,驗證程序輸出的格式正確。(詳見11章)
|
||||
**练习 5.7:** 完善startElement和endElement函数,使其成为通用的HTML输出器。要求:输出注释结点,文本结点以及每个元素的属性(< a href='...'>)。使用简略格式输出没有孩子结点的元素(即用`<img/>`代替`<img></img>`)。编写测试,验证程序输出的格式正确。(详见11章)
|
||||
|
||||
**練習 5.8:** 脩改pre和post函數,使其返迴布爾類型的返迴值。返迴false時,中止forEachNoded的遍歷。使用脩改後的代碼編寫ElementByID函數,根據用戶輸入的id査找第一個擁有該id元素的HTML元素,査找成功後,停止遍歷。
|
||||
**练习 5.8:** 修改pre和post函数,使其返回布尔类型的返回值。返回false时,中止forEachNoded的遍历。使用修改后的代码编写ElementByID函数,根据用户输入的id查找第一个拥有该id元素的HTML元素,查找成功后,停止遍历。
|
||||
|
||||
```Go
|
||||
func ElementByID(doc *html.Node, id string) *html.Node
|
||||
```
|
||||
|
||||
**練習 5.9:** 編寫函數expand,將s中的"foo"替換爲f("foo")的返迴值。
|
||||
**练习 5.9:** 编写函数expand,将s中的"foo"替换为f("foo")的返回值。
|
||||
|
||||
```Go
|
||||
func expand(s string, f func(string) string) string
|
||||
|
@@ -1,8 +1,8 @@
|
||||
### 5.6.1. 警告:捕獲迭代變量
|
||||
### 5.6.1. 警告:捕获迭代变量
|
||||
|
||||
本節,將介紹Go詞法作用域的一個陷阱。請務必仔細的閲讀,弄清楚發生問題的原因。卽使是經驗豐富的程序員也會在這個問題上犯錯誤。
|
||||
本节,将介绍Go词法作用域的一个陷阱。请务必仔细的阅读,弄清楚发生问题的原因。即使是经验丰富的程序员也会在这个问题上犯错误。
|
||||
|
||||
考慮這個樣一個問題:你被要求首先創建一些目録,再將目録刪除。在下面的例子中我們用函數值來完成刪除操作。下面的示例代碼需要引入os包。爲了使代碼簡單,我們忽略了所有的異常處理。
|
||||
考虑这个样一个问题:你被要求首先创建一些目录,再将目录删除。在下面的例子中我们用函数值来完成删除操作。下面的示例代码需要引入os包。为了使代码简单,我们忽略了所有的异常处理。
|
||||
|
||||
```Go
|
||||
var rmdirs []func()
|
||||
@@ -19,7 +19,7 @@ for _, rmdir := range rmdirs {
|
||||
}
|
||||
```
|
||||
|
||||
你可能會感到睏惑,爲什麽要在循環體中用循環變量d賦值一個新的局部變量,而不是像下面的代碼一樣直接使用循環變量dir。需要註意,下面的代碼是錯誤的。
|
||||
你可能会感到困惑,为什么要在循环体中用循环变量d赋值一个新的局部变量,而不是像下面的代码一样直接使用循环变量dir。需要注意,下面的代码是错误的。
|
||||
|
||||
```go
|
||||
var rmdirs []func()
|
||||
@@ -31,9 +31,9 @@ for _, dir := range tempDirs() {
|
||||
}
|
||||
```
|
||||
|
||||
問題的原因在於循環變量的作用域。在上面的程序中,for循環語句引入了新的詞法塊,循環變量dir在這個詞法塊中被聲明。在該循環中生成的所有函數值都共享相同的循環變量。需要註意,函數值中記録的是循環變量的內存地址,而不是循環變量某一時刻的值。以dir爲例,後續的迭代會不斷更新dir的值,當刪除操作執行時,for循環已完成,dir中存儲的值等於最後一次迭代的值。這意味着,每次對os.RemoveAll的調用刪除的都是相同的目録。
|
||||
问题的原因在于循环变量的作用域。在上面的程序中,for循环语句引入了新的词法块,循环变量dir在这个词法块中被声明。在该循环中生成的所有函数值都共享相同的循环变量。需要注意,函数值中记录的是循环变量的内存地址,而不是循环变量某一时刻的值。以dir为例,后续的迭代会不断更新dir的值,当删除操作执行时,for循环已完成,dir中存储的值等于最后一次迭代的值。这意味着,每次对os.RemoveAll的调用删除的都是相同的目录。
|
||||
|
||||
通常,爲了解決這個問題,我們會引入一個與循環變量同名的局部變量,作爲循環變量的副本。比如下面的變量dir,雖然這看起來很奇怪,但卻很有用。
|
||||
通常,为了解决这个问题,我们会引入一个与循环变量同名的局部变量,作为循环变量的副本。比如下面的变量dir,虽然这看起来很奇怪,但却很有用。
|
||||
|
||||
```Go
|
||||
for _, dir := range tempDirs() {
|
||||
@@ -42,7 +42,7 @@ for _, dir := range tempDirs() {
|
||||
}
|
||||
```
|
||||
|
||||
這個問題不僅存在基於range的循環,在下面的例子中,對循環變量i的使用也存在同樣的問題:
|
||||
这个问题不仅存在基于range的循环,在下面的例子中,对循环变量i的使用也存在同样的问题:
|
||||
|
||||
```Go
|
||||
var rmdirs []func()
|
||||
@@ -55,4 +55,4 @@ for i := 0; i < len(dirs); i++ {
|
||||
}
|
||||
```
|
||||
|
||||
如果你使用go語句(第八章)或者defer語句(5.8節)會經常遇到此類問題。這不是go或defer本身導致的,而是因爲它們都會等待循環結束後,再執行函數值。
|
||||
如果你使用go语句(第八章)或者defer语句(5.8节)会经常遇到此类问题。这不是go或defer本身导致的,而是因为它们都会等待循环结束后,再执行函数值。
|
||||
|
@@ -1,19 +1,19 @@
|
||||
## 5.6. 匿名函數
|
||||
## 5.6. 匿名函数
|
||||
|
||||
擁有函數名的函數隻能在包級語法塊中被聲明,通過函數字面量(function literal),我們可繞過這一限製,在任何表達式中表示一個函數值。函數字面量的語法和函數聲明相似,區别在於func關鍵字後沒有函數名。函數值字面量是一種表達式,它的值被成爲匿名函數(anonymous function)。
|
||||
拥有函数名的函数只能在包级语法块中被声明,通过函数字面量(function literal),我们可绕过这一限制,在任何表达式中表示一个函数值。函数字面量的语法和函数声明相似,区别在于func关键字后没有函数名。函数值字面量是一种表达式,它的值被成为匿名函数(anonymous function)。
|
||||
|
||||
函數字面量允許我們在使用時函數時,再定義它。通過這種技巧,我們可以改寫之前對strings.Map的調用:
|
||||
函数字面量允许我们在使用时函数时,再定义它。通过这种技巧,我们可以改写之前对strings.Map的调用:
|
||||
|
||||
```Go
|
||||
strings.Map(func(r rune) rune { return r + 1 }, "HAL-9000")
|
||||
```
|
||||
|
||||
更爲重要的是,通過這種方式定義的函數可以訪問完整的詞法環境(lexical environment),這意味着在函數中定義的內部函數可以引用該函數的變量,如下例所示:
|
||||
更为重要的是,通过这种方式定义的函数可以访问完整的词法环境(lexical environment),这意味着在函数中定义的内部函数可以引用该函数的变量,如下例所示:
|
||||
|
||||
<u><i>gopl.io/ch5/squares</i></u>
|
||||
```Go
|
||||
// squares返迴一個匿名函數。
|
||||
// 該匿名函數每次被調用時都會返迴下一個數的平方。
|
||||
// squares返回一个匿名函数。
|
||||
// 该匿名函数每次被调用时都会返回下一个数的平方。
|
||||
func squares() func() int {
|
||||
var x int
|
||||
return func() int {
|
||||
@@ -30,17 +30,17 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
函數squares返迴另一個類型爲 func() int 的函數。對squares的一次調用會生成一個局部變量x併返迴一個匿名函數。每次調用時匿名函數時,該函數都會先使x的值加1,再返迴x的平方。第二次調用squares時,會生成第二個x變量,併返迴一個新的匿名函數。新匿名函數操作的是第二個x變量。
|
||||
函数squares返回另一个类型为 func() int 的函数。对squares的一次调用会生成一个局部变量x并返回一个匿名函数。每次调用时匿名函数时,该函数都会先使x的值加1,再返回x的平方。第二次调用squares时,会生成第二个x变量,并返回一个新的匿名函数。新匿名函数操作的是第二个x变量。
|
||||
|
||||
squares的例子證明,函數值不僅僅是一串代碼,還記録了狀態。在squares中定義的匿名內部函數可以訪問和更新squares中的局部變量,這意味着匿名函數和squares中,存在變量引用。這就是函數值屬於引用類型和函數值不可比較的原因。Go使用閉包(closures)技術實現函數值,Go程序員也把函數值叫做閉包。
|
||||
squares的例子证明,函数值不仅仅是一串代码,还记录了状态。在squares中定义的匿名内部函数可以访问和更新squares中的局部变量,这意味着匿名函数和squares中,存在变量引用。这就是函数值属于引用类型和函数值不可比较的原因。Go使用闭包(closures)技术实现函数值,Go程序员也把函数值叫做闭包。
|
||||
|
||||
通過這個例子,我們看到變量的生命週期不由它的作用域決定:squares返迴後,變量x仍然隱式的存在於f中。
|
||||
通过这个例子,我们看到变量的生命周期不由它的作用域决定:squares返回后,变量x仍然隐式的存在于f中。
|
||||
|
||||
接下來,我們討論一個有點學術性的例子,考慮這樣一個問題:給定一些計算機課程,每個課程都有前置課程,隻有完成了前置課程才可以開始當前課程的學習;我們的目標是選擇出一組課程,這組課程必須確保按順序學習時,能全部被完成。每個課程的前置課程如下:
|
||||
接下来,我们讨论一个有点学术性的例子,考虑这样一个问题:给定一些计算机课程,每个课程都有前置课程,只有完成了前置课程才可以开始当前课程的学习;我们的目标是选择出一组课程,这组课程必须确保按顺序学习时,能全部被完成。每个课程的前置课程如下:
|
||||
|
||||
<u><i>gopl.io/ch5/toposort</i></u>
|
||||
```Go
|
||||
// prereqs記録了每個課程的前置課程
|
||||
// prereqs记录了每个课程的前置课程
|
||||
var prereqs = map[string][]string{
|
||||
"algorithms": {"data structures"},
|
||||
"calculus": {"linear algebra"},
|
||||
@@ -59,7 +59,7 @@ var prereqs = map[string][]string{
|
||||
}
|
||||
```
|
||||
|
||||
這類問題被稱作拓撲排序。從概念上説,前置條件可以構成有向圖。圖中的頂點表示課程,邊表示課程間的依賴關繫。顯然,圖中應該無環,這也就是説從某點出發的邊,最終不會迴到該點。下面的代碼用深度優先蒐索了整張圖,獲得了符合要求的課程序列。
|
||||
这类问题被称作拓扑排序。从概念上说,前置条件可以构成有向图。图中的顶点表示课程,边表示课程间的依赖关系。显然,图中应该无环,这也就是说从某点出发的边,最终不会回到该点。下面的代码用深度优先搜索了整张图,获得了符合要求的课程序列。
|
||||
|
||||
```Go
|
||||
func main() {
|
||||
@@ -91,7 +91,7 @@ func topoSort(m map[string][]string) []string {
|
||||
}
|
||||
```
|
||||
|
||||
當匿名函數需要被遞歸調用時,我們必須首先聲明一個變量(在上面的例子中,我們首先聲明了 visitAll),再將匿名函數賦值給這個變量。如果不分成兩部,函數字面量無法與visitAll綁定,我們也無法遞歸調用該匿名函數。
|
||||
当匿名函数需要被递归调用时,我们必须首先声明一个变量(在上面的例子中,我们首先声明了 visitAll),再将匿名函数赋值给这个变量。如果不分成两部,函数字面量无法与visitAll绑定,我们也无法递归调用该匿名函数。
|
||||
|
||||
```Go
|
||||
visitAll := func(items []string) {
|
||||
@@ -101,7 +101,7 @@ visitAll := func(items []string) {
|
||||
}
|
||||
```
|
||||
|
||||
在topsort中,首先對prereqs中的key排序,再調用visitAll。因爲prereqs映射的是切片而不是更複雜的map,所以數據的遍歷次序是固定的,這意味着你每次運行topsort得到的輸出都是一樣的。 topsort的輸出結果如下:
|
||||
在topsort中,首先对prereqs中的key排序,再调用visitAll。因为prereqs映射的是切片而不是更复杂的map,所以数据的遍历次序是固定的,这意味着你每次运行topsort得到的输出都是一样的。 topsort的输出结果如下:
|
||||
|
||||
```
|
||||
1: intro to programming
|
||||
@@ -119,7 +119,7 @@ visitAll := func(items []string) {
|
||||
13: programming languages
|
||||
```
|
||||
|
||||
讓我們迴到findLinks這個例子。我們將代碼移動到了links包下,將函數重命名爲Extract,在第八章我們會再次用到這個函數。新的匿名函數被引入,用於替換原來的visit函數。該匿名函數負責將新連接添加到切片中。在Extract中,使用forEachNode遍歷HTML頁面,由於Extract隻需要在遍歷結點前操作結點,所以forEachNode的post參數被傳入nil。
|
||||
让我们回到findLinks这个例子。我们将代码移动到了links包下,将函数重命名为Extract,在第八章我们会再次用到这个函数。新的匿名函数被引入,用于替换原来的visit函数。该匿名函数负责将新连接添加到切片中。在Extract中,使用forEachNode遍历HTML页面,由于Extract只需要在遍历结点前操作结点,所以forEachNode的post参数被传入nil。
|
||||
|
||||
<u><i>gopl.io/ch5/links</i></u>
|
||||
```Go
|
||||
@@ -166,11 +166,11 @@ func Extract(url string) ([]string, error) {
|
||||
}
|
||||
```
|
||||
|
||||
上面的代碼對之前的版本做了改進,現在links中存儲的不是href屬性的原始值,而是通過resp.Request.URL解析後的值。解析後,這些連接以絶對路徑的形式存在,可以直接被http.Get訪問。
|
||||
上面的代码对之前的版本做了改进,现在links中存储的不是href属性的原始值,而是通过resp.Request.URL解析后的值。解析后,这些连接以绝对路径的形式存在,可以直接被http.Get访问。
|
||||
|
||||
網頁抓取的核心問題就是如何遍歷圖。在topoSort的例子中,已經展示了深度優先遍歷,在網頁抓取中,我們會展示如何用廣度優先遍歷圖。在第8章,我們會介紹如何將深度優先和廣度優先結合使用。
|
||||
网页抓取的核心问题就是如何遍历图。在topoSort的例子中,已经展示了深度优先遍历,在网页抓取中,我们会展示如何用广度优先遍历图。在第8章,我们会介绍如何将深度优先和广度优先结合使用。
|
||||
|
||||
下面的函數實現了廣度優先算法。調用者需要輸入一個初始的待訪問列表和一個函數f。待訪問列表中的每個元素被定義爲string類型。廣度優先算法會爲每個元素調用一次f。每次f執行完畢後,會返迴一組待訪問元素。這些元素會被加入到待訪問列表中。當待訪問列表中的所有元素都被訪問後,breadthFirst函數運行結束。爲了避免同一個元素被訪問兩次,代碼中維護了一個map。
|
||||
下面的函数实现了广度优先算法。调用者需要输入一个初始的待访问列表和一个函数f。待访问列表中的每个元素被定义为string类型。广度优先算法会为每个元素调用一次f。每次f执行完毕后,会返回一组待访问元素。这些元素会被加入到待访问列表中。当待访问列表中的所有元素都被访问后,breadthFirst函数运行结束。为了避免同一个元素被访问两次,代码中维护了一个map。
|
||||
|
||||
<u><i>gopl.io/ch5/findlinks3</i></u>
|
||||
```Go
|
||||
@@ -192,9 +192,9 @@ func breadthFirst(f func(item string) []string, worklist []string) {
|
||||
}
|
||||
```
|
||||
|
||||
就像我們在章節3解釋的那樣,append的參數“f(item)...”,會將f返迴的一組元素一個個添加到worklist中。
|
||||
就像我们在章节3解释的那样,append的参数“f(item)...”,会将f返回的一组元素一个个添加到worklist中。
|
||||
|
||||
在我們網頁抓取器中,元素的類型是url。crawl函數會將URL輸出,提取其中的新鏈接,併將這些新鏈接返迴。我們會將crawl作爲參數傳遞給breadthFirst。
|
||||
在我们网页抓取器中,元素的类型是url。crawl函数会将URL输出,提取其中的新链接,并将这些新链接返回。我们会将crawl作为参数传递给breadthFirst。
|
||||
|
||||
```go
|
||||
func crawl(url string) []string {
|
||||
@@ -207,7 +207,7 @@ func crawl(url string) []string {
|
||||
}
|
||||
```
|
||||
|
||||
爲了使抓取器開始運行,我們用命令行輸入的參數作爲初始的待訪問url。
|
||||
为了使抓取器开始运行,我们用命令行输入的参数作为初始的待访问url。
|
||||
|
||||
```Go
|
||||
func main() {
|
||||
@@ -217,7 +217,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
讓我們從 https://golang.org 開始,下面是程序的輸出結果:
|
||||
让我们从 https://golang.org 开始,下面是程序的输出结果:
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch5/findlinks3
|
||||
@@ -232,16 +232,16 @@ https://www.youtube.com/watch?v=XCsL89YtqCs
|
||||
http://research.swtch.com/gotour
|
||||
```
|
||||
|
||||
當所有發現的鏈接都已經被訪問或電腦的內存耗盡時,程序運行結束。
|
||||
当所有发现的链接都已经被访问或电脑的内存耗尽时,程序运行结束。
|
||||
|
||||
**練習5.10:** 重寫topoSort函數,用map代替切片併移除對key的排序代碼。驗證結果的正確性(結果不唯一)。
|
||||
**练习5.10:** 重写topoSort函数,用map代替切片并移除对key的排序代码。验证结果的正确性(结果不唯一)。
|
||||
|
||||
**練習5.11:** 現在線性代數的老師把微積分設爲了前置課程。完善topSort,使其能檢測有向圖中的環。
|
||||
**练习5.11:** 现在线性代数的老师把微积分设为了前置课程。完善topSort,使其能检测有向图中的环。
|
||||
|
||||
**練習5.12:** gopl.io/ch5/outline2(5.5節)的startElement和endElement共用了全局變量depth,將它們脩改爲匿名函數,使其共享outline中的局部變量。
|
||||
**练习5.12:** gopl.io/ch5/outline2(5.5节)的startElement和endElement共用了全局变量depth,将它们修改为匿名函数,使其共享outline中的局部变量。
|
||||
|
||||
**練習5.13:** 脩改crawl,使其能保存發現的頁面,必要時,可以創建目録來保存這些頁面。隻保存來自原始域名下的頁面。假設初始頁面在golang.org下,就不要保存vimeo.com下的頁面。
|
||||
**练习5.13:** 修改crawl,使其能保存发现的页面,必要时,可以创建目录来保存这些页面。只保存来自原始域名下的页面。假设初始页面在golang.org下,就不要保存vimeo.com下的页面。
|
||||
|
||||
**練習5.14:** 使用breadthFirst遍歷其他數據結構。比如,topoSort例子中的課程依賴關繫(有向圖),個人計算機的文件層次結構(樹),你所在城市的公交或地鐵線路(無向圖)。
|
||||
**练习5.14:** 使用breadthFirst遍历其他数据结构。比如,topoSort例子中的课程依赖关系(有向图),个人计算机的文件层次结构(树),你所在城市的公交或地铁线路(无向图)。
|
||||
|
||||
{% include "./ch5-06-1.md" %}
|
||||
|
@@ -1,8 +1,8 @@
|
||||
## 5.7. 可變參數
|
||||
## 5.7. 可变参数
|
||||
|
||||
參數數量可變的函數稱爲爲可變參數函數。典型的例子就是fmt.Printf和類似函數。Printf首先接收一個的必備參數,之後接收任意個數的後續參數。
|
||||
参数数量可变的函数称为为可变参数函数。典型的例子就是fmt.Printf和类似函数。Printf首先接收一个的必备参数,之后接收任意个数的后续参数。
|
||||
|
||||
在聲明可變參數函數時,需要在參數列表的最後一個參數類型之前加上省略符號“...”,這表示該函數會接收任意數量的該類型參數。
|
||||
在声明可变参数函数时,需要在参数列表的最后一个参数类型之前加上省略符号“...”,这表示该函数会接收任意数量的该类型参数。
|
||||
|
||||
<u><i>gopl.io/ch5/sum</i></u>
|
||||
```Go
|
||||
@@ -15,7 +15,7 @@ func sum(vals...int) int {
|
||||
}
|
||||
```
|
||||
|
||||
sum函數返迴任意個int型參數的和。在函數體中,vals被看作是類型爲[] int的切片。sum可以接收任意數量的int型參數:
|
||||
sum函数返回任意个int型参数的和。在函数体中,vals被看作是类型为[] int的切片。sum可以接收任意数量的int型参数:
|
||||
|
||||
```Go
|
||||
fmt.Println(sum()) // "0"
|
||||
@@ -23,14 +23,14 @@ fmt.Println(sum(3)) // "3"
|
||||
fmt.Println(sum(1, 2, 3, 4)) // "10"
|
||||
```
|
||||
|
||||
在上面的代碼中,調用者隱式的創建一個數組,併將原始參數複製到數組中,再把數組的一個切片作爲參數傳給被調函數。如果原始參數已經是切片類型,我們該如何傳遞給sum?隻需在最後一個參數後加上省略符。下面的代碼功能與上個例子中最後一條語句相同。
|
||||
在上面的代码中,调用者隐式的创建一个数组,并将原始参数复制到数组中,再把数组的一个切片作为参数传给被调函数。如果原始参数已经是切片类型,我们该如何传递给sum?只需在最后一个参数后加上省略符。下面的代码功能与上个例子中最后一条语句相同。
|
||||
|
||||
```Go
|
||||
values := []int{1, 2, 3, 4}
|
||||
fmt.Println(sum(values...)) // "10"
|
||||
```
|
||||
|
||||
雖然在可變參數函數內部,...int 型參數的行爲看起來很像切片類型,但實際上,可變參數函數和以切片作爲參數的函數是不同的。
|
||||
虽然在可变参数函数内部,...int 型参数的行为看起来很像切片类型,但实际上,可变参数函数和以切片作为参数的函数是不同的。
|
||||
|
||||
```Go
|
||||
func f(...int) {}
|
||||
@@ -39,7 +39,7 @@ fmt.Printf("%T\n", f) // "func(...int)"
|
||||
fmt.Printf("%T\n", g) // "func([]int)"
|
||||
```
|
||||
|
||||
可變參數函數經常被用於格式化字符串。下面的errorf函數構造了一個以行號開頭的,經過格式化的錯誤信息。函數名的後綴f是一種通用的命名規范,代表該可變參數函數可以接收Printf風格的格式化字符串。
|
||||
可变参数函数经常被用于格式化字符串。下面的errorf函数构造了一个以行号开头的,经过格式化的错误信息。函数名的后缀f是一种通用的命名规范,代表该可变参数函数可以接收Printf风格的格式化字符串。
|
||||
|
||||
```Go
|
||||
func errorf(linenum int, format string, args ...interface{}) {
|
||||
@@ -51,13 +51,13 @@ linenum, name := 12, "count"
|
||||
errorf(linenum, "undefined: %s", name) // "Line 12: undefined: count"
|
||||
```
|
||||
|
||||
interfac{}表示函數的最後一個參數可以接收任意類型,我們會在第7章詳細介紹。
|
||||
interfac{}表示函数的最后一个参数可以接收任意类型,我们会在第7章详细介绍。
|
||||
|
||||
**練習5.15:** 編寫類似sum的可變參數函數max和min。考慮不傳參時,max和min該如何處理,再編寫至少接收1個參數的版本。
|
||||
**练习5.15:** 编写类似sum的可变参数函数max和min。考虑不传参时,max和min该如何处理,再编写至少接收1个参数的版本。
|
||||
|
||||
**練習5.16:**編寫多參數版本的strings.Join。
|
||||
**练习5.16:**编写多参数版本的strings.Join。
|
||||
|
||||
**練習5.17:**編寫多參數版本的ElementsByTagName,函數接收一個HTML結點樹以及任意數量的標籤名,返迴與這些標籤名匹配的所有元素。下面給出了2個例子:
|
||||
**练习5.17:**编写多参数版本的ElementsByTagName,函数接收一个HTML结点树以及任意数量的标签名,返回与这些标签名匹配的所有元素。下面给出了2个例子:
|
||||
|
||||
```Go
|
||||
func ElementsByTagName(doc *html.Node, name...string) []*html.Node
|
||||
|
@@ -1,8 +1,8 @@
|
||||
## 5.8. Deferred函數
|
||||
## 5.8. Deferred函数
|
||||
|
||||
在findLinks的例子中,我們用http.Get的輸出作爲html.Parse的輸入。隻有url的內容的確是HTML格式的,html.Parse才可以正常工作,但實際上,url指向的內容很豐富,可能是圖片,純文本或是其他。將這些格式的內容傳遞給html.parse,會産生不良後果。
|
||||
在findLinks的例子中,我们用http.Get的输出作为html.Parse的输入。只有url的内容的确是HTML格式的,html.Parse才可以正常工作,但实际上,url指向的内容很丰富,可能是图片,纯文本或是其他。将这些格式的内容传递给html.parse,会产生不良后果。
|
||||
|
||||
下面的例子獲取HTML頁面併輸出頁面的標題。title函數會檢査服務器返迴的Content-Type字段,如果發現頁面不是HTML,將終止函數運行,返迴錯誤。
|
||||
下面的例子获取HTML页面并输出页面的标题。title函数会检查服务器返回的Content-Type字段,如果发现页面不是HTML,将终止函数运行,返回错误。
|
||||
|
||||
<u><i>gopl.io/ch5/title1</i></u>
|
||||
```Go
|
||||
@@ -32,7 +32,7 @@ func title(url string) error {
|
||||
}
|
||||
```
|
||||
|
||||
下面展示了運行效果:
|
||||
下面展示了运行效果:
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch5/title1
|
||||
@@ -44,11 +44,11 @@ $ ./title1 https://golang.org/doc/gopher/frontpage.png
|
||||
title: https://golang.org/doc/gopher/frontpage.png has type image/png, not text/html
|
||||
```
|
||||
|
||||
resp.Body.close調用了多次,這是爲了確保title在所有執行路徑下(卽使函數運行失敗)都關閉了網絡連接。隨着函數變得複雜,需要處理的錯誤也變多,維護清理邏輯變得越來越睏難。而Go語言獨有的defer機製可以讓事情變得簡單。
|
||||
resp.Body.close调用了多次,这是为了确保title在所有执行路径下(即使函数运行失败)都关闭了网络连接。随着函数变得复杂,需要处理的错误也变多,维护清理逻辑变得越来越困难。而Go语言独有的defer机制可以让事情变得简单。
|
||||
|
||||
你隻需要在調用普通函數或方法前加上關鍵字defer,就完成了defer所需要的語法。當defer語句被執行時,跟在defer後面的函數會被延遲執行。直到包含該defer語句的函數執行完畢時,defer後的函數才會被執行,不論包含defer語句的函數是通過return正常結束,還是由於panic導致的異常結束。你可以在一個函數中執行多條defer語句,它們的執行順序與聲明順序相反。
|
||||
你只需要在调用普通函数或方法前加上关键字defer,就完成了defer所需要的语法。当defer语句被执行时,跟在defer后面的函数会被延迟执行。直到包含该defer语句的函数执行完毕时,defer后的函数才会被执行,不论包含defer语句的函数是通过return正常结束,还是由于panic导致的异常结束。你可以在一个函数中执行多条defer语句,它们的执行顺序与声明顺序相反。
|
||||
|
||||
defer語句經常被用於處理成對的操作,如打開、關閉、連接、斷開連接、加鎖、釋放鎖。通過defer機製,不論函數邏輯多複雜,都能保證在任何執行路徑下,資源被釋放。釋放資源的defer應該直接跟在請求資源的語句後。在下面的代碼中,一條defer語句替代了之前的所有resp.Body.Close
|
||||
defer语句经常被用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁。通过defer机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放。释放资源的defer应该直接跟在请求资源的语句后。在下面的代码中,一条defer语句替代了之前的所有resp.Body.Close
|
||||
|
||||
<u><i>gopl.io/ch5/title2</i></u>
|
||||
```Go
|
||||
@@ -71,7 +71,7 @@ func title(url string) error {
|
||||
}
|
||||
```
|
||||
|
||||
在處理其他資源時,也可以采用defer機製,比如對文件的操作:
|
||||
在处理其他资源时,也可以采用defer机制,比如对文件的操作:
|
||||
|
||||
<u><i>io/ioutil</i></u>
|
||||
```Go
|
||||
@@ -86,7 +86,7 @@ func ReadFile(filename string) ([]byte, error) {
|
||||
}
|
||||
```
|
||||
|
||||
或是處理互斥鎖(9.2章)
|
||||
或是处理互斥锁(9.2章)
|
||||
|
||||
```Go
|
||||
var mu sync.Mutex
|
||||
@@ -98,7 +98,7 @@ func lookup(key string) int {
|
||||
}
|
||||
```
|
||||
|
||||
調試複雜程序時,defer機製也常被用於記録何時進入和退出函數。下例中的bigSlowOperation函數,直接調用trace記録函數的被調情況。bigSlowOperation被調時,trace會返迴一個函數值,該函數值會在bigSlowOperation退出時被調用。通過這種方式, 我們可以隻通過一條語句控製函數的入口和所有的出口,甚至可以記録函數的運行時間,如例子中的start。需要註意一點:不要忘記defer語句後的圓括號,否則本該在進入時執行的操作會在退出時執行,而本該在退出時執行的,永遠不會被執行。
|
||||
调试复杂程序时,defer机制也常被用于记录何时进入和退出函数。下例中的bigSlowOperation函数,直接调用trace记录函数的被调情况。bigSlowOperation被调时,trace会返回一个函数值,该函数值会在bigSlowOperation退出时被调用。通过这种方式, 我们可以只通过一条语句控制函数的入口和所有的出口,甚至可以记录函数的运行时间,如例子中的start。需要注意一点:不要忘记defer语句后的圆括号,否则本该在进入时执行的操作会在退出时执行,而本该在退出时执行的,永远不会被执行。
|
||||
|
||||
<u><i>gopl.io/ch5/trace</i></u>
|
||||
```Go
|
||||
@@ -118,7 +118,7 @@ func trace(msg string) func() {
|
||||
}
|
||||
```
|
||||
|
||||
每一次bigSlowOperation被調用,程序都會記録函數的進入,退出,持續時間。(我們用time.Sleep模擬一個耗時的操作)
|
||||
每一次bigSlowOperation被调用,程序都会记录函数的进入,退出,持续时间。(我们用time.Sleep模拟一个耗时的操作)
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch5/trace
|
||||
@@ -127,9 +127,9 @@ $ ./trace
|
||||
2015/11/18 09:53:36 exit bigSlowOperation (10.000589217s)
|
||||
```
|
||||
|
||||
我們知道,defer語句中的函數會在return語句更新返迴值變量後再執行,又因爲在函數中定義的匿名函數可以訪問該函數包括返迴值變量在內的所有變量,所以,對匿名函數采用defer機製,可以使其觀察函數的返迴值。
|
||||
我们知道,defer语句中的函数会在return语句更新返回值变量后再执行,又因为在函数中定义的匿名函数可以访问该函数包括返回值变量在内的所有变量,所以,对匿名函数采用defer机制,可以使其观察函数的返回值。
|
||||
|
||||
以double函數爲例:
|
||||
以double函数为例:
|
||||
|
||||
```Go
|
||||
func double(x int) int {
|
||||
@@ -137,7 +137,7 @@ func double(x int) int {
|
||||
}
|
||||
```
|
||||
|
||||
我們隻需要首先命名double的返迴值,再增加defer語句,我們就可以在double每次被調用時,輸出參數以及返迴值。
|
||||
我们只需要首先命名double的返回值,再增加defer语句,我们就可以在double每次被调用时,输出参数以及返回值。
|
||||
|
||||
```Go
|
||||
func double(x int) (result int) {
|
||||
@@ -149,9 +149,9 @@ _ = double(4)
|
||||
// "double(4) = 8"
|
||||
```
|
||||
|
||||
可能doulbe函數過於簡單,看不出這個小技巧的作用,但對於有許多return語句的函數而言,這個技巧很有用。
|
||||
可能doulbe函数过于简单,看不出这个小技巧的作用,但对于有许多return语句的函数而言,这个技巧很有用。
|
||||
|
||||
被延遲執行的匿名函數甚至可以脩改函數返迴給調用者的返迴值:
|
||||
被延迟执行的匿名函数甚至可以修改函数返回给调用者的返回值:
|
||||
|
||||
```Go
|
||||
func triple(x int) (result int) {
|
||||
@@ -161,7 +161,7 @@ func triple(x int) (result int) {
|
||||
fmt.Println(triple(4)) // "12"
|
||||
```
|
||||
|
||||
在循環體中的defer語句需要特别註意,因爲隻有在函數執行完畢後,這些被延遲的函數才會執行。下面的代碼會導致繫統的文件描述符耗盡,因爲在所有文件都被處理之前,沒有文件會被關閉。
|
||||
在循环体中的defer语句需要特别注意,因为只有在函数执行完毕后,这些被延迟的函数才会执行。下面的代码会导致系统的文件描述符耗尽,因为在所有文件都被处理之前,没有文件会被关闭。
|
||||
|
||||
```Go
|
||||
for _, filename := range filenames {
|
||||
@@ -175,7 +175,7 @@ for _, filename := range filenames {
|
||||
}
|
||||
```
|
||||
|
||||
一種解決方法是將循環體中的defer語句移至另外一個函數。在每次循環時,調用這個函數。
|
||||
一种解决方法是将循环体中的defer语句移至另外一个函数。在每次循环时,调用这个函数。
|
||||
|
||||
```Go
|
||||
for _, filename := range filenames {
|
||||
@@ -193,7 +193,7 @@ func doFile(filename string) error {
|
||||
}
|
||||
```
|
||||
|
||||
下面的代碼是fetch(1.5節)的改進版,我們將http響應信息寫入本地文件而不是從標準輸出流輸出。我們通過path.Base提出url路徑的最後一段作爲文件名。
|
||||
下面的代码是fetch(1.5节)的改进版,我们将http响应信息写入本地文件而不是从标准输出流输出。我们通过path.Base提出url路径的最后一段作为文件名。
|
||||
|
||||
<u><i>gopl.io/ch5/fetch</i></u>
|
||||
```Go
|
||||
@@ -222,6 +222,6 @@ func fetch(url string) (filename string, n int64, err error) {
|
||||
}
|
||||
```
|
||||
|
||||
對resp.Body.Close延遲調用我們已經見過了,在此不做解釋。上例中,通過os.Create打開文件進行寫入,在關閉文件時,我們沒有對f.close采用defer機製,因爲這會産生一些微妙的錯誤。許多文件繫統,尤其是NFS,寫入文件時發生的錯誤會被延遲到文件關閉時反饋。如果沒有檢査文件關閉時的反饋信息,可能會導致數據丟失,而我們還誤以爲寫入操作成功。如果io.Copy和f.close都失敗了,我們傾向於將io.Copy的錯誤信息反饋給調用者,因爲它先於f.close發生,更有可能接近問題的本質。
|
||||
对resp.Body.Close延迟调用我们已经见过了,在此不做解释。上例中,通过os.Create打开文件进行写入,在关闭文件时,我们没有对f.close采用defer机制,因为这会产生一些微妙的错误。许多文件系统,尤其是NFS,写入文件时发生的错误会被延迟到文件关闭时反馈。如果没有检查文件关闭时的反馈信息,可能会导致数据丢失,而我们还误以为写入操作成功。如果io.Copy和f.close都失败了,我们倾向于将io.Copy的错误信息反馈给调用者,因为它先于f.close发生,更有可能接近问题的本质。
|
||||
|
||||
**練習5.18:**不脩改fetch的行爲,重寫fetch函數,要求使用defer機製關閉文件。
|
||||
**练习5.18:**不修改fetch的行为,重写fetch函数,要求使用defer机制关闭文件。
|
@@ -1,10 +1,10 @@
|
||||
## 5.9. Panic異常
|
||||
## 5.9. Panic异常
|
||||
|
||||
Go的類型繫統會在編譯時捕獲很多錯誤,但有些錯誤隻能在運行時檢査,如數組訪問越界、空指針引用等。這些運行時錯誤會引起painc異常。
|
||||
Go的类型系统会在编译时捕获很多错误,但有些错误只能在运行时检查,如数组访问越界、空指针引用等。这些运行时错误会引起painc异常。
|
||||
|
||||
一般而言,當panic異常發生時,程序會中斷運行,併立卽執行在該goroutine(可以先理解成線程,在第8章會詳細介紹)中被延遲的函數(defer 機製)。隨後,程序崩潰併輸出日誌信息。日誌信息包括panic value和函數調用的堆棧跟蹤信息。panic value通常是某種錯誤信息。對於每個goroutine,日誌信息中都會有與之相對的,發生panic時的函數調用堆棧跟蹤信息。通常,我們不需要再次運行程序去定位問題,日誌信息已經提供了足夠的診斷依據。因此,在我們填寫問題報告時,一般會將panic異常和日誌信息一併記録。
|
||||
一般而言,当panic异常发生时,程序会中断运行,并立即执行在该goroutine(可以先理解成线程,在第8章会详细介绍)中被延迟的函数(defer 机制)。随后,程序崩溃并输出日志信息。日志信息包括panic value和函数调用的堆栈跟踪信息。panic value通常是某种错误信息。对于每个goroutine,日志信息中都会有与之相对的,发生panic时的函数调用堆栈跟踪信息。通常,我们不需要再次运行程序去定位问题,日志信息已经提供了足够的诊断依据。因此,在我们填写问题报告时,一般会将panic异常和日志信息一并记录。
|
||||
|
||||
不是所有的panic異常都來自運行時,直接調用內置的panic函數也會引發panic異常;panic函數接受任何值作爲參數。當某些不應該發生的場景發生時,我們就應該調用panic。比如,當程序到達了某條邏輯上不可能到達的路徑:
|
||||
不是所有的panic异常都来自运行时,直接调用内置的panic函数也会引发panic异常;panic函数接受任何值作为参数。当某些不应该发生的场景发生时,我们就应该调用panic。比如,当程序到达了某条逻辑上不可能到达的路径:
|
||||
|
||||
```Go
|
||||
switch s := suit(drawCard()); s {
|
||||
@@ -17,7 +17,7 @@ switch s := suit(drawCard()); s {
|
||||
}
|
||||
```
|
||||
|
||||
斷言函數必須滿足的前置條件是明智的做法,但這很容易被濫用。除非你能提供更多的錯誤信息,或者能更快速的發現錯誤,否則不需要使用斷言,編譯器在運行時會幫你檢査代碼。
|
||||
断言函数必须满足的前置条件是明智的做法,但这很容易被滥用。除非你能提供更多的错误信息,或者能更快速的发现错误,否则不需要使用断言,编译器在运行时会帮你检查代码。
|
||||
|
||||
```Go
|
||||
func Reset(x *Buffer) {
|
||||
@@ -28,11 +28,11 @@ func Reset(x *Buffer) {
|
||||
}
|
||||
```
|
||||
|
||||
雖然Go的panic機製類似於其他語言的異常,但panic的適用場景有一些不同。由於panic會引起程序的崩潰,因此panic一般用於嚴重錯誤,如程序內部的邏輯不一致。勤奮的程序員認爲任何崩潰都表明代碼中存在漏洞,所以對於大部分漏洞,我們應該使用Go提供的錯誤機製,而不是panic,盡量避免程序的崩潰。在健壯的程序中,任何可以預料到的錯誤,如不正確的輸入、錯誤的配置或是失敗的I/O操作都應該被優雅的處理,最好的處理方式,就是使用Go的錯誤機製。
|
||||
虽然Go的panic机制类似于其他语言的异常,但panic的适用场景有一些不同。由于panic会引起程序的崩溃,因此panic一般用于严重错误,如程序内部的逻辑不一致。勤奋的程序员认为任何崩溃都表明代码中存在漏洞,所以对于大部分漏洞,我们应该使用Go提供的错误机制,而不是panic,尽量避免程序的崩溃。在健壮的程序中,任何可以预料到的错误,如不正确的输入、错误的配置或是失败的I/O操作都应该被优雅的处理,最好的处理方式,就是使用Go的错误机制。
|
||||
|
||||
考慮regexp.Compile函數,該函數將正則表達式編譯成有效的可匹配格式。當輸入的正則表達式不合法時,該函數會返迴一個錯誤。當調用者明確的知道正確的輸入不會引起函數錯誤時,要求調用者檢査這個錯誤是不必要和纍贅的。我們應該假設函數的輸入一直合法,就如前面的斷言一樣:當調用者輸入了不應該出現的輸入時,觸發panic異常。
|
||||
考虑regexp.Compile函数,该函数将正则表达式编译成有效的可匹配格式。当输入的正则表达式不合法时,该函数会返回一个错误。当调用者明确的知道正确的输入不会引起函数错误时,要求调用者检查这个错误是不必要和累赘的。我们应该假设函数的输入一直合法,就如前面的断言一样:当调用者输入了不应该出现的输入时,触发panic异常。
|
||||
|
||||
在程序源碼中,大多數正則表達式是字符串字面值(string literals),因此regexp包提供了包裝函數regexp.MustCompile檢査輸入的合法性。
|
||||
在程序源码中,大多数正则表达式是字符串字面值(string literals),因此regexp包提供了包装函数regexp.MustCompile检查输入的合法性。
|
||||
|
||||
```Go
|
||||
package regexp
|
||||
@@ -46,13 +46,13 @@ func MustCompile(expr string) *Regexp {
|
||||
}
|
||||
```
|
||||
|
||||
包裝函數使得調用者可以便捷的用一個編譯後的正則表達式爲包級别的變量賦值:
|
||||
包装函数使得调用者可以便捷的用一个编译后的正则表达式为包级别的变量赋值:
|
||||
|
||||
```Go
|
||||
var httpSchemeRE = regexp.MustCompile(`^https?:`) //"http:" or "https:"
|
||||
```
|
||||
|
||||
顯然,MustCompile不能接收不合法的輸入。函數名中的Must前綴是一種針對此類函數的命名約定,比如template.Must(4.6節)
|
||||
显然,MustCompile不能接收不合法的输入。函数名中的Must前缀是一种针对此类函数的命名约定,比如template.Must(4.6节)
|
||||
|
||||
```Go
|
||||
func main() {
|
||||
@@ -65,7 +65,7 @@ func f(x int) {
|
||||
}
|
||||
```
|
||||
|
||||
上例中的運行輸出如下:
|
||||
上例中的运行输出如下:
|
||||
|
||||
```
|
||||
f(3)
|
||||
@@ -76,7 +76,7 @@ defer 2
|
||||
defer 3
|
||||
```
|
||||
|
||||
當f(0)被調用時,發生panic異常,之前被延遲執行的的3個fmt.Printf被調用。程序中斷執行後,panic信息和堆棧信息會被輸出(下面是簡化的輸出):
|
||||
当f(0)被调用时,发生panic异常,之前被延迟执行的的3个fmt.Printf被调用。程序中断执行后,panic信息和堆栈信息会被输出(下面是简化的输出):
|
||||
|
||||
```
|
||||
panic: runtime error: integer divide by zero
|
||||
@@ -92,9 +92,9 @@ main.main()
|
||||
src/gopl.io/ch5/defer1/defer.go:10
|
||||
```
|
||||
|
||||
我們在下一節將看到,如何使程序從panic異常中恢複,阻止程序的崩潰。
|
||||
我们在下一节将看到,如何使程序从panic异常中恢复,阻止程序的崩溃。
|
||||
|
||||
爲了方便診斷問題,runtime包允許程序員輸出堆棧信息。在下面的例子中,我們通過在main函數中延遲調用printStack輸出堆棧信息。
|
||||
为了方便诊断问题,runtime包允许程序员输出堆栈信息。在下面的例子中,我们通过在main函数中延迟调用printStack输出堆栈信息。
|
||||
|
||||
```Go
|
||||
gopl.io/ch5/defer2
|
||||
@@ -109,7 +109,7 @@ func printStack() {
|
||||
}
|
||||
```
|
||||
|
||||
printStack的簡化輸出如下(下面隻是printStack的輸出,不包括panic的日誌信息):
|
||||
printStack的简化输出如下(下面只是printStack的输出,不包括panic的日志信息):
|
||||
|
||||
```
|
||||
goroutine 1 [running]:
|
||||
@@ -127,4 +127,4 @@ main.main()
|
||||
src/gopl.io/ch5/defer2/defer.go:15
|
||||
```
|
||||
|
||||
將panic機製類比其他語言異常機製的讀者可能會驚訝,runtime.Stack爲何能輸出已經被釋放函數的信息?在Go的panic機製中,延遲函數的調用在釋放堆棧信息之前。
|
||||
将panic机制类比其他语言异常机制的读者可能会惊讶,runtime.Stack为何能输出已经被释放函数的信息?在Go的panic机制中,延迟函数的调用在释放堆栈信息之前。
|
||||
|
@@ -1,10 +1,10 @@
|
||||
## 5.10. Recover捕獲異常
|
||||
## 5.10. Recover捕获异常
|
||||
|
||||
通常來説,不應該對panic異常做任何處理,但有時,也許我們可以從異常中恢複,至少我們可以在程序崩潰前,做一些操作。舉個例子,當web服務器遇到不可預料的嚴重問題時,在崩潰前應該將所有的連接關閉;如果不做任何處理,會使得客戶端一直處於等待狀態。如果web服務器還在開發階段,服務器甚至可以將異常信息反饋到客戶端,幫助調試。
|
||||
通常来说,不应该对panic异常做任何处理,但有时,也许我们可以从异常中恢复,至少我们可以在程序崩溃前,做一些操作。举个例子,当web服务器遇到不可预料的严重问题时,在崩溃前应该将所有的连接关闭;如果不做任何处理,会使得客户端一直处于等待状态。如果web服务器还在开发阶段,服务器甚至可以将异常信息反馈到客户端,帮助调试。
|
||||
|
||||
如果在deferred函數中調用了內置函數recover,併且定義該defer語句的函數發生了panic異常,recover會使程序從panic中恢複,併返迴panic value。導致panic異常的函數不會繼續運行,但能正常返迴。在未發生panic時調用recover,recover會返迴nil。
|
||||
如果在deferred函数中调用了内置函数recover,并且定义该defer语句的函数发生了panic异常,recover会使程序从panic中恢复,并返回panic value。导致panic异常的函数不会继续运行,但能正常返回。在未发生panic时调用recover,recover会返回nil。
|
||||
|
||||
讓我們以語言解析器爲例,説明recover的使用場景。考慮到語言解析器的複雜性,卽使某個語言解析器目前工作正常,也無法肯定它沒有漏洞。因此,當某個異常出現時,我們不會選擇讓解析器崩潰,而是會將panic異常當作普通的解析錯誤,併附加額外信息提醒用戶報告此錯誤。
|
||||
让我们以语言解析器为例,说明recover的使用场景。考虑到语言解析器的复杂性,即使某个语言解析器目前工作正常,也无法肯定它没有漏洞。因此,当某个异常出现时,我们不会选择让解析器崩溃,而是会将panic异常当作普通的解析错误,并附加额外信息提醒用户报告此错误。
|
||||
|
||||
```Go
|
||||
func Parse(input string) (s *Syntax, err error) {
|
||||
@@ -17,17 +17,17 @@ func Parse(input string) (s *Syntax, err error) {
|
||||
}
|
||||
```
|
||||
|
||||
deferred函數幫助Parse從panic中恢複。在deferred函數內部,panic value被附加到錯誤信息中;併用err變量接收錯誤信息,返迴給調用者。我們也可以通過調用runtime.Stack往錯誤信息中添加完整的堆棧調用信息。
|
||||
deferred函数帮助Parse从panic中恢复。在deferred函数内部,panic value被附加到错误信息中;并用err变量接收错误信息,返回给调用者。我们也可以通过调用runtime.Stack往错误信息中添加完整的堆栈调用信息。
|
||||
|
||||
不加區分的恢複所有的panic異常,不是可取的做法;因爲在panic之後,無法保證包級變量的狀態仍然和我們預期一致。比如,對數據結構的一次重要更新沒有被完整完成、文件或者網絡連接沒有被關閉、獲得的鎖沒有被釋放。此外,如果寫日誌時産生的panic被不加區分的恢複,可能會導致漏洞被忽略。
|
||||
不加区分的恢复所有的panic异常,不是可取的做法;因为在panic之后,无法保证包级变量的状态仍然和我们预期一致。比如,对数据结构的一次重要更新没有被完整完成、文件或者网络连接没有被关闭、获得的锁没有被释放。此外,如果写日志时产生的panic被不加区分的恢复,可能会导致漏洞被忽略。
|
||||
|
||||
雖然把對panic的處理都集中在一個包下,有助於簡化對複雜和不可以預料問題的處理,但作爲被廣泛遵守的規范,你不應該試圖去恢複其他包引起的panic。公有的API應該將函數的運行失敗作爲error返迴,而不是panic。同樣的,你也不應該恢複一個由他人開發的函數引起的panic,比如説調用者傳入的迴調函數,因爲你無法確保這樣做是安全的。
|
||||
虽然把对panic的处理都集中在一个包下,有助于简化对复杂和不可以预料问题的处理,但作为被广泛遵守的规范,你不应该试图去恢复其他包引起的panic。公有的API应该将函数的运行失败作为error返回,而不是panic。同样的,你也不应该恢复一个由他人开发的函数引起的panic,比如说调用者传入的回调函数,因为你无法确保这样做是安全的。
|
||||
|
||||
有時我們很難完全遵循規范,舉個例子,net/http包中提供了一個web服務器,將收到的請求分發給用戶提供的處理函數。很顯然,我們不能因爲某個處理函數引發的panic異常,殺掉整個進程;web服務器遇到處理函數導致的panic時會調用recover,輸出堆棧信息,繼續運行。這樣的做法在實踐中很便捷,但也會引起資源洩漏,或是因爲recover操作,導致其他問題。
|
||||
有时我们很难完全遵循规范,举个例子,net/http包中提供了一个web服务器,将收到的请求分发给用户提供的处理函数。很显然,我们不能因为某个处理函数引发的panic异常,杀掉整个进程;web服务器遇到处理函数导致的panic时会调用recover,输出堆栈信息,继续运行。这样的做法在实践中很便捷,但也会引起资源泄漏,或是因为recover操作,导致其他问题。
|
||||
|
||||
基於以上原因,安全的做法是有選擇性的recover。換句話説,隻恢複應該被恢複的panic異常,此外,這些異常所占的比例應該盡可能的低。爲了標識某個panic是否應該被恢複,我們可以將panic value設置成特殊類型。在recover時對panic value進行檢査,如果發現panic value是特殊類型,就將這個panic作爲errror處理,如果不是,則按照正常的panic進行處理(在下面的例子中,我們會看到這種方式)。
|
||||
基于以上原因,安全的做法是有选择性的recover。换句话说,只恢复应该被恢复的panic异常,此外,这些异常所占的比例应该尽可能的低。为了标识某个panic是否应该被恢复,我们可以将panic value设置成特殊类型。在recover时对panic value进行检查,如果发现panic value是特殊类型,就将这个panic作为errror处理,如果不是,则按照正常的panic进行处理(在下面的例子中,我们会看到这种方式)。
|
||||
|
||||
下面的例子是title函數的變形,如果HTML頁面包含多個`<title>`,該函數會給調用者返迴一個錯誤(error)。在soleTitle內部處理時,如果檢測到有多個`<title>`,會調用panic,阻止函數繼續遞歸,併將特殊類型bailout作爲panic的參數。
|
||||
下面的例子是title函数的变形,如果HTML页面包含多个`<title>`,该函数会给调用者返回一个错误(error)。在soleTitle内部处理时,如果检测到有多个`<title>`,会调用panic,阻止函数继续递归,并将特殊类型bailout作为panic的参数。
|
||||
|
||||
```Go
|
||||
// soleTitle returns the text of the first non-empty title element
|
||||
@@ -60,8 +60,8 @@ func soleTitle(doc *html.Node) (title string, err error) {
|
||||
}
|
||||
```
|
||||
|
||||
在上例中,deferred函數調用recover,併檢査panic value。當panic value是bailout{}類型時,deferred函數生成一個error返迴給調用者。當panic value是其他non-nil值時,表示發生了未知的panic異常,deferred函數將調用panic函數併將當前的panic value作爲參數傳入;此時,等同於recover沒有做任何操作。(請註意:在例子中,對可預期的錯誤采用了panic,這違反了之前的建議,我們在此隻是想向讀者演示這種機製。)
|
||||
在上例中,deferred函数调用recover,并检查panic value。当panic value是bailout{}类型时,deferred函数生成一个error返回给调用者。当panic value是其他non-nil值时,表示发生了未知的panic异常,deferred函数将调用panic函数并将当前的panic value作为参数传入;此时,等同于recover没有做任何操作。(请注意:在例子中,对可预期的错误采用了panic,这违反了之前的建议,我们在此只是想向读者演示这种机制。)
|
||||
|
||||
有些情況下,我們無法恢複。某些致命錯誤會導致Go在運行時終止程序,如內存不足。
|
||||
有些情况下,我们无法恢复。某些致命错误会导致Go在运行时终止程序,如内存不足。
|
||||
|
||||
**練習5.19:** 使用panic和recover編寫一個不包含return語句但能返迴一個非零值的函數。
|
||||
**练习5.19:** 使用panic和recover编写一个不包含return语句但能返回一个非零值的函数。
|
@@ -1,6 +1,6 @@
|
||||
# 第五章 函數
|
||||
# 第五章 函数
|
||||
|
||||
函數可以讓我們將一個語句序列打包爲一個單元,然後可以從程序中其它地方多次調用。函數的機製可以讓我們將一個大的工作分解爲小的任務,這樣的小任務可以讓不同程序員在不同時間、不同地方獨立完成。一個函數同時對用戶隱藏了其實現細節。由於這些因素,對於任何編程語言來説,函數都是一個至關重要的部分。
|
||||
函数可以让我们将一个语句序列打包为一个单元,然后可以从程序中其它地方多次调用。函数的机制可以让我们将一个大的工作分解为小的任务,这样的小任务可以让不同程序员在不同时间、不同地方独立完成。一个函数同时对用户隐藏了其实现细节。由于这些因素,对于任何编程语言来说,函数都是一个至关重要的部分。
|
||||
|
||||
我們已經見過許多函數了。現在,讓我們多花一點時間來徹底地討論函數特性。本章的運行示例是一個網絡蜘蛛,也就是web蒐索引擎中負責抓取網頁部分的組件,它們根據抓取網頁中的鏈接繼續抓取鏈接指向的頁面。一個網絡蜘蛛的例子給我們足夠的機會去探索遞歸函數、匿名函數、錯誤處理和函數其它的很多特性。
|
||||
我们已经见过许多函数了。现在,让我们多花一点时间来彻底地讨论函数特性。本章的运行示例是一个网络蜘蛛,也就是web搜索引擎中负责抓取网页部分的组件,它们根据抓取网页中的链接继续抓取链接指向的页面。一个网络蜘蛛的例子给我们足够的机会去探索递归函数、匿名函数、错误处理和函数其它的很多特性。
|
||||
|
||||
|
Reference in New Issue
Block a user