函数声明 基本语法 | 每个函数声明,都包含了一个函数名 、一个形参列表 、一个可选的返回列表 、一个函数体 |
func name(parameter-list) (return-list) { ??????? body } | |
形参与返回值 | 形参列表,指定了一组变量的参数名和参数类型 |
形参是函数的局部变量,这些形参都由调用者提供的实参传递而来 | |
返回列表,指定了函数返回值的类型 | |
当函数返回一个未命名的返回值,或者函数没有返回值的时候,返回列表的圆括号可以省略 | |
如果一个函数既没有返回列表,也没有任何返回值,那么这个函数的目的是调用函数所带来的附加效果 | |
代码示例 | 在下面的 hypot 函数中: func? name( x , y float64 ) float64 { ??????? return? math.Sqrt( x*x? +? y*y ) } fmt.Println( hypot( 3 , 4 ) )??? // " 5 " x 和 y 是函数声明中的形参,3 和 4 是调用函数时的实参,并且函数返回一个类型为 float64 的值 |
形参与返回值 | 返回值可以像形参一样命名; 每个命名的返回值会声明为一个局部变量,并根据变量类型初始化为对应的零值 |
当函数存在返回列表时,必须显式地以 return 语句结束; 除非函数明确不会走完整个执行流程,比如在函数中抛出宕机异常或函数体内存在一个没有 break 退出条件的无限 for 循环 | |
如果几个形参或者返回值的类型相同,那么类型标识符只需要写一次 | |
代码示例 | 以下两个声明是完全相同的: func f(i , j , k int , s , t string)?????????????????????? {? /* ... */? } func f(i int , j int , k int , s string , t string)??? {? /* ... */? } |
匿名变量 | 空白标识符 " _ " 用来强调这个形参在函数中未被使用 |
代码示例 | 下面使用 4 种方式声明一个带有两个形参和一个返回值的函数,所有变量都是 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 语言没有默认参数值的概念,也不能指定形参名 所以除了用于文档说明之外,形参和返回值的命名不会影响调用者 | |
形参与返回值 局部变量 | 形参变量是函数的局部变量,形参的初始值由调用者提供的实参传递 |
函数形参以及命名的返回值,同属于函数最外层作用域的局部变量 | |
按值传递 | 实参是 "按值传递" 的,所以函数接收到的是每个实参的副本(拷贝) |
修改函数的形参变量,不会影响调用者提供的实参(的值) | |
然而,如果提供的实参包含引用类型,比如 指针 、slice 、map 、函数 、通道,那么当函数使用形参变量时,就有可能间接地修改实参变量 | |
有些函数的声明没有函数体,说明这个函数使用了除 Go 以外的语言实现 | |
这样的声明定义了该函数的签名 package math func Sin(x float64) float64? // 使用汇编语言实现 |
递归的含义 | 函数可以递归调用,这意味着函数可以直接或间接地调用自己 |
递归,是一种实用的技术,可以处理许多带有递归特性的数据结构 | |
下面使用递归处理 HTML 文件 | |
下面的代码示例使用了一个非标准的包 golang.org/x/net/html ,它提供了解析 HTML 的功能; golang.org/x/... 下的仓库(比如网络 、国际化语言处理 、移动平台 、图片处理 、加密功能以及开发者工具)都由 Go 团队负责设计和维护;这些包并不属于标准库,原因是它们还在开发当中,或者很少被 Go 程序员使用 | |
我们需要的 golang.org/x/net/html API 如下面的代码所示; 函数 html.Parse 读入一段字节序列,解析它们,然后返回 HTML 文档树的根节点 html.Node ;HTML 有多种节点,比如文本 、注释等; 此处,我们只关心表单的 "元素节点 <name key='value'> " | |
类型声明 | 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 ,使用递归的 visit 函数获取 HTML 文本的超链接,并且把所有的超链接输出 |
gop1.io/ch5/findlinks1 package main import ( ?? ? ?? "fmt" ?? ? ?? "os" ?? ? ?? "golang.org/x/net/html" ) func main() { ??????? doc , err := html.Parse(os.Stdin)??? // 标准输入进入 Parse 函数进行解析 ??????? if err != nil {???? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // 判断解析函数执行是否出错 ??????????????? fmt.Fprintf(os.Stderr , "findlinks1 :%v\n" , err) ??????????????? os.Exit(1) ??????? } ??????? for _ , link := range visit(nil , doc) {?? // 获取 HTML 文本的超链接 ??????????????? fmt.Println(link)???????????????????????? // 输出超链接 ??????? } } | |
visit 函数 | visit 函数遍历 HTML 树上的所有节点,从 HTML 锚元素 <a href='...'> 中得到 href 属性的内容,将获取到的链接内容添加到字符串 slice ,最后返回这个 slice |
// visit 函数会将 n 节点中的每个链接添加到结果中 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 进行递归,visit 递归地调用自己去访问节点 n 的所有子节点,并且将访问过的节点保存在 FirstChild 链表中 | |
下面的程序使用递归遍历所有 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 递归调用自己时,被调用的函数会接收到栈的副本,所以 outline 无法修改调用者的栈 | |
函数调用栈 | 许多变成语言使用固定长度的函数调用栈;大小在 64KB 到 2MB 之间; 递归的深度会受限于固定长度的栈大小,所以当进行深度递归调用时必须严防 “栈溢出” ;固定长度的栈甚至会造成一定的安全隐患 相比固定长的栈 ,Go 语言的实现使用了可变长度的栈,栈大小随使用而增长,可达 1GB 左右的上限;安全使用栈,不必担心溢出问题 |
多返回值概念 | 一个函数能够返回不止一个结果 |
标准包内的许多函数返回两个值:一个期望得到的计算结果、一个错误值(或者一个表示函数是否调用正确的布尔值) | |