第5章 函数
函数可以让我们将一个语句序列打包成一个单元,然后可以从程序中其他地方多次调用,函数的机制可以让我们把一个大的工作分解成小任务。前面我们已经接触过函数,本章我们将讨论函数的更多特性
5.1 函数的声明
函数的声明包括函数名、形式参数列表、返回值列表(可省略)以及函数体
func name(parameter-list)(result-list) {
body
}
形式参数列表包含参数名称和参数类型,它们是局部变量,由参数调用者提供。返回值列表包含了返回值的变量名称和类型,如果函数没有返回值或者返回值是一个无名变量,返回值列表括号可以省略,没有返回值,函数则不会返回任何值
func hypo(x,y float64) float64 {
return math.Sqrt(x*x+y*y)
}
func main() {
fmt.Println(hypo(3,4))
}// 5
如果函数在声明时包含了参列表,那函数体必须以return语句结尾,除非函数明显无法到达结尾处,例如函数在结尾调用了Panic异常或函数存在无限循环
func hypo(x, y float64) (z float64) {
z = math.Sqrt(x*x + y*y)
return z
}
func main() {
fmt.Println(hypo(3, 4))
}
函数的多个形参或返回值的类型相同时,可以一起声明
func f(i,j,k int,s,t string) {/*...*/}
func f(i int,j int,k int, s string,t string)
func add(x int, y int) int { return x + y }
func sub(x int, y int) (z int) { z = x - y; return }
func first(x int, _ int) int { return x }
func zero(int, int) int { return 0 }
func main() {
fmt.Printf("%T\n",add)
fmt.Printf("%T\n",sub)
fmt.Printf("%T\n",first)
fmt.Printf("%T\n",zero)
}
//
func(int, int) int
func(int, int) int
func(int, int) int
func(int, int) int
我们发现上面这四个函数类型都是相同的,函数类型被称为函数的签名,如果两个函数类型(签名)相同,那么形参或返回值名称以及是否省略不影响函数签名
调用函数时须按形参声明顺序提供参数,Go语言在调用时,形参没有默认值,也无法通过任何方法可以通过参数名指定形参,因此形参名和返回值的变量名称对调用者而言没有多大意义
在函数体中,函数的形参作为局部变量,被初始化为调用者提供的值,函数的形参和有名返回值作为函数最外层的局部变量,被存储在相同的词法块中
实参通过值的方式传递,因此参数的形参时实参的拷贝,对形参进行修改不会影响时参
但是,如果实参包括引用类型,如指针、slice、map、functition、channel等类型,时参可能由于函数的间接引用被修改
我们可能会遇到一些没有函数体的声明,这表示该函数不是以Go实现的吗。这样的声明定义了函数签名
func Sin(x float64) float //implemented in assembly language
5.2 递归
函数时可以递归的,这表明函数可以直接或者间接调用自身。递归技术对很多问题而言都是强有力的,例如处理递归数据结构。在4.4节我们通过遍历二叉树来实现简单的插入排序。本章,我们再次使用它来处理HTML文件
html.Parse函数读入一组bytes.解析后,返回html.node类型的HTML页面树状结构根节点。HTML拥有很多类型的结点如text(文本),commnets(注释)类型,在下面的例子中,我们只关注 < name key = ‘value’>形式的结点
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标准输入,通过递归函数visit获得links,并打印出这些links
func main() {
doc, err := html.Parse(os.Stdin)
if err != nil {
fmt.Fprintf(os.Stderr, "findlinks1:%s\n", err)
os.Exit(1)
}
for _, link := range visit(nil, doc) {
fmt.Println(link)
}
}
visit函数遍历HTML的结点树,从每一个anchor元素的href属性获得link,将这些links存入字符串数组中,并返回这个字符串数组
func visit(links []string, n *html.Node) []string {
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的孩子结点时,visit递归的调用自身,这些孩子结点存放在FirstChild链表中
我们可以以Go的主页(golang.org)作为目标,运行findlinks
在outline函数中,我们通过递归的方式遍历整个HTML结点树,并输出数的结构。在outline内部,每遇到一个HTML元素标签,就将其入栈,并输出
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)
fmt.Println(stack)
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
outline(stack, c)
}
}
注意:outline有入栈操作,但是并没有相应的出栈操作,当outline调用自身时,被调用者接受的是stack的拷贝,被调用者对stack的元素追加操作,修改的是stack的拷贝,其可能会修改slice的底层数组甚至是申请开辟一块新的内存空间进行扩容,但是这个过程不会修改调用方的stack与其调用自身之前完全一致
正如以上这个程序运行所见,大部分HTML只需几层递归就能被处理,但仍然有些页面需要深层的递归
大部分编程语言使用固定大小的函数调用栈,常见的大小从64KB到2MB不等。固定大小栈会限制递归的深度,当你用递归处理大量数据时,需要避免栈溢出。除此之外还会导致安全性问题
与此相反,Go语言使用可变栈,栈的大小按需增加(初始时很小),这使得我们使用递归时不必考虑溢出和安全问题