Go 中的函数

函数声明

函数的类型称作函数的签名。当两个函数拥有相同的形参列表和返回列表时,认为这两个函数的类型或签名是相同的。而形参和返回值的名字不会影响到函数类型、采用简写同样也不会影响到函数的类型。
实参是按值传递的,所以函数接收到的是每个实参的副本;修改函数的形参变量并不会影响到调用者提供的实参。然而,如果提供的实参包含引用类型,比如指针、slice、map、函数或者通道,那么当函数使用形参变量时就要可能会间接地修改实参变量。

错误处理

对于固定或者不可预测的错误,在短暂的间隔后对操作进行重试是合乎情理的,超出一定的重试次数和限定的时间后再报错退出。

// WaitForServer 尝试连接 URL 对应的服务器
// 在一分钟内使用指数退避策略进行重试
// 所有的尝试失败后返回错误
func WaitForServer(url string) error {
	const timeout = 1 * time.Minute
	deadline := time.Now().Add(timeout)
	for tries := 0; time.Now().Before(deadline); tries++ {
		_, err := http.Get(url)
		if err == nil {
			return nil     // 成功
		}
		log.Printf("server not responding(%s); retrying...", err)
		time.Sleep(time.Second << uint(tries)) // 指数退避策略
	}
	return fmt.Errorf("server %s failed to response after %s", url, timeout)

}

函数变量

函数在 Go 语言中是头等重要的值,就像其他值,函数变量也有类型,而且它们可以赋给变量后者传递或者从其他函数中返回。函数变量可以像其他函数一样调用,比如:

	f := square
	fmt.Println(f(3))   // 9

	f = negative
	fmt.Println(f(3))   // -3
	fmt.Printf("%T\n", f)  // func(int) int

	f = product   // 编译错误:不能把类型 func(int, int) int 赋给 func(int) int

函数类型的零值是 nil(空值),调用一个空的函数变量将导致 宕机。

	var f func(int) int
	f(3)  // 宕机, 调用空函数

函数变量可以和空值相比较,但它们本身不可比较,所以不可以互相进行比较或者作为键值出现在 map 中。
函数变量使得函数不仅将数据进行参数化,还将函数的行为当作参数进行传递。标准库中蕴含大量的例子。比如, strings.Map 对字符串中的每一个字符使用一个函数,将结果变成另一个字符串。

func add1(r rune) rune {
	return r + 1

}

	fmt.Println(strings.Map(add1, "HAL-9000"))    // IBM.:111
	fmt.Println(strings.Map(add1, "VMS"))		// WNT
	fmt.Println(strings.Map(add1, "Admix"))		// Benjy

匿名函数

函数字面量就像函数声明,但在 func 关键字后面没有函数的名称。它是一个表达式,它的值称作匿名函数。以匿名函数定义的函数能够获取到整个词法环境,因此里层的函数可以使用外层函数中的变量。

	f := square()
	fmt.Println(f())    // 1
	fmt.Println(f())	// 4
	fmt.Println(f())	// 9
	fmt.Println(f())	// 16

func square() func() int {
	var x int
	return func() int {
		x++
		return x * x
	}
}

上面的例子说明了函数变量不仅是一段代码还可以拥有状态。里层的匿名函数能够获取和更新外层 square 函数的局部变量。这些隐藏的变量引用就是我们把函数归类为引用类型而且函数变量无法进行比较的原因。函数变量类似于使用闭包方法实现的变量, Go 程序员通常把函数编程称为闭包。
我们再一次看到了变量的声明周期不是由它的作用域所决定的:变量 x 在 main 函数中返回 square 函数后依旧存在(虽然 x 在这个时候是隐藏在函数变量 f 中的)。

捕获迭代变量

假设一个程序必须创建一系类的目录之后又会删除它们。可以使用一个包含函数变量的 slice 进行清理操作。

	var rmdirs []func()
	for _, d := range tempDirs() {
		dir := d				// 这一行很重要
		os.MkdirAll(dir, 0755)   // 创建父目录
		rmdirs = append(rmdirs, func() {
			os.RemoveAll(dir)
		})
	}
	
	// ...做一些其他处理...
	for _, rmdir := range rmdirs {
		rmdir()   // 清理
	}

你可能会奇怪,为什么在循环体内将循环变量赋给一个新的局部变量 dir,而不是在下面这个略有错误的变体中直接使用循环变量 dir。

	var rmdirs []func()
	for _, dir := range tempDirs() {
		os.MkdirAll(dir, 0755)   // 创建父目录
		rmdirs = append(rmdirs, func() {
			os.RemoveAll(dir)
		})
	}

这个原因是循环变量的作用域的规则限制。dir 在 for 循环引进的一个块作用域内进行声明。在循环里创建的所有函数变量共享相同的变量—— 一个可访问的存储位置,而不是固定的值。dir 变量的值在不断地迭代中更新,因此当调用清理函数时,dir 变量已经被每一次的 for 循环更新多次。因此,dir 变量的实际取值是最后一次迭代时的值并且所有的 os.RemoveAll 调用最终都试图删除同一个目录。
这样的隐患不仅存在于使用 range 的 for 循环里。在下面的循环中也面临由于无意间捕获的索引变量 i 而导致的同样问题。

	var rmdirs []func()
	dirs := tempDirs()
	for i := 0; i < len(dirs); i++  {
		os.MkdirAll(dirs[i], 0755)   // OK
		rmdirs = append(rmdirs, func() {
			os.RemoveAll(dirs[i])  // 不正确
		})
	}

延迟函数调用

defer 语句经常使用成对的操作,比如打开和关闭,连接和断开,加锁和解锁,即使是再复杂的控制流,资源在任何情况下都能否正确释放。
defer 语句也可以用来调试一个复杂的函数,即在函数的“入口”和 “出口” 处设置调试行为。
下面的 bigSlowOperation 函数在开头调用 trace 函数,在函数刚进入的时候执行输出,然后返回一个函数变量,当其被调用的时候执行退出函数的操作。但别忘了 defer 语句末尾的圆括号,否则入口的操作会在函数退出时执行而出口的操作永远都不会调用。

package main

import (
	"log"
	"time"
)

func main()  {
	defer trace("main")()    // 别忘记这对圆括号
	// ... 一些其他的操作...
	time.Sleep(10 * time.Second)
}

func trace(msg string) func() {
	start := time.Now()
	log.Printf("enter %s", msg)
	return func() {
		log.Printf("exit %s (%s)", msg, time.Since(start))
	}
}

// 执行结果为:
2019/07/08 08:31:30 enter main
2019/07/08 08:31:40 exit main (10.00302384s)

延迟执行的函数在 return 语句之后执行,并且可以更新函数的结果变量。因为匿名函数可以得到其外层函数在作用域内的变量(包括命名的结果)。

	_ = double(4)

	// 输出:
	// double(4) = 8

func double(x int) (result int) {
	defer func() {
		fmt.Printf("double(%d) = %d\n", x, result)
	}()
	return x + x
}

因为延迟函数不到函数的最后一刻是不会执行的。要注意循环里 defer 语句的使用。下面的这段代码就可能会用尽所有的文件描述符,这是因为处理完成后却没有文件关闭。

	for _, filename := range filenames{
		f, err := os.Open(filename)
		if err != nil {
			return err
		}
		defer f.Close()
		// ... 处理文件f ...

一种解决方式是将循环体(包括 defer 语句)放到另一个函数里,每次循环迭代都会调用文件的关闭函数。

for _, filename := range filenames {
		if err := doFile(filename); err != nil {
			return err
		}
	}

func doFile(filename string) error  {
	f, err := os.Open(filename)
	if err != nil {
		return err
	}
	defer f.Close()
	// ... 处理文件f ...
}

恢复

如果内置的 recover 函数在延迟函数的内部调用,而且这个包含 defer 语句的函数发生宕机,recover 会终止当前的宕机状态并且返回宕机的值。函数不会从之前宕机的地方继续运行而是正常返回。

func Parse(input string) (s *Syntax, err error) {
	defer func() {
		if p := recover(); p != nil {
			err = fmt.Errorf("internal error: %v", p)
		}
	}()
	// ... 解析器 ...
}

Parse 函数中的延迟函数会从宕机状态恢复,并使用宕机值组成一条错误消息:理想的写法是使用 runtime.Stack 将整个调用栈包含进来。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值