第五章

  • 函数声明
  • 递归
  • 多返回值
  • 错误
  • 函数变量
  • 匿名函数
  • 变长函数
  • 延迟函数调用
  • 宕机
  • 恢复

练习5.1

改变findlinks程序,使用递归调用visit(而不是循环)遍历n.FirstChild链表

  • 可怕的递归ヽ(*。>Д<)o゜可怕
  • 循环还挺好理解的,可是改成递归真。。。不好理解
  • 本题就修改一句就好了
1
return visit(visit(links, n.FirstChild), n.NextSibling)

练习5.2

写一个函数,用于统计HTML文档树内所有的元素个数,如p,div,span等

  • 将传入的slice修改为map,其他不变
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
"fmt"
"os"

"golang.org/x/net/html"
)

func main() {
doc, err := html.Parse(os.Stdin)
if err != nil {
fmt.Fprintf(os.Stderr, "findelems: %v\n", err)
os.Exit(1)
}
elements := map[string]int{}
visit(elements, doc)
for elem, count := range elements {
fmt.Printf("%s\t%d\n", elem, count)
}
}

func visit(e map[string]int, n *html.Node) {
if n.Type == html.ElementNode {
e[n.Data]++
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
visit(e, c)
}
}

练习5.3

写一个函数,用于输出HTML文档树种所有文本节点的内容.但不包括<script><style>元素,因为这些内容在web浏览器中是不可见的

  • 还是差不多,递归读取到每一个节点,选择性输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
"fmt"
"os"
"golang.org/x/net/html"
)

func main() {
doc, err := html.Parse(os.Stdin)
if err != nil {
fmt.Fprintf(os.Stderr, "findtexts: %v\n", err)
os.Exit(1)
}
visit(doc)
}

func visit(n *html.Node) {
if n != nil && n.Type == html.ElementNode {
if n.Data == "script" || n.Data == "style" {
return
}
}
if n.Type == html.TextNode {
fmt.Println(n.Data)
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
visit(c)
}
}

练习5.4:

扩展visit函数,使之能够获得到其他种类的链接地址,比如图片、脚本或样式表的链接

  • 用一个字典匹配所有的类型,先匹配n.Data,在遍历属性得到需要的地址

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
     var filter = map[string]string{
    "a": "href",
    "img": "src",
    "script": "src",
    }

    func visit(links []string, n *html.Node) []string {
    for k, v := range filter {
    if n.Type == html.ElementNode && n.Data == k {
    for _, a := range n.Attr {
    if a.Key == v {
    links = append(links, a.Val)
    }
    }
    }
    }
    for c := n.FirstChild; c != nil; c = c.NextSibling {
    links = visit(links, c)
    }
    return links
    }

练习5.5:

实现函数countWordsAndImages

  • ElementNode、TextNode、ComentNode、DoctypeNode、ErrorNode
  • 判断为TextNode的时候统计字符个数,n.Data为img的时候图片数目累加
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func countWordsAndImages(n *html.Node) (words, images int) {
if n.Type == html.TextNode{
scanner := bufio.NewScanner(strings.NewReader(n.Data))
scanner.Split(bufio.ScanWords)
for scanner.Scan() {
words++
}
}
if n.Type == html.ElementNode && n.Data == "img" {
images++
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
ws, is := countWordsAndImages(c)
words += ws
images += is
}
return words, images
}

练习5.6:

修改gopl.io/ch3/surface中的函数corner,以使用命名的结果以及裸返回语句

  • 最简单的概念题了,返回参数规范化
  • return的时候忽略后面的语句,不过我想我写代码的时候可能不会选择忽略
1
2
3
4
5
6
7
8
9
10
func corner(i, j int) (sx, sy float64) {
x := xyrange * (float64(i)/cells - 0.5)
y := xyrange * (float64(j)/cells - 0.5)

z := f(x, y)

sx = width/2 + (x-y)*cos30*xyscale
sy = height/2 + (x+y)*sin30*xyscale - z*zscale
return
}

练习5.7:

开发startElement和endElement函数并应用到一个普通的HTML输出代码中.输出注释节点、文本节点和所有元素属性(<a href='...'>).当一个元素没有子节点时,使用简短的形式,比如<img/>而不是<img></img>.写一个测试程序保证输出可以正确解析

  • 抱歉,我感觉挺复杂

练习5.8:

修改forEachNode使得pre和post函数返回一个布尔型的结果来确定遍历是否继续下去.使用它写一个函数ElementByID,该函数使用下面的函数签名并且找到第一个符合id属性的HTML元素.函数在找到符合条件的元素时应该尽快停止遍历.func 使用它写一个函数ElementByID(doc *html.Node, id string) *html.Node

  • 终止递归,每次执行pre、post的时候选择判断返回值,如果为真表示找到直接进行return
  • 因为传递给forEachNode的是html.Node的指针,当return的时候即为找到的第一个id,如果遍历完了还没有,最终返回值为n.FirstChild会为nil
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func ElementByID(doc *html.Node, id string) *html.Node {
var elem *html.Node
forEachNode(doc,
func(n *html.Node) bool {
if n.Type == html.ElementNode {
for _, a := range n.Attr {
if a.Key == "id" && a.Val == id {
elem = n
return false
}
}
}
return true
},
nil)
return elem
}

练习5.9:

写一个函数expand(s string,f func(string)string)string,该函数替换参数s中每一个子字符串”$foo”为f(“foo”)的返回值

  • 感觉应该是类似于${foo}这样,否则$后面会有很长的字符串,不利于模板进行分割
1
2
3
4
5
6
7
8
package main

import "strings"

func Expand(s string, f func(string) string) string {
ret := strings.Replace(s, "$foo", f("foo"), 1024)
return ret
}

练习5.10:

重写topSort以使用map代替slice并去掉开头的排序.结果不是唯一的,验证这个结果是合法的拓扑排序

  • 跳过┑( ̄Д  ̄)┍

练习5.11:

现在有”线性代数”这门课程,它的先决课程是”微积分”.拓展topSort以函数输出结果

  • 跳过┑( ̄Д  ̄)┍

练习5.12:

5.5节(gopl.io/ch5/outline2)的startElement和endElement函数共享一个全局变量depth.把它们变为匿名函数以共享outline函数的一个局部变量

  • 跳过┑( ̄Д  ̄)┍

练习5.13:

修改crawl函数保存找到的页面,根据需要创建目录.不要保存不同域名下的页面.比如本来的页面来自golang.org,那么就把它们保存下来但是不要保存vimeo.com下的页面

  • 跳过┑( ̄Д  ̄)┍

练习5.14:

使用广度优先遍历搜索一个不同的拓扑结构.比如,你可以借鉴拓扑排序的例子里的课程依赖关系,计算机文件系统的分层结构,或者从当前城市的官网上下载公共汽车或者地铁的路线图.

  • 跳过┑( ̄Д  ̄)┍

练习5.15:

模仿sum写两个变长函数max和min.当不带任何参数调用这些函数的时候应该怎么应对?编写类似函数的变种,要求至少需要一个参数

  • 当缺少参数的时候直接panic
  • 比较的时候直接使用math模块的最大值最小值使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import "math"

func Max(in ...int) int {
if len(in) == 0 {
panic("At least one element")
}
ret := math.MinInt64
for _, v := range in {
if v > ret {
ret = v
}
}
return ret
}

func Min(in ...int) int {
if len(in) == 0 {
panic("At least one element")
}

ret := math.MaxInt64
for _, v := range in {
if v < ret {
ret = v
}
}
return ret
}

练习5.16:

写一个变长版本的strings.Join函数

  • 可以直接使用strings.Join返回
  • 选择使用bytes.Buffer模块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"strings"
"bytes"
"fmt"
)

func Join(in ...string) string {
return strings.Join(in, "")
}

func Join2(in ...string) string {
var buf bytes.Buffer
for _, v := range in {
buf.Write([]byte(v))
}
return buf.String()
}

func main() {
fmt.Println(Join2("a", "b", "c"))
}

练习5.17:

写一个变长函数ElementsByTagname,已知一个HTML节点树和零个或多个名字,返回所有符合给出名字的元素

  • 衔接前面的题目,遍历判断一下即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "golang.org/x/net/html"

func ElementsByTagName(doc *html.Node, name ...string) []*html.Node {
if len(name) == 0 {
return nil
}
var visit func(elems []*html.Node, n *html.Node) []*html.Node
visit = func(elems []*html.Node, n *html.Node) []*html.Node {
for _, tag := range name {
if n.Type == html.ElementNode && n.Data == tag {
elems = append(elems, n)
}
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
elems = visit(elems, c)
}
return elems
}
return visit(nil, doc)
}

练习5.18:

不改变原本的行为,重写fetch函数以使用defer语句关闭打开的可写的文件

  • 基本不变,直接将原有的写法放置到嵌套的defer函数内部,这样关闭失败的时候同样的逻辑会写入err
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func fetch(url string) (filename string, n int64, err error) {
resp, err := http.Get(url)
if err != nil {
return "", 0, err
}
defer resp.Body.Close()

local := path.Base(resp.Request.URL.Path)
if local == "/" {
local = "index.html"
}
f, err := os.Create(local)
if err != nil {
return "", 0, err
}
defer func() {
if closeErr := f.Close(); err == nil {
err = closeErr
}
}()
n, err = io.Copy(f, resp.Body)
return local, n, err
}

练习5.19:

使用panic和recover写一个函数,它没有return语句,但是能够返回一个非零的值

  • 利用defer语句的作用域和函数同级,在defer语句中执行recover再设置函数返回值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

func NoReturn() (r int) {
defer func() {
if p := recover(); p != nil {
r = 1
} else {
r = 2
}
}()
panic(1)
}

func main() {
fmt.Println(NoReturn())
}