拥有函数名的函数只能在包级语法块中被声明,通过函数字面量(function literal),我们可绕过这一限制,在任何表达式中表示一个函数值。函数字面量的语法和函数声明相似,区别在于func关键字后没有函数名。函数值字面量是一种表达式,它的值被称为匿名函数(anonymous function)。
函数字面量允许我们在使用函数时,再定义它。通过这种技巧,我们可以改写之前对strings.Map的调用:
Copy strings. Map ( func (r rune ) rune { return r + 1 }, "HAL-9000" )
更为重要的是,通过这种方式定义的函数可以访问完整的词法环境(lexical environment),这意味着在函数中定义的内部函数可以引用该函数的变量,如下例所示:
gopl.io/ch5/squares
Copy // squares返回一个匿名函数。
// 该匿名函数每次被调用时都会返回下一个数的平方。
func squares () func () int {
var x int
return func () int {
x ++
return x * x
}
}
func main () {
f := squares ()
fmt. Println ( f ()) // "1"
fmt. Println ( f ()) // "4"
fmt. Println ( f ()) // "9"
fmt. Println ( f ()) // "16"
}
函数squares返回另一个类型为 func() int 的函数。对squares的一次调用会生成一个局部变量x并返回一个匿名函数。每次调用时匿名函数时,该函数都会先使x的值加1,再返回x的平方。第二次调用squares时,会生成第二个x变量,并返回一个新的匿名函数。新匿名函数操作的是第二个x变量。
squares的例子证明,函数值不仅仅是一串代码,还记录了状态。在squares中定义的匿名内部函数可以访问和更新squares中的局部变量,这意味着匿名函数和squares中,存在变量引用。这就是函数值属于引用类型和函数值不可比较的原因。Go使用闭包(closures)技术实现函数值,Go程序员也把函数值叫做闭包。
通过这个例子,我们看到变量的生命周期不由它的作用域决定:squares返回后,变量x仍然隐式的存在于f中。
接下来,我们讨论一个有点学术性的例子,考虑这样一个问题:给定一些计算机课程,每个课程都有前置课程,只有完成了前置课程才可以开始当前课程的学习;我们的目标是选择出一组课程,这组课程必须确保按顺序学习时,能全部被完成。每个课程的前置课程如下:
gopl.io/ch5/toposort
Copy // prereqs记录了每个课程的前置课程
var prereqs = map [ string ][] string {
"algorithms" : { "data structures" },
"calculus" : { "linear algebra" },
"compilers" : {
"data structures" ,
"formal languages" ,
"computer organization" ,
},
"data structures" : { "discrete math" },
"databases" : { "data structures" },
"discrete math" : { "intro to programming" },
"formal languages" : { "discrete math" },
"networks" : { "operating systems" },
"operating systems" : { "data structures" , "computer organization" },
"programming languages" : { "data structures" , "computer organization" },
}
这类问题被称作拓扑排序。从概念上说,前置条件可以构成有向图。图中的顶点表示课程,边表示课程间的依赖关系。显然,图中应该无环,这也就是说从某点出发的边,最终不会回到该点。下面的代码用深度优先搜索了整张图,获得了符合要求的课程序列。
Copy func main () {
for i, course := range topoSort (prereqs) {
fmt. Printf ( " %d :\t %s \n" , i + 1 , course)
}
}
func topoSort (m map [ string ][] string ) [] string {
var order [] string
seen := make ( map [ string ] bool )
var visitAll func (items [] string )
visitAll = func (items [] string ) {
for _, item := range items {
if ! seen[item] {
seen[item] = true
visitAll (m[item])
order = append (order, item)
}
}
}
var keys [] string
for key := range m {
keys = append (keys, key)
}
sort. Strings (keys)
visitAll (keys)
return order
}
当匿名函数需要被递归调用时,我们必须首先声明一个变量(在上面的例子中,我们首先声明了 visitAll),再将匿名函数赋值给这个变量。如果不分成两部,函数字面量无法与visitAll绑定,我们也无法递归调用该匿名函数。
Copy visitAll := func (items [] string ) {
// ...
visitAll (m[item]) // compile error: undefined: visitAll
// ...
}
在topsort中,首先对prereqs中的key排序,再调用visitAll。因为prereqs映射的是切片而不是更复杂的map,所以数据的遍历次序是固定的,这意味着你每次运行topsort得到的输出都是一样的。 topsort的输出结果如下:
Copy 1: intro to programming
2: discrete math
3: data structures
4: algorithms
5: linear algebra
6: calculus
7: formal languages
8: computer organization
9: compilers
10: databases
11: operating systems
12: networks
13: programming languages
让我们回到findLinks这个例子。我们将代码移动到了links包下,将函数重命名为Extract,在第八章我们会再次用到这个函数。新的匿名函数被引入,用于替换原来的visit函数。该匿名函数负责将新连接添加到切片中。在Extract中,使用forEachNode遍历HTML页面,由于Extract只需要在遍历结点前操作结点,所以forEachNode的post参数被传入nil。
gopl.io/ch5/links
Copy // 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。
gopl.io/ch5/findlinks3
Copy // breadthFirst calls f for each item in the worklist.
// Any items returned by f are added to the worklist.
// f is called at most once for each item.
func breadthFirst (f func (item string ) [] string , worklist [] string ) {
seen := make ( map [ string ] bool )
for len (worklist) > 0 {
items := worklist
worklist = nil
for _, item := range items {
if ! seen[item] {
seen[item] = true
worklist = append (worklist, f (item) ... )
}
}
}
}
就像我们在章节3解释的那样,append的参数“f(item)...”,会将f返回的一组元素一个个添加到worklist中。
在我们网页抓取器中,元素的类型是url。crawl函数会将URL输出,提取其中的新链接,并将这些新链接返回。我们会将crawl作为参数传递给breadthFirst。
Copy func crawl (url string ) [] string {
fmt. Println (url)
list, err := links. Extract (url)
if err != nil {
log. Print (err)
}
return list
}
为了使抓取器开始运行,我们用命令行输入的参数作为初始的待访问url。
Copy func main () {
// Crawl the web breadth-first,
// starting from the command-line arguments.
breadthFirst (crawl, os.Args[ 1 :])
}
让我们从 https://golang.org 开始,下面是程序的输出结果:
Copy $ go build gopl.io/ch5/findlinks3
$ ./findlinks3 https://golang.org
https://golang.org/
https://golang.org/doc/
https://golang.org/pkg/
https://golang.org/project/
https://code.google.com/p/go-tour/
https://golang.org/doc/code.html
https://www.youtube.com/watch?v=XCsL89YtqCs
http://research.swtch.com/gotour
当所有发现的链接都已经被访问或电脑的内存耗尽时,程序运行结束。
练习5.10: 重写topoSort函数,用map代替切片并移除对key的排序代码。验证结果的正确性(结果不唯一)。
练习5.11: 现在线性代数的老师把微积分设为了前置课程。完善topSort,使其能检测有向图中的环。
练习5.12: gopl.io/ch5/outline2(5.5节)的startElement和endElement共用了全局变量depth,将它们修改为匿名函数,使其共享outline中的局部变量。
练习5.13: 修改crawl,使其能保存发现的页面,必要时,可以创建目录来保存这些页面。只保存来自原始域名下的页面。假设初始页面在golang.org下,就不要保存vimeo.com下的页面。
练习5.14: 使用breadthFirst遍历其他数据结构。比如,topoSort例子中的课程依赖关系(有向图),个人计算机的文件层次结构(树),你所在城市的公交或地铁线路(无向图)。