mirror of
https://github.com/gopl-zh/gopl-zh.github.com.git
synced 2024-12-29 00:06:20 +00:00
第5章,部分字词修订。
This commit is contained in:
parent
9c0723ff6a
commit
ff3c5b0a70
@ -18,7 +18,7 @@ func hypot(x, y float64) float64 {
|
||||
fmt.Println(hypot(3,4)) // "5"
|
||||
```
|
||||
|
||||
x和y是形参名,3和4是调用时的传入的实数,函数返回了一个float64类型的值。
|
||||
x和y是形参名,3和4是调用时的传入的实参,函数返回了一个float64类型的值。
|
||||
返回值也可以像形式参数一样被命名。在这种情况下,每个返回值被声明成一个局部变量,并根据该返回值的类型,将其初始化为0。
|
||||
如果一个函数在声明时,包含返回值列表,该函数必须以 return语句结尾,除非函数明显无法运行到结尾处。例如函数在结尾时调用了panic异常或函数中存在无限循环。
|
||||
|
||||
@ -43,7 +43,7 @@ fmt.Printf("%T\n", first) // "func(int, int) int"
|
||||
fmt.Printf("%T\n", zero) // "func(int, int) int"
|
||||
```
|
||||
|
||||
函数的类型被称为函数的标识符。如果两个函数形式参数列表和返回值列表中的变量类型一一对应,那么这两个函数被认为有相同的类型和标识符。形参和返回值的变量名不影响函数标识符也不影响它们是否可以以省略参数类型的形式表示。
|
||||
函数的类型被称为函数的标识符。如果两个函数形式参数列表和返回值列表中的变量类型一一对应,那么这两个函数被认为有相同的类型或标识符。形参和返回值的变量名不影响函数标识符,也不影响它们是否可以以省略参数类型的形式表示。
|
||||
|
||||
每一次函数调用都必须按照声明顺序为所有参数提供实参(参数值)。在函数调用时,Go语言没有默认参数值,也没有任何方法可以通过参数名指定形参,因此形参和返回值的变量名对于函数调用者而言没有意义。
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
下文的示例代码使用了非标准包 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
|
||||
@ -151,7 +151,7 @@ $ ./fetch https://golang.org | ./outline
|
||||
|
||||
正如你在上面实验中所见,大部分HTML页面只需几层递归就能被处理,但仍然有些页面需要深层次的递归。
|
||||
|
||||
大部分编程语言使用固定大小的函数调用栈,常见的大小从64KB到2MB不等。固定大小栈会限制递归的深度,当你用递归处理大量数据时,需要避免栈溢出;除此之外,还会导致安全性问题。与相反,Go语言使用可变栈,栈的大小按需增加(初始时很小)。这使得我们使用递归时不必考虑溢出和安全问题。
|
||||
大部分编程语言使用固定大小的函数调用栈,常见的大小从64KB到2MB不等。固定大小栈会限制递归的深度,当你用递归处理大量数据时,需要避免栈溢出;除此之外,还会导致安全性问题。与此相反,Go语言使用可变栈,栈的大小按需增加(初始时很小)。这使得我们使用递归时不必考虑溢出和安全问题。
|
||||
|
||||
**练习 5.1:** 修改findlinks代码中遍历n.FirstChild链表的部分,将循环调用visit,改成递归调用。
|
||||
|
||||
|
@ -55,7 +55,7 @@ links, err := findLinks(url)
|
||||
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))
|
||||
@ -82,7 +82,7 @@ func HourMinSec(t time.Time) (hour, minute, second int)
|
||||
|
||||
虽然良好的命名很重要,但你也不必为每一个返回值都取一个适当的名字。比如,按照惯例,函数的最后一个bool类型的返回值表示函数是否运行成功,error类型的返回值代表函数的错误信息,对于这些类似的惯例,我们不必思考合适的命名,它们都无需解释。
|
||||
|
||||
如果一个函数将所有的返回值都显示的变量名,那么该函数的return语句可以省略操作数。这称之为bare return。
|
||||
如果一个函数所有的返回值都有显式的变量名,那么该函数的return语句可以省略操作数。这称之为bare return。
|
||||
|
||||
```Go
|
||||
// CountWordsAndImages does an HTTP GET request for the HTML
|
||||
@ -96,7 +96,7 @@ func CountWordsAndImages(url string) (words, images int, err error) {
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("parsing HTML: %s", err)
|
||||
return
|
||||
return
|
||||
}
|
||||
words, images = countWordsAndImages(doc)
|
||||
return
|
||||
|
@ -1,6 +1,6 @@
|
||||
### 5.4.1. 错误处理策略
|
||||
|
||||
当一次函数调用返回错误时,调用者有应该选择何时的方式处理错误。根据情况的不同,有很多处理方式,让我们来看看常用的五种方式。
|
||||
当一次函数调用返回错误时,调用者应该选择合适的方式处理错误。根据情况的不同,有很多处理方式,让我们来看看常用的五种方式。
|
||||
|
||||
首先,也是最常用的方式是传播错误。这意味着函数中某个子程序的失败,会变成该函数的失败。下面,我们以5.3节的findLinks函数作为例子。如果findLinks对http.Get的调用失败,findLinks会直接将这个HTTP错误返回给调用者:
|
||||
|
||||
@ -21,7 +21,7 @@ if err != nil {
|
||||
}
|
||||
```
|
||||
|
||||
fmt.Errorf函数使用fmt.Sprintf格式化错误信息并返回。我们使用该函数前缀添加额外的上下文信息到原始错误信息。当错误最终由main函数处理时,错误信息应提供清晰的从原因到后果的因果链,就像美国宇航局事故调查时做的那样:
|
||||
fmt.Errorf函数使用fmt.Sprintf格式化错误信息并返回。我们使用该函数添加额外的前缀上下文信息到原始错误信息。当错误最终由main函数处理时,错误信息应提供清晰的从原因到后果的因果链,就像美国宇航局事故调查时做的那样:
|
||||
|
||||
```
|
||||
genesis: crashed: no parachute: G-switch failed: bad relay orientation
|
||||
@ -31,9 +31,9 @@ genesis: crashed: no parachute: G-switch failed: bad relay orientation
|
||||
|
||||
编写错误信息时,我们要确保错误信息对问题细节的描述是详尽的。尤其是要注意错误信息表达的一致性,即相同的函数或同包内的同一组函数返回的错误在构成和处理方式上是相似的。
|
||||
|
||||
以OS包为例,OS包确保文件操作(如os.Open、Read、Write、Close)返回的每个错误的描述不仅仅包含错误的原因(如无权限,文件目录不存在)也包含文件名,这样调用者在构造新的错误信息时无需再添加这些信息。
|
||||
以os包为例,os包确保文件操作(如os.Open、Read、Write、Close)返回的每个错误的描述不仅仅包含错误的原因(如无权限,文件目录不存在)也包含文件名,这样调用者在构造新的错误信息时无需再添加这些信息。
|
||||
|
||||
一般而言,被调函数f(x)会将调用信息和参数信息作为发生错误时的上下文放在错误信息中并返回给调用者,调用者需要添加一些错误信息中不包含的信息,比如添加url到html.Parse返回的错误中。
|
||||
一般而言,被调用函数f(x)会将调用信息和参数信息作为发生错误时的上下文放在错误信息中并返回给调用者,调用者需要添加一些错误信息中不包含的信息,比如添加url到html.Parse返回的错误中。
|
||||
|
||||
让我们来看看处理错误的第二种策略。如果错误的发生是偶然性的,或由不可预知的问题导致的。一个明智的选择是重新尝试失败的操作。在重试时,我们需要限制重试的时间间隔或重试的次数,防止无限制的重试。
|
||||
|
||||
@ -118,6 +118,6 @@ if err != nil {
|
||||
os.RemoveAll(dir) // ignore errors; $TMPDIR is cleaned periodically
|
||||
```
|
||||
|
||||
尽管os.RemoveAll会失败,但上面的例子并没有做错误处理。这是因为操作系统会定期的清理临时目录。正因如此,虽然程序没有处理错误,但程序的逻辑不会因此受到影响。我们应该在每次函数调用后,都养成考虑错误处理的习惯,当你决定忽略某个错误时,你应该在清晰的记录下你的意图。
|
||||
尽管os.RemoveAll会失败,但上面的例子并没有做错误处理。这是因为操作系统会定期的清理临时目录。正因如此,虽然程序没有处理错误,但程序的逻辑不会因此受到影响。我们应该在每次函数调用后,都养成考虑错误处理的习惯,当你决定忽略某个错误时,你应该清晰地写下你的意图。
|
||||
|
||||
在Go中,错误处理有一套独特的编码风格。检查某个子函数是否失败后,我们通常将处理失败的逻辑代码放在处理成功的代码之前。如果某个错误会导致函数返回,那么成功时的逻辑代码不应放在else语句块中,而应直接放在函数体中。Go中大部分函数的代码结构几乎相同,首先是一系列的初始检查,防止错误发生,之后是函数的实际逻辑。
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
在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操作的函数都会面临出现错误的可能,只有没有经验的程序员才会相信读写操作不会失败,即使是简单的读写。因此,当本该可信的操作出乎意料的失败后,我们必须弄清楚导致失败的原因。
|
||||
|
||||
@ -31,9 +31,9 @@ fmt.Printf("%v", err)
|
||||
|
||||
在Go中,函数运行失败时会返回错误信息,这些错误信息被认为是一种预期的值而非异常(exception),这使得Go有别于那些将函数运行失败看作是异常的语言。虽然Go有各种异常机制,但这些机制仅被使用在处理那些未被预料到的错误,即bug,而不是那些在健壮程序中应该被避免的程序错误。对于Go的异常机制我们将在5.9介绍。
|
||||
|
||||
Go这样设计的原因是由于对于某个应该在控制流程中处理的错误而言,将这个错误以异常的形式抛出会混乱对错误的描述,这通常会导致一些糟糕的后果。当某个程序错误被当作异常处理后,这个错误会将堆栈根据信息返回给终端用户,这些信息复杂且无用,无法帮助定位错误。
|
||||
Go这样设计的原因是由于对于某个应该在控制流程中处理的错误而言,将这个错误以异常的形式抛出会混乱对错误的描述,这通常会导致一些糟糕的后果。当某个程序错误被当作异常处理后,这个错误会将堆栈跟踪信息返回给终端用户,这些信息复杂且无用,无法帮助定位错误。
|
||||
|
||||
正因此,Go使用控制流机制(如if和return)处理异常,这使得编码人员能更多的关注错误处理。
|
||||
正因此,Go使用控制流机制(如if和return)处理错误,这使得编码人员能更多的关注错误处理。
|
||||
|
||||
{% include "./ch5-04-1.md" %}
|
||||
|
||||
|
@ -45,7 +45,7 @@
|
||||
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
|
||||
@ -84,7 +84,7 @@ func endElement(n *html.Node) {
|
||||
}
|
||||
```
|
||||
|
||||
上面的代码利用fmt.Printf的一个小技巧控制输出的缩进。`%*s`中的`*`会在字符串之前填充一些空格。在例子中,每次输出会先填充`depth*2`数量的空格,再输出"",最后再输出HTML标签。
|
||||
上面的代码利用fmt.Printf的一个小技巧控制输出的缩进。`%*s`中的`*`会在字符串之前填充一些空格。在例子中,每次输出会先填充`depth*2`数量的空格,再输出"",最后再输出HTML标签。
|
||||
|
||||
如果我们像下面这样调用forEachNode:
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
本节,将介绍Go词法作用域的一个陷阱。请务必仔细的阅读,弄清楚发生问题的原因。即使是经验丰富的程序员也会在这个问题上犯错误。
|
||||
|
||||
考虑这个样一个问题:你被要求首先创建一些目录,再将目录删除。在下面的例子中我们用函数值来完成删除操作。下面的示例代码需要引入os包。为了使代码简单,我们忽略了所有的异常处理。
|
||||
考虑这样一个问题:你被要求首先创建一些目录,再将目录删除。在下面的例子中我们用函数值来完成删除操作。下面的示例代码需要引入os包。为了使代码简单,我们忽略了所有的异常处理。
|
||||
|
||||
```Go
|
||||
var rmdirs []func()
|
||||
|
@ -1,6 +1,6 @@
|
||||
## 5.6. 匿名函数
|
||||
|
||||
拥有函数名的函数只能在包级语法块中被声明,通过函数字面量(function literal),我们可绕过这一限制,在任何表达式中表示一个函数值。函数字面量的语法和函数声明相似,区别在于func关键字后没有函数名。函数值字面量是一种表达式,它的值被成为匿名函数(anonymous function)。
|
||||
拥有函数名的函数只能在包级语法块中被声明,通过函数字面量(function literal),我们可绕过这一限制,在任何表达式中表示一个函数值。函数字面量的语法和函数声明相似,区别在于func关键字后没有函数名。函数值字面量是一种表达式,它的值被称为匿名函数(anonymous function)。
|
||||
|
||||
函数字面量允许我们在使用函数时,再定义它。通过这种技巧,我们可以改写之前对strings.Map的调用:
|
||||
|
||||
@ -30,7 +30,7 @@ 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程序员也把函数值叫做闭包。
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
## 5.7. 可变参数
|
||||
|
||||
参数数量可变的函数称为为可变参数函数。典型的例子就是fmt.Printf和类似函数。Printf首先接收一个的必备参数,之后接收任意个数的后续参数。
|
||||
参数数量可变的函数称为可变参数函数。典型的例子就是fmt.Printf和类似函数。Printf首先接收一个必备的参数,之后接收任意个数的后续参数。
|
||||
|
||||
在声明可变参数函数时,需要在参数列表的最后一个参数类型之前加上省略符号“...”,这表示该函数会接收任意数量的该类型参数。
|
||||
|
||||
@ -23,7 +23,7 @@ fmt.Println(sum(3)) // "3"
|
||||
fmt.Println(sum(1, 2, 3, 4)) // "10"
|
||||
```
|
||||
|
||||
在上面的代码中,调用者隐式的创建一个数组,并将原始参数复制到数组中,再把数组的一个切片作为参数传给被调函数。如果原始参数已经是切片类型,我们该如何传递给sum?只需在最后一个参数后加上省略符。下面的代码功能与上个例子中最后一条语句相同。
|
||||
在上面的代码中,调用者隐式的创建一个数组,并将原始参数复制到数组中,再把数组的一个切片作为参数传给被调用函数。如果原始参数已经是切片类型,我们该如何传递给sum?只需在最后一个参数后加上省略符。下面的代码功能与上个例子中最后一条语句相同。
|
||||
|
||||
```Go
|
||||
values := []int{1, 2, 3, 4}
|
||||
|
@ -103,11 +103,9 @@ func lookup(key string) int {
|
||||
<u><i>gopl.io/ch5/trace</i></u>
|
||||
```Go
|
||||
func bigSlowOperation() {
|
||||
defer trace("bigSlowOperation")() // don't forget the
|
||||
extra parentheses
|
||||
defer trace("bigSlowOperation")() // don't forget the extra parentheses
|
||||
// ...lots of work…
|
||||
time.Sleep(10 * time.Second) // simulate slow
|
||||
operation by sleeping
|
||||
time.Sleep(10 * time.Second) // simulate slow operation by sleeping
|
||||
}
|
||||
func trace(msg string) func() {
|
||||
start := time.Now()
|
||||
@ -169,8 +167,7 @@ for _, filename := range filenames {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close() // NOTE: risky; could run out of file
|
||||
descriptors
|
||||
defer f.Close() // NOTE: risky; could run out of file descriptors
|
||||
// ...process f…
|
||||
}
|
||||
```
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user