《Go程序设计语言》 5 函数

函数声明

func name(参数列表) (返回列表) {
	函数体
}
  • 函数的类型被称为函数签名,这一点与C++依然是一样的,如果两个函数拥有相同的参数列表和返回值列表,则这两个函数的函数签名是一样的
  • 参数是按值传递

递归

GO语言使用可变长度的栈,栈大小随递归深度增加,最大可达1GB上限,基本上不用担心递归过深的问题

比如下面这个输出HTML节点树的程序就用到了递归,与其他语言的递归没有不同,只不过因为可变长度栈的存在,我们可以更加大胆的使用递归

func outline(stack []string n *html.Node) {
	if n.Type == html.ElementNode {
		stack = append(stack,n.Data)
		fmt.Println(stack)
	}
	for c:=n.FistChild ; c!=nil ; c=c.NextSibling{
		outline(stack,c)
	}
}

多返回值

wow,这里是C++完全没有的东西了。使用时,像下面这样。

func CountWordsAndImages(url string) (words,images int,err error) {
	resp,err := http.Get(url)
	if err != nil {
		return 0,0,err
	}
	doc,err := html.Parse(resp.Body)
	resp.Body.Close()
	if err != nil {
		err = fmt.Errorf("parsing HTML:%s",err)
		return 0,0,err
	}
	words,images = countWordsAndImages(doc)
	return words,images,err
}
  • 多返回值函数一般与之前讲的多重赋值结合
  • 注意细节:返回列表也可以标明“形式返回变量名字”,这种情况下我们可以使用裸返回。即单一的return可以代替return words,image,err

错误

当函数调用发生错误时,习惯上讲错误值作为最后一个结果返回

错误处理策略

当一个函数调用返回一个错误时,调用者应当负责检查错误并采取合适的处理应对

  • 将错误传递下去,这通常发生在嵌套调用之中。我们要为原始的错误不断添加额外的上下文描述来建立一个可读性强的错误描述
    • 错误信息被频繁的串接起来,所以要避免换行,首字母也不该大写
  • 对于不固定不可预测的错误,需要在短暂的时间间隔后对操作进行重试,超出一定重试次数和限定时间后才报错退出
  • 在主程序部分,调用者可以输出错误并停止程序
  • 在一些情况下,应记录错误信息到日志,然后程序继续运行

文件尾

使用io.EOF标识文件尾

in := bufop.NewReader(os.Stdin)
for {
	r,_,err := in.ReadRune()
	if err == io.EOF { //到达文件尾,结束读取
		break
	}
	if err != nil { //有别的错误
		return fmt.Errorf("read failed:%v",err)
	}
}

函数变量

函数可以被当作变量声明出来,也可以进行赋值(这一点与C++的函数指针的runnable对象有所不同,在GO里,函数也是一种值,与别的值类型没有区别)

func square(n int) int {return n*n}
f := square

var g func(int) int

自然的,可以将函数作为参数传递给另一个函数。比如下面这个处理节点数的程序,定义了在访问节点前的操作pre和访问和节点后的操作post

func forEachNode(n *html.Node, pre,post func(n *html.Node)) {
	if pre != nil {
		pre(n)
	}
	for c:=n.FistChild ; c!=nil ; c=c.NextSibling{
		forEachNode(c,pre,post)
	}
	if post != nil {
		post(n)
	}
}

匿名函数

命名函数只能在包级别的作用域声明。

函数字面量,其值也被称为匿名函数,是func后不写函数名称的定义方式

我们可以通过这样的方式为函数赋予内部的状态,就像我们在C++中通过重载()编写伪函数差不多,实现闭包功能

func squares func() int {
	var x int
	return func int {
		x++
		return x*x
	}
}
f := squares()
a := f() //1
b := f() //4

这也意味我们可以在函数的内部定义匿名函数,以获得更好的功能分块,使得代码可读性增强。比如下满这个实现对有向无环图的拓扑排序

func topoSort(m map[string][]string) []string {
	var order []string
	sen := 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
}

这里有几点可以注意

  • Go语言的邻接表使用的数据结构map[string][]string
  • 为了使用匿名函数递归,采取了“先声明,后定义”的方式

警告:捕获迭代变量

当我们试图遍历一个切片并存储其成员的地址时,采用了下面的方式

slice := []int{1, 2, 3}
pointers := []*int{}
for _, v := range slice {
    pointers = append(pointers, &v)
}

这种方式最终被发现是不正确的,这是因为所有的迭代变量在循环中是同一个实例,也就是说存储的所有&v只会是一个值,该值随着每次迭代更新一次,但是保持一致,直到迭代结束,我们的切片里存储的所有地址都只是最后一个元素的地址

为了解决这个问题,需要在循环内部新声明一个变量

slice := []int{1, 2, 3}
pointers := []*int{}
for i := range slice {
    v := slice[i]
    pointers = append(pointers, &v)
}

此时,每次迭代的时候,v都是一个新的变量实例

变长函数

  • 同其它语言一样,只有最后一个参数可以是变化参数列表
  • 在最后的类型名之前使用省略号...int,表示可以传入任意数目的该类型参数
func sum(vals ...int) int {
	total := 0
	for _,val := range vals {
		total += val
	}
	return total
}

这个例子展示了原理,在函数体内,vals就是一个slice

延迟函数调用

原书这里写的一塌糊涂,我直接问了问chatGPT。在这里主要是defer的应用。defer应用在函数定义里,后面跟着一个函数的调用。其作用有点类似与函数的“析构”和“构造“

  • defer somefunc()后的函数调用不会立即执行,而是依次压栈。直到外部函数返回的前一刻,清空栈,全部执行。有点像外部函数的析构

  • defer somefunc()(),即再加一个括号,其后面的函数在外部函数被调用时立即执行,有点像外部函数的构造

  • defer语句通常适用于成对的操作,打开和关闭文件,连接和断开,加锁和解锁(感觉就是非常像构造和析构)

func someFunc() {
    defer fmt.Println("world")
    defer fmt.Println("hello")()
    fmt.Println(",")
}

如果在main中调用以上函数,会打印出hello,world

宕机和恢复(panic(“…”)/recover())

宕机

运行时错误会引起宕机,一般而言,当发生宕机。程序会中断运行。Go语言程序在宕机时,会将堆栈和 goroutine 信息输出到控制台

我们也可以使用panic()来手动引发宕机

package main

import "fmt"

func main() {
    defer fmt.Println("宕机后要做的事情1")
    defer fmt.Println("宕机后要做的事情2")
    panic("宕机")
    fmt.Println("hello")
}
宕机后要做的事情2
宕机后要做的事情1
panic: 宕机

goroutine 1 [running]:
main.main()
	/tmp/sandbox50552110/prog.go:8 +0xac

Program exited.
  • 宕机后会直接中断程序,其后面的任何代码都不会被执行了
  • 但是defer仍起作用

恢复

recover必须在defer延迟函数的内部调用,包含这个defer的函数发生宕机时,recover()会终止当前宕机状态并返回宕机的值,函数正常返回

func returnNonZero() int {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovering from panic and returning 1")
            return 1
        }
    }()
    
    panic("Panicking!")
}

func main() {
    value := returnNonZero()
    fmt.Println(value)
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值