注:学习《Go语言圣经》笔记,PDF点击下载,建议看书。
Go语言小白学习笔记,书上的内容照搬,大佬看了勿喷,以后熟悉了会总结成自己的读书笔记。
函数可以是递归的, 这意味着函数可以直接或间接的调用自身。 对许多问题而言, 递归是一种强有力的技术, 例如处理递归的数据结构。 在4.4节, 我们通过遍历二叉树来实现简单的插入排序, 在本章节, 我们再次使用它来处理HTML文件。
下文的示例代码使用了非标准包 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’ >形式的结点。
visit函数遍历HTML的节点树, 从每一个anchor元素的href属性获得link,将这些links存入字符串数组中, 并返回这个字符串数组。
为了遍历结点n的所有后代结点, 每次遇到n的孩子结点时, visit递归的调用自身。 这些孩子结点存放在FirstChild链表中。
让我们以Go的主页( golang.org) 作为目标, 运行findlinks。 我们以fetch( 1.5章) 的输出作为findlinks的输入。 下面的输出做了简化处理。
注意在页面中出现的链接格式, 在之后我们会介绍如何将这些链接, 根据根路径(https://golang.org ) 生成可以直接访问的url。
在函数outline中, 我们通过递归的方式遍历整个HTML结点树, 并输出树的结构。 在outline内部, 每遇到一个HTML元素标签, 就将其入栈, 并输出。
有一点值得注意: outline有入栈操作, 但没有相对应的出栈操作。 当outline调用自身时, 被调用者接收的是stack的拷贝。 被调用者的入栈操作, 修改的是stack的拷贝, 而不是调用者的stack,因对当函数返回时,调用者的stack并未被修改。
正如你在上面实验中所见, 大部分HTML页面只需几层递归就能被处理, 但仍然有些页面需要深层次的递归。
大部分编程语言使用固定大小的函数调用栈, 常见的大小从64KB到2MB不等。 固定大小栈会限制递归的深度, 当你用递归处理大量数据时, 需要避免栈溢出; 除此之外, 还会导致安全性问题。 与相反,Go语言使用可变栈, 栈的大小按需增加(初始时很小)。 这使得我们使用递归时不必考虑溢出和安全问题。
在Go中, 一个函数可以返回多个值。 我们已经在之前例子中看到, 许多标准库中的函数返回2个值, 一个是期望得到的返回值, 另一个是函数出错时的错误信息。 下面的例子会展示如何编写多返回值的函数。
下面的程序是findlinks的改进版本。 修改后的findlinks可以自己发起HTTP请求, 这样我们就不必再运行fetch。 因为HTTP请求和解析操作可能会失败, 因此findlinks声明了2个返回值: 链接列表和错误信息。 一般而言, HTML的解析器可以处理HTML页面的错误结点, 构造出HTML页面结构, 所以解析HTML很少失败。 这意味着如果findlinks函数失败了, 很可能是由于I/O的错误导致的。
gopl.io/ch5/findlinks2
package main
import (
"fmt"
"html"
"net/http"
"os"
)
func main() {
for _, url := range os.Args[1:] {
links, err := findLinks(url)
if err != nil {
fmt.Fprintf(os.Stderr, "findlinks2: %v\n", err)
continue
}
for _, link := range links {
fmt.Println(link)
}
}
}
// findLinks performs an HTTP GET request for url, parses the
// response as HTML, and extracts and returns the links.
func findLinks(url string) ([]string, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return nil, fmt.Errorf("getting %s: %s", url, resp.Status)
}
doc, err := html.Parse(resp.Body)
resp.Body.Close()
if err != nil {
return nil, fmt.Errorf("parsing %s as HTML: %v", url, err)
}
return visit(nil, doc), nil
}
在findlinks中, 有4处return语句, 每一处return都返回了一组值。 前三处return, 将http和html包中的错误信息传递给findlinks的调用者。 第一处return直接返回错误信息, 其他两处通过fmt.Errorf( §7.8) 输出详细的错误信息。 如果findlinks成功结束, 最后的return语句将一组解析获得的连接返回给用户。
在finallinks中, 我们必须确保resp.Body被关闭, 释放网络资源。 虽然Go的垃圾回收机制会回收不被使用的内存, 但是这不包括操作系统层面的资源, 比如打开的文件、 网络连接。 因此我们必须显式的释放这些资源。
一个函数内部可以将另一个有多返回值的函数作为返回值, 下面的例子展示了与findLinks有相同功能的函数, 两者的区别在于下面的例子先输出参数:
当你调用接受多参数的函数时, 可以将一个返回多参数的函数作为该函数的参数。 虽然这很少出现在实际生产代码中, 但这个特性在debug时很方便, 我们只需要一条语句就可以输出所有的返回值。 下面的代码是等价的:
虽然良好的命名很重要, 但你也不必为每一个返回值都取一个适当的名字。 比如, 按照惯例, 函数的最后一个bool类型的返回值表示函数是否运行成功, error类型的返回值代表函数的错误信息, 对于这些类似的惯例, 我们不必思考合适的命名, 它们都无需解释。
如果一个函数将所有的返回值都显示的变量名, 那么该函数的return语句可以省略操作数。 这称之为bare return。
当一个函数有多处return语句以及许多返回值时, bare return 可以减少代码的重复, 但是使得代码难以被理解。 举个例子, 如果你没有仔细的审查代码, 很难发现前2处return等价于return 0,0,err( Go会将返回值 words和images在函数体的开始处, 根据它们的类型, 将其初始化为0) , 最后一处return等价于 return words, image, nil。 基于以上原因, 不宜过度使用bare return。
...
:拥有函数名的函数只能在包级语法块中被声明, 通过函数字面量( function literal) , 我们可绕过这一限制, 在任何表达式中表示一个函数值。 函数字面量的语法和函数声明相似, 区别在于func关键字后没有函数名。 函数值字面量是一种表达式, 它的值被称为匿名函数( anonymous function) 。
更为重要的是, 通过这种方式定义的函数可以访问完整的词法环境( lexical environment) ,这意味着在函数中定义的内部函数可以引用该函数的变量, 如下例所示:
函数squares返回另一个类型为 func() int 的函数。 对squares的一次调用会生成一个局部变量x并返回一个匿名函数。 每次调用时匿名函数时, 该函数都会先使x的值加1, 再返回x的平方。 第二次调用squares时, 会生成第二个x变量, 并返回一个新的匿名函数。 新匿名函数操作的是第二个x变量。
squares的例子证明, 函数值不仅仅是一串代码, 还记录了状态。 在squares中定义的匿名内部函数可以访问和更新squares中的局部变量, 这意味着匿名函数和squares中, 存在变量引用。 这就是函数值属于引用类型和函数值不可比较的原因。 Go使用闭包( closures) 技术实现函数值, Go程序员也把函数值叫做闭包。
通过这个例子, 我们看到变量的生命周期不由它的作用域决定: squares返回后, 变量x仍然隐式的存在于f中。
接下来, 我们讨论一个有点学术性的例子, 考虑这样一个问题: 给定一些计算机课程, 每个课程都有前置课程, 只有完成了前置课程才可以开始当前课程的学习; 我们的目标是选择出一组课程, 这组课程必须确保按顺序学习时, 能全部被完成。 每个课程的前置课程如下:
这类问题被称作拓扑排序。 从概念上说, 前置条件可以构成有向图。 图中的顶点表示课程,边表示课程间的依赖关系。 显然, 图中应该无环, 这也就是说从某点出发的边, 最终不会回到该点。 下面的代码用深度优先搜索了整张图, 获得了符合要求的课程序列。
当匿名函数需要被递归调用时, 我们必须首先声明一个变量( 在上面的例子中, 我们首先声明了 visitAll) , 再将匿名函数赋值给这个变量。 如果不分成两部, 函数字面量无法与visitAll绑定, 我们也无法递归调用该匿名函数。
在topsort中, 首先对prereqs中的key排序, 再调用visitAll。 因为prereqs映射的是切片而不是更复杂的map, 所以数据的遍历次序是固定的, 这意味着你每次运行topsort得到的输出都是一样的。 topsort的输出结果如下:
让我们回到findLinks这个例子。 我们将代码移动到了links包下, 将函数重命名为Extract, 在第八章我们会再次用到这个函数。 新的匿名函数被引入, 用于替换原来的visit函数。 该匿名函数负责将新连接添加到切片中。 在Extract中, 使用forEachNode遍历HTML页面, 由于Extract只需要在遍历结点前操作结点, 所以forEachNode的post参数被传入nil。
gopl.io/ch5/links
// Package links provides a link-extraction function.
package links
import (
"fmt"
"net/http"
"golang.org/x/net/html"
)
// Extract makes an HTTP GET request to the specified URL, parses
// the response as HTML, and returns the links in the HTML document.
func Extract(url string) ([]string, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return nil, fmt.Errorf("getting %s: %s", url, resp.Status)
}
doc, err := html.Parse(resp.Body)
resp.Body.Close()
if err != nil {
return nil, fmt.Errorf("parsing %s as HTML: %v", url, err)
}
var links []string
visitNode := func(n *html.Node) {
if n.Type == html.ElementNode && n.Data == "a" {
for _, a := range n.Attr {
if a.Key != "href" {
continue
}
link, err := resp.Request.URL.Parse(a.Val)
if err != nil {
continue // ignore bad URLs
}
links = append(links, link.String())
}
}
}
forEachNode(doc, visitNode, nil)
return links, nil
}
上面的代码对之前的版本做了改进, 现在links中存储的不是href属性的原始值, 而是通过resp.Request.URL解析后的值。 解析后, 这些连接以绝对路径的形式存在, 可以直接被http.Get访问。
网页抓取的核心问题就是如何遍历图。 在topoSort的例子中, 已经展示了深度优先遍历, 在网页抓取中, 我们会展示如何用广度优先遍历图。 在第8章, 我们会介绍如何将深度优先和广度优先结合使用。
下面的函数实现了广度优先算法。 调用者需要输入一个初始的待访问列表和一个函数f。 待访问列表中的每个元素被定义为string类型。 广度优先算法会为每个元素调用一次f。 每次f执行完毕后, 会返回一组待访问元素。 这些元素会被加入到待访问列表中。 当待访问列表中的所有元素都被访问后, breadthFirst函数运行结束。 为了避免同一个元素被访问两次, 代码中维护了一个map。
就像我们在章节3解释的那样, append的参数“f(item)…”, 会将f返回的一组元素一个个添加到worklist中。
在我们网页抓取器中, 元素的类型是url。 crawl函数会将URL输出, 提取其中的新链接, 并将这些新链接返回。 我们会将crawl作为参数传递给breadthFirst。
当所有发现的链接都已经被访问或电脑的内存耗尽时, 程序运行结束。
, 该函数会给调用者返回一个错误( error) 。 在soleTitle内部处理时, 如果检测到有多个
, 会调用panic, 阻止函数继续递归, 并将特殊类型bailout作为panic的参数。