当前位置: 首页 > 工具软件 > Bible Reader > 使用案例 >

Go语言圣经 - 第5章 函数 - 5.1 函数的声明 & 5.2 递归

窦哲彦
2023-12-01

第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语言使用可变栈,栈的大小按需增加(初始时很小),这使得我们使用递归时不必考虑溢出和安全问题

 类似资料: