Golang function 函数详解

前言

对于任何语言而言,函数都是结构化编程中的重要一环,通过函数机制,可以把一个大的模块分解成多个小任务,让代码结构更清晰,可复用性大大提高。

本文将介绍如何定义,调用函数,如何定义和接收函数错误,以及 golang 特有的可变参数,闭包,defer,panic 和 recover 等机制。

函数定义

Go 语言的函数基本组成为:函数名,入参列表,函数体,返回参数列表。

func function_name( [parameter list] ) [return_types] {
     
   // 函数体
}

// 如:
// func find_max(num1, num2 int) int {}
// func swap(x, y string) (string, string) {}

如果一个函数所有的返回值都有显式的变量名,那么该函数的return语句可以省略操作数。这称之为 bare return,但是不建议使用这种方法,会导致程序的可读性变差。

func CountWordsAndImages(url string) (words, images int, err error) {
     
	// 函数体
	return          // 等价于 return words, images, err
}

words, images, err := CountWordsAndImages("/test/test.go")
//...

注意:

  1. 每一次函数调用都必须按照声明顺序为所有参数提供实参(参数值)。在函数调用时,Go语言没有默认参数值,也没有任何方法可以通过参数名指定形参。
  2. 默认情况下,Go 语言使用的是值传递,因此函数的形参是实参的拷贝, 在调用过程中对形参进行修改不会影响实参。 如果需要在函数中修改变量,需要传入变量的指针。
  3. 跟 golang 的变量命名规则一致,如果函数名的首字母为大写,函数为可导出函数,可以在包外被调用,如果为小写,则只能在本包内调用。
  4. golang 的函数可以有多个返回值,调用时需要依次接收,如果有某个回参不需要被使用,必须要在对应位置使用 blank identifier _ 接收。
  5. 函数作为 golang 中的 first class values,拥有类型,并且可以被复制给其他变量,本身也可以作为函数的入参和回参。

可变参数

参数数量可变的函数称为为可变参数函数。例如 fmt.Printf() 函数,首先接收一个必备的参数,之后接收任意个数的后续参数。

func Printf(format string, a ...interface{
     }) (n int, err error) {
     
	return Fprintf(os.Stdout, format, a...)
}

在声明可变参数函数时,只能把可变参数作为参数列表的最后一个参数,在参数类型之前加上省略符号 ...,这表示该函数会接收任意数量的该类型参数。如:

func sum(vals...int) int {
     
    total := 0
    for _, val := range vals {
     
        total += val
    }
    return total
}

fmt.Println(sum()) // 0
fmt.Println(sum(3)) // 3
fmt.Println(sum(1, 2, 3, 4)) // 10

fmt.Printf("%T", sum) // func(...int)

实际上,可变参数会被处理为一个slice传递给函数,如果原始参数本身就是一个 slice,则需要使用 ... 先将切片展开,然后再作为参数传递,如:

values := []int{
     1, 2, 3, 4}
fmt.Println(sum(values...)) // "10"

函数错误处理

内置的 error 类型是接口类型,可能是 nil 或 non-nil,nil 意味着函数运行成功,non-nil 代表运行失败。对于non-nil的error类型, 我们可以通过调用error的Error函数或者输出函数获得字符串类型的错误信息。golang 推荐在任何有可能出现错误的函数中都添加 error 回参来增加程序的鲁棒性,比如 db 读写,接口调用等。

错误相关的方法都为于 errors 包中,在 go 语言的编码风格中,为了程序逻辑的连贯性,通常会将处理失败的逻辑代码放在处理成功的代码之前,并且不需要将正常逻辑放在 else 中。

func f() error {
     
}

err := f()
if err != nil {
     
	// 错误处理
}

// 正常逻辑

当函数出现错误时,通常在子程序中将错误返回给调用层并打印日志,调用层再进行相应的处理。也可以在发生错误时采取重试,回滚等操作。

闭包 (closures)

使用函数字面量(function literal)可以在任何表达式中声明一个函数并使用,这种方法声明的函数没有函数名,称为匿名函数(anonymous function),通常在 defer、go 等情况使用,如:

defer func() {
     
// do something...
}()

在一个函数中定义内部匿名函数时,匿名函数可以访问该函数完整的词法环境 (lexical environment),这意味着在函数中定义的内部函数可以引用该函数的变,如:

// squares 函数的入参为空,返回值为一个匿名函数,该匿名函数的返回值为 int
func squares() func() int {
     
  var x int 
  return func() int {
     
    x++
    return x * x
  }
}

func main() {
     
  	f := squares()
  	fmt.Println(f())		// 1
  	fmt.Println(f())		// 4
  	fmt.Println(f())		// 9
  
	g := squares()
	fmt.Println(g())		// 1
	fmt.Println(g())		// 4
	fmt.Println(g())		// 9
}

每次调用 squares 会生成一个局部变量 x 并返回一个匿名函数,每次调用该匿名函数时,函数都会使 x 的值加 1,这意味着匿名内部函数可以引用并修改 squares 的局部变量,也就是说匿名函数和squares中,存在变量引用,这种方式称为闭包。

通过这个例子,可以看出变量的生命周期不由作用域决定,squares 返回后,x 仍隐式的存在于 f 中。

defer 函数

在某些需要释放资源的函数中(如关闭文件,关闭 db,处理互斥锁等),为了保证资源被释放,需要在每一个逻辑中进行处理,如下伪代码:

func demo() {
     
	file, err := os.Open(filename)
	if err != nil {
     
		return
	}
	
	操作。。。
	if err1 != nil {
     
		file.Close()
		return
	}
	
	继续操作。。。
	if err2 != nil {
     
		file.Close()
		return
	}
	
	正常结束。。。
	file.Close()
	return
}

为了确保在所有执行路径下(即使函数运行失败)都释放了资源,在所有的节点都要进行 file.Close() 操作,随着函数变得复杂,需要处理的错误也变多,维护清理逻辑变得越来越困难。而 Go 语言独有的 defer 机制可以很好的解决这个问题。

defer 语句经常被用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁,释放资源的defer应该直接跟在请求资源的语句后。通过 defer 机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放。

当defer语句被执行时,跟在defer后面的函数会被延迟执行,直到包含该 defer 语句的函数执行完毕时,defer 的函数才会被执行,不论包含defer语句的函数是通过return正常结束,还是由于panic导致的异常结束。

使用 defer 修改后的示例如下:

func demo() {
     
	file, err := os.Open(filename)
	if err != nil {
     
		return
	}
	defer file.Close()
	
	操作。。。
	if err1 != nil {
     
		return
	}
	
	继续操作。。。
	if err2 != nil {
     
		return
	}
	
	正常结束。。。
	return
}

可以在一个函数中执行多条defer语句,它们的执行顺序与声明顺序相反,先 defer 的函数后执行,类似一个栈。

要注意的是尽量不要在循环体中直接加入 defer,否则在所有文件都被处理完之后,defer 才会被执行,在这个过程中,不会有任何一个文件被成功关闭,可能导致系统的文件描述符被耗尽。如:

for _, filename := range filenames {
     
  f, err := os.Open(filename)
  if err ! = nil {
     
    return err
  }
  defer f.Close()    // 风险操作,可能导致文件描述符耗尽
}

正确的做法是将循环体中的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()   // defer 在每一次 doFile 执行完毕后都会运行
}

需要注意的是 defer 后面跟着的应该是一个函数调用,而不是函数声明,所以要特别注意括号的使用,尤其是在 defer 中使用匿名函数时,如:

func add(x, y int) int {
     
  defer func() {
     
    fmt.Println("defer")
  }()					// 这个括号不能忘记
  return x + y
}

panic 和 recover

panic

有些错误在编译时无法被发现,只会在运行时引发,如数组越界、空指针引用等。这些运行时错误会引起 painc 异常,中断程序并且立即执行该 goroutine 中的 defer 函数,随后,程序崩溃并输出日志信息,包括 panic value 和函数调用的堆栈跟踪信息。

也可以手动调用内置的panic函数引发panic异常,如:

if err != nil {
     
	panic(fmt.Printf("error: %s", err))
}

但是要注意,由于 panic 会引起程序的崩溃,所以除非是非常严重的错误,否则尽量避免使用 Panic 。对于大部分漏洞,我们应该使用Go提供的错误机制来处理异常,而不是panic,以避免程序的崩溃。在健壮的程序中,任何可以预料到的错误,如不正确的输入、错误的配置或是失败的 I/O 操作都应该被优雅的处理

recover

使用 recover 可以帮助程序从 panic 异常中恢复。或者至少在程序崩溃前做一些处理,例如当web服务器遇到不可预料的严重问题时,在崩溃前将所有的连接关闭,以免客户端一直处于等待状态。

如果在 defer 函数中调用了内置函数recover,并且定义该 defer 语句的函数发生了 panic 异常,recover 会使程序从 panic 中恢复,并返回 panic value,导致panic异常的函数不会继续运行,但能正常返回。

在未发生 panic 时调用 recover,recover 会返回 nil。使用示例如下:

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

defer 函数帮助 Parse 从 panic 中恢复。在 defer 函数内部,panic value 被附加到错误信息中,并用 err 变量接收错误信息,返回给调用者。

然而,在实际使用中不推荐使用 recover 函数,否则很有可能不能及时的结束已经出错的函数,并导致更加严重的后果。

你可能感兴趣的:(Golang,后端开发,golang,go)