5 函数
函数使我们可以将一个语句序列包装为一个单元,可以在程序中的其他地方调用它,可能调用多次。 函数对用户隐藏了其实现细节。由于所有这些原因,函数是任何编程语言的重要组成部分。
我们已经看到了许多函数。 现在,我们花时间进行更深入的讨论。本章的运行示例是一个Web爬虫,即,该Web搜索引擎的组件负责获取网页,发现其中的链接,获取由这些链接标识的页面,以及 网络爬虫为我们提供了充分的机会来探索递归,匿名函数,错误处理以及Go独有的函数方面。
5.1 函数声明
函数声明具有名称,参数列表,结果的可选列表和函数体:
func name(parameter-list) (result-list) {
body
}
参数列表指定函数参数的名称和类型,它们是由调用方提供其值或参数的局部变量。结果列表指定函数返回的值的类型。如果函数返回一个未命名的结果或 完全没有结果,括号是可选的,通常会省略。离开结果列表将完全声明一个函数,该函数不返回任何值,仅出于其作用而调用。hypot函数中,
func hypot(x, y float64) float64 {
return math.Sqrt(x*x + y*y)
}
fmt.Println(hypot(3, 4)) // "5"
x和y是声明中的参数,3和4是调用的参数,并且该函数返回float64类型值。
像参数一样,结果可以被命名,在这种情况下,每个名称都声明一个局部变量,该局部变量的类型初始化为零值。
具有结果列表的函数必须以returnstate结尾,除非执行明显不能到达函数的末尾,也许是因为该函数以对panic的调用或无限for循环而没有break。
就像我们在hypot中看到的那样,可以对相同类型的参数或结果序列进行分解,以便类型本身只能写入一次,这两个声明是等效的:
func f(i, j, k int, s, t string) { /* ... */ }
func f(i int, j int, k int, s string, t string) { /* ... */ }
这是用两个参数声明一个结果为int类型的函数的四种方法,空白标识符可用于强调参数未使用。
func add(x int, y int) int { return x + y }
func sub(x, y int) (z int) { z = x - y; return }
func first(x int, _ int) int { return x }
func zero(int, int) int { return 0 }
fmt.Printf("%T\n", add) // "func(int, int) int"
fmt.Printf("%T\n", sub) // "func(int, int) int"
fmt.Printf("%T\n", first) // "func(int, int) int"
fmt.Printf("%T\n", zero) // "func(int, int) int"
函数的类型有时称为其签名。如果两个函数具有相同的参数类型序列和相同的结果类型序列,则它们具有相同的类型或签名。参数和结果的名称不影响类型,也不影响类型 是否使用分解形式声明它们。
每个函数调用都必须按声明参数的顺序为每个参数提供一个参数.Go没有默认参数值的概念,也没有通过名称指定参数的任何方式,因此参数和结果的名称与调用者无关 除了作为文档。
参数是函数体内的局部变量,其初始值设置为调用方提供的参数。函数参数和命名结果是与函数的最外部局部变量在同一词法块中的变量。
参数是通过值传递的,因此该函数接收每个参数的副本。 对副本的修改不会影响调用者。但是,如果参数包含某种引用,例如指针,切片,映射,函数或通道,则调用者可能会受到函数对参数间接引用的变量所做的任何修改的影响 。
您可能偶尔会遇到没有主体的函数声明,这表明该函数是用Go以外的语言实现的。此类声明定义了函数签名。
package math
func Sin(x float64) float64 // implemented in assembly language
5.2 递归
递归是解决许多问题的一项强大技术,当然,它对于处理递归数据结构至关重要。在第4.4节中,我们在树上使用了递归来实现简单的插入排序。在本节中,我们将再次使用它来处理HTML文档。
下面的示例程序使用了一个非标准软件包golang.org/x/net/html,该软件包提供了HTML解析器。golang.org/x / …存储库保存了Go团队为以下应用程序设计和维护的软件包 网络,国际化文本处理,移动平台,图像处理,加密和开发人员工具。这些软件包不在标准库中,因为它们仍在开发中,或者因为大多数Go程序员很少使用它们。
下面显示了我们需要的golang.org/x/net/html API部分。html.Parse函数读取一个字节序列,对其进行解析,然后返回HTML文档树的根,即html.Node。 .HTML有几种类型的节点-文本,注释等,但是在这里,我们只关心形式的元素节点。
golang.org/x/net/html
package html
type Node struct {
Type NodeType
Data string
Attr []Attribute
FirstChild, NextSibling *Node
}
type NodeType int32
const (
ErrorNode NodeType = iota
TextNode
DocumentNode
ElementNode
CommentNode
DoctypeNode
)
type Attribute struct {
Key, Val string
}
func Parse(r io.Reader) (*Node, error)
main函数将标准输入解析为HTML,使用递归访问函数提取链接,并打印每个发现的链接:
gopl.io/ch5/findlinks1
// Findlinks1 prints the links in an HTML document read from standard input.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, "findlinks1: %v\n", err)
os.Exit(1)
}
for _, link := range visit(nil, doc) {
fmt.Println(link)
}
}
visit函数遍历HTML节点树,从每个锚元素的href属性中提取链接,将链接附加到字符串切片中,并返回结果切片:
// visit appends to links each link found in n and returns the result.
func visit(links []string, n *html.Node) []string {
if n.Type == html.ElementNode && n.Data == "a" {
for _, a := range n.Attr {
if a.Key == "href" {
links = append(links, a.Val)
}
}
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
links = visit(links, c)
}
return links
}
要使第n个节点的树下降,访问会递归地调用每个孩子的n个孩子,这些孩子都保存在FirstChild链接列表中。
让我们在Go主页上运行findlinks,将输出offetch(第1.5节)传递到输入offindlinks。为简洁起见,我们对输出进行了少许编辑。
$ go build gopl.io/ch1/fetch$
go build gopl.io/ch5/findlinks1$
./fetch https://golang.org | ./findlinks1
#
/doc/
/pkg/
/help/
/blog/
http://play.golang.org/
//tour.golang.org/
https://golang.org/dl/
//blog.golang.org/
/LICENSE/
doc/tos.html
http://www.google.com/intl/en/policies/privacy/
请注意页面中出现的各种形式的链接。稍后,我们将了解如何相对于baseURL https://golang.org解析链接,以创建绝对URL。
下一个程序在HTML节点树上使用递归,以轮廓显示树的结构。 遇到每个元素时,它将元素的标签压入堆栈,然后打印堆栈。
gopl.io/ch5/outline
func main() {
doc, err := html.Parse(os.Stdin)
if err != nil {
fmt.Fprintf(os.Stderr, "outline: %v\n", err)
os.Exit(1)
}
outline(nil, doc)
}
func outline(stack []string, n *html.Node) {
if n.Type == html.ElementNode {
stack = append(stack, n.Data) // push tag
fmt.Println(stack)
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
outline(stack, c)
}
}
请注意一个微妙之处:尽管大纲将元素“推”到堆栈上,但没有相应的弹出窗口。当大纲递归调用自身时,被调用方会收到堆栈的副本。尽管被调用方可以将元素追加到此slice上,从而修改其下层数组,甚至可能分配一个 新数组,它不会修改调用方可见的初始元素,因此当函数返回时,调用方的堆栈与调用前相同。
这是https://golang.org的概述,为简洁起见再次进行了编辑:
$ go build gopl.io/ch5/outline
$ ./fetch https://golang.org | ./outline
[html]
[html head]
[html head meta]
[html head title]
[html head link]
[html body]
[html body div]
[html body div]
[html body div div]
[html body div div form]
[html body div div form div]
[html body div div form div a]
...
正如您通过试验大纲所看到的那样,大多数HTML文档只能进行少量递归处理,但是构建需要深度递归的病理网页并不难。
许多编程语言实现都使用固定大小的函数调用堆栈。 固定大小的堆栈通常限制从64KB到2MB的大小,因此递归深度受到限制,因此在递归遍历大型数据结构时必须小心避免堆栈溢出;固定大小的堆栈甚至可能会带来安全风险。 ,典型的Go实现使用可变大小的堆栈,这些堆栈从小开始并根据需要增长,直至达到千兆字节的限制。这使我们可以安全地使用递归,而不必担心溢出。
Exercise omitted
未完待续…