gopl-zh.github.com/ch5/ch5-05.md
2021-04-26 08:04:43 +08:00

133 lines
4.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## 5.5. 函数值
在Go中函数被看作第一类值first-class values函数像其他值一样拥有类型可以被赋值给其他变量传递给函数从函数返回。对函数值function value的调用类似函数调用。例子如下
```Go
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错误
```Go
var f func(int) int
f(3) // 此处f的值为nil, 会引起panic错误
```
函数值可以与nil比较
```Go
var f func(int) int
if f != nil {
f(3)
}
```
但是函数值之间是不可比较的也不能用函数值作为map的key。
函数值使得我们不仅仅可以通过数据来参数化函数亦可通过行为。标准库中包含许多这样的例子。下面的代码展示了如何使用这个技巧。strings.Map对字符串中的每个字符调用add1函数并将每个add1函数的返回值组成一个新的字符串返回给调用者。
```Go
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"
```
5.2节的findLinks函数使用了辅助函数visit遍历和操作了HTML页面的所有结点。使用函数值我们可以将遍历结点的逻辑和操作结点的逻辑分离使得我们可以复用遍历的逻辑从而对结点进行不同的操作。
<u><i>gopl.io/ch5/outline2</i></u>
```Go
// 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)
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
forEachNode(c, pre, post)
}
if post != nil {
post(n)
}
}
```
该函数接收2个函数作为参数分别在结点的孩子被访问前和访问后调用。这样的设计给调用者更大的灵活性。举个例子现在我们有startElement和endElement两个函数用于输出HTML元素的开始标签和结束标签`<b>...</b>`
```Go
var depth int
func startElement(n *html.Node) {
if n.Type == html.ElementNode {
fmt.Printf("%*s<%s>\n", depth*2, "", n.Data)
depth++
}
}
func endElement(n *html.Node) {
if n.Type == html.ElementNode {
depth--
fmt.Printf("%*s</%s>\n", depth*2, "", n.Data)
}
}
```
上面的代码利用fmt.Printf的一个小技巧控制输出的缩进。`%*s`中的`*`会在字符串之前填充一些空格。在例子中,每次输出会先填充`depth*2`数量的空格,再输出""最后再输出HTML标签。
如果我们像下面这样调用forEachNode
```Go
forEachNode(doc, startElement, endElement)
```
与之前的outline程序相比我们得到了更加详细的页面结构
```
$ go build gopl.io/ch5/outline2
$ ./outline2 http://gopl.io
<html>
<head>
<meta>
</meta>
<title>
</title>
<style>
</style>
</head>
<body>
<table>
<tbody>
<tr>
<td>
<a>
<img>
</img>
...
```
**练习 5.7** 完善startElement和endElement函数使其成为通用的HTML输出器。要求输出注释结点文本结点以及每个元素的属性< a href='...'>)。使用简略格式输出没有孩子结点的元素(即用`<img/>`代替`<img></img>`。编写测试验证程序输出的格式正确。详见11章
**练习 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")的返回值。
```Go
func expand(s string, f func(string) string) string
```