Golang-函数5

http://c.biancheng.net/golang/func/

Go语言函数(Go语言func)

函数构成了代码执行的逻辑结构,在Go语言中,函数的基本组成为:关键字 func、函数名、参数列表、返回值、函数体和返回语句,每一个程序都包含很多的函数,函数是基本的代码块。

因为Go语言是编译型语言,所以函数编写的顺序是无关紧要的,鉴于可读性的需求,最好把 main() 函数写在文件的前面,其他函数按照一定逻辑顺序进行编写(例如函数被调用的顺序)。

编写多个函数的主要目的是将一个需要很多行代码的复杂问题分解为一系列简单的任务来解决,而且,同一个任务(函数)可以被多次调用,有助于代码重用(事实上,好的程序是非常注意 DRY 原则的,即不要重复你自己(Don't Repeat Yourself),意思是执行特定任务的代码只能在程序里面出现一次)。

当函数执行到代码块最后一行}之前或者 return 语句的时候会退出,其中 return 语句可以带有零个或多个参数,这些参数将作为返回值供调用者使用,简单的 return 语句也可以用来结束 for 的死循环,或者结束一个协程(goroutine)。

Go语言里面拥三种类型的函数:

  • 普通的带有名字的函数
  • 匿名函数或者 lambda 函数
  • 方法

普通函数声明(定义)

函数声明包括函数名、形式参数列表、返回值列表(可省略)以及函数体。

func 函数名(形式参数列表)(返回值列表){
    函数体
}

形式参数列表描述了函数的参数名以及参数类型,这些参数作为局部变量,其值由参数调用者提供,返回值列表描述了函数返回值的变量名以及类型,如果函数返回一个无名变量或者没有返回值,返回值列表的括号是可以省略的。

如果一个函数声明不包括返回值列表,那么函数体执行完毕后,不会返回任何值,在下面的 hypot 函数中:

  1. func hypot(x, y float64) float64 {
  2. return math.Sqrt(x*x + y*y)
  3. }
  4. fmt.Println(hypot(3,4)) // "5"

x 和 y 是形参名,3 和 4 是调用时的传入的实数,函数返回了一个 float64 类型的值,返回值也可以像形式参数一样被命名,在这种情况下,每个返回值被声明成一个局部变量,并根据该返回值的类型,将其初始化为 0。

如果一个函数在声明时,包含返回值列表,那么该函数必须以 return 语句结尾,除非函数明显无法运行到结尾处,例如函数在结尾时调用了 panic 异常或函数中存在无限循环。

正如 hypot 函数一样,如果一组形参或返回值有相同的类型,我们不必为每个形参都写出参数类型,下面 2 个声明是等价的:

  1. func f(i, j, k int, s, t string) { /* ... */ }
  2. func f(i int, j int, k int, s string, t string) { /* ... */ }

下面,我们给出 4 种方法声明拥有 2 个 int 型参数和 1 个 int 型返回值的函数,空白标识符_可以强调某个参数未被使用。

  1. func add(x int, y int) int {return x + y}
  2. func sub(x, y int) (z int) { z = x - y; return}
  3. func first(x int, _ int) int { return x }
  4. func zero(int, int) int { return 0 }
  5. fmt.Printf("%T\n", add) // "func(int, int) int"
  6. fmt.Printf("%T\n", sub) // "func(int, int) int"
  7. fmt.Printf("%T\n", first) // "func(int, int) int"
  8. fmt.Printf("%T\n", zero) // "func(int, int) int"

函数的类型被称为函数的标识符,如果两个函数形式参数列表和返回值列表中的变量类型一一对应,那么这两个函数被认为有相同的类型和标识符,形参和返回值的变量名不影响函数标识符也不影响它们是否可以以省略参数类型的形式表示。

每一次函数在调用时都必须按照声明顺序为所有参数提供实参(参数值),在函数调用时,Go语言没有默认参数值,也没有任何方法可以通过参数名指定形参,因此形参和返回值的变量名对于函数调用者而言没有意义。

在函数中,实参通过值传递的方式进行传递,因此函数的形参是实参的拷贝,对形参进行修改不会影响实参,但是,如果实参包括引用类型,如指针、slice(切片)、map、function、channel 等类型,实参可能会由于函数的间接引用被修改。

函数的返回值

Go语言支持多返回值,多返回值能方便地获得函数执行后的多个返回参数,Go语言经常使用多返回值中的最后一个返回参数返回函数执行中可能发生的错误,示例代码如下:

  1. conn, err := connectToNetwork()

在这段代码中,connectToNetwork 返回两个参数,conn 表示连接对象,err 返回错误信息。

其它编程语言中函数的返回值

  • C/C++ 语言中只支持一个返回值,在需要返回多个数值时,则需要使用结构体返回结果,或者在参数中使用指针变量,然后在函数内部修改外部传入的变量值,实现返回计算结果,C++ 语言中为了安全性,建议在参数返回数据时使用“引用”替代指针。
  • C# 语言也没有多返回值特性,C# 语言后期加入的 ref 和 out 关键字能够通过函数的调用参数获得函数体中修改的数据。
  • lua 语言没有指针,但支持多返回值,在大块数据使用时方便很多。


Go语言既支持安全指针,也支持多返回值,因此在使用函数进行逻辑编写时更为方便。

1) 同一种类型返回值

如果返回值是同一种类型,则用括号将多个返回值类型括起来,用逗号分隔每个返回值的类型。

使用 return 语句返回时,值列表的顺序需要与函数声明的返回值类型一致,示例代码如下:

  1. func typedTwoValues() (int, int) {
  2.     return 1, 2
  3. }
  4. func main() {
  5.     a, b := typedTwoValues()
  6.     fmt.Println(a, b)
  7. }

代码输出结果:

1 2

纯类型的返回值对于代码可读性不是很友好,特别是在同类型的返回值出现时,无法区分每个返回参数的意义。

2) 带有变量名的返回值

Go语言支持对返回值进行命名,这样返回值就和参数一样拥有参数变量名和类型。

命名的返回值变量的默认值为类型的默认值,即数值为 0,字符串为空字符串,布尔为 false、指针为 nil 等。

下面代码中的函数拥有两个整型返回值,函数声明时将返回值命名为 a 和 b,因此可以在函数体中直接对函数返回值进行赋值,在命名的返回值方式的函数体中,在函数结束前需要显式地使用 return 语句进行返回,代码如下:

  1. func namedRetValues() (a, b int) {
  2. a = 1
  3. b = 2
  4. return
  5. }

代码说明如下:

  • 第 1 行,对两个整型返回值进行命名,分别为 a 和 b。
  • 第 3 行和第 4 行,命名返回值的变量与这个函数的布局变量的效果一致,可以对返回值进行赋值和值获取。
  • 第 6 行,当函数使用命名返回值时,可以在 return 中不填写返回值列表,如果填写也是可行的,下面代码的执行效果和上面代码的效果一样。
    1. func namedRetValues() (a, b int) {
    2. a = 1
    3. return a, 2
    4. }

提示

同一种类型返回值和命名返回值两种形式只能二选一,混用时将会发生编译错误,例如下面的代码:

  1. func namedRetValues() (a, b int, int)

编译报错提示:

mixed named and unnamed function parameters

意思是:在函数参数中混合使用了命名和非命名参数。

调用函数

函数在定义后,可以通过调用的方式,让当前代码跳转到被调用的函数中进行执行,调用前的函数局部变量都会被保存起来不会丢失,被调用的函数运行结束后,恢复到调用函数的下一行继续执行代码,之前的局部变量也能继续访问。

函数内的局部变量只能在函数体中使用,函数调用结束后,这些局部变量都会被释放并且失效。

Go语言的函数调用格式如下:

返回值变量列表 = 函数名(参数列表)

下面是对各个部分的说明:

  • 函数名:需要调用的函数名。
  • 参数列表:参数变量以逗号分隔,尾部无须以分号结尾。
  • 返回值变量列表:多个返回值使用逗号分隔。


例如,加法函数调用样式如下:

result := add(1,1)

Go语言函数变量——把函数作为值保存到变量中

在Go语言中,函数也是一种类型,可以和其他类型一样保存在变量中,下面的代码定义了一个函数变量 f,并将一个函数名为 fire() 的函数赋给函数变量 f,这样调用函数变量 f 时,实际调用的就是 fire() 函数,代码如下:

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func fire() {
  6. fmt.Println("fire")
  7. }
  8. func main() {
  9. var f func()
  10. f = fire
  11. f()
  12. }

代码输出结果:

fire

代码说明:

  • 第 7 行,定义了一个 fire() 函数。
  • 第 13 行,将变量 f 声明为 func() 类型,此时 f 就被俗称为“回调函数”,此时 f 的值为 nil。
  • 第 15 行,将 fire() 函数作为值,赋给函数变量 f,此时 f 的值为 fire() 函数。
  • 第 17 行,使用函数变量 f 进行函数调用,实际调用的是 fire() 函数。

Go语言匿名函数——没有函数名字的函数

Go语言支持匿名函数,即在需要使用函数时再定义函数,匿名函数没有函数名只有函数体,函数可以作为一种类型被赋值给函数类型的变量,匿名函数也往往以变量方式传递,这与C语言的回调函数比较类似,不同的是,Go语言支持随时在代码里定义匿名函数。

匿名函数是指不需要定义函数名的一种函数实现方式,由一个不带函数名的函数声明和函数体组成,下面来具体介绍一下匿名函数的定义及使用。

定义一个匿名函数

匿名函数的定义格式如下:

func(参数列表)(返回参数列表){
    函数体
}

匿名函数的定义就是没有名字的普通函数定义。

1) 在定义时调用匿名函数

匿名函数可以在声明后调用,例如:

  1. func(data int) {
  2. fmt.Println("hello", data)
  3. }(100)

注意第3行}后的(100),表示对匿名函数进行调用,传递参数为 100。

2) 将匿名函数赋值给变量

匿名函数可以被赋值,例如:

  1. // 将匿名函数体保存到f()中
  2. f := func(data int) {
  3. fmt.Println("hello", data)
  4. }
  5. // 使用f()调用
  6. f(100)

匿名函数的用途非常广泛,它本身就是一种值,可以方便地保存在各种容器中实现回调函数和操作封装。

匿名函数用作回调函数

下面的代码实现对切片的遍历操作,遍历中访问每个元素的操作使用匿名函数来实现,用户传入不同的匿名函数体可以实现对元素不同的遍历操作,代码如下:

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. // 遍历切片的每个元素, 通过给定函数进行元素访问
  6. func visit(list []int, f func(int)) {
  7. for _, v := range list {
  8. f(v)
  9. }
  10. }
  11. func main() {
  12. // 使用匿名函数打印切片内容
  13. visit([]int{1, 2, 3, 4}, func(v int) {
  14. fmt.Println(v)
  15. })
  16. }

代码说明如下:

  • 第 8 行,使用 visit() 函数将整个遍历过程进行封装,当要获取遍历期间的切片值时,只需要给 visit() 传入一个回调参数即可。
  • 第 18 行,准备一个整型切片 []int{1,2,3,4} 传入 visit() 函数作为遍历的数据。
  • 第 19~20 行,定义了一个匿名函数,作用是将遍历的每个值打印出来。


匿名函数作为回调函数的设计在Go语言的系统包中也比较常见,strings 包中就有类似的设计,代码如下:

  1. func TrimFunc(s string, f func(rune) bool) string {
  2. return TrimRightFunc(TrimLeftFunc(s, f), f)
  3. }

使用匿名函数实现操作封装

下面这段代码将匿名函数作为 map 的键值,通过命令行参数动态调用匿名函数,代码如下:

  1. package main
  2. import (
  3. "flag"
  4. "fmt"
  5. )
  6. var skillParam = flag.String("skill", "", "skill to perform")
  7. func main() {
  8. flag.Parse()
  9. var skill = map[string]func(){
  10. "fire": func() {
  11. fmt.Println("chicken fire")
  12. },
  13. "run": func() {
  14. fmt.Println("soldier run")
  15. },
  16. "fly": func() {
  17. fmt.Println("angel fly")
  18. },
  19. }
  20. if f, ok := skill[*skillParam]; ok {
  21. f()
  22. } else {
  23. fmt.Println("skill not found")
  24. }
  25. }

代码说明如下:

  • 第 8 行,定义命令行参数 skill,从命令行输入 --skill 可以将=后的字符串传入 skillParam 指针变量。
  • 第 12 行,解析命令行参数,解析完成后,skillParam 指针变量将指向命令行传入的值。
  • 第 14 行,定义一个从字符串映射到 func() 的 map,然后填充这个 map。
  • 第 15~23 行,初始化 map 的键值对,值为匿名函数。
  • 第 26 行,skillParam 是一个 *string 类型的指针变量,使用 *skillParam 获取到命令行传过来的值,并在 map 中查找对应命令行参数指定的字符串的函数。
  • 第 29 行,如果在 map 定义中存在这个参数就调用,否则打印“技能没有找到”。


运行代码,结果如下:

PS D:\code> go run main.go --skill=fly
angel fly
PS D:\code> go run main.go --skill=run
soldier run 

Go语言函数类型实现接口——把函数作为接口来调用

函数和其他类型一样都属于“一等公民”,其他类型能够实现接口,函数也可以,本节将对结构体与函数实现接口的过程进行对比。

首先给出本节完整的代码:

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. // 调用器接口
  6. type Invoker interface {
  7. // 需要实现一个Call方法
  8. Call(interface{})
  9. }
  10. // 结构体类型
  11. type Struct struct {
  12. }
  13. // 实现Invoker的Call
  14. func (s *Struct) Call(p interface{}) {
  15. fmt.Println("from struct", p)
  16. }
  17. // 函数定义为类型
  18. type FuncCaller func(interface{})
  19. // 实现Invoker的Call
  20. func (f FuncCaller) Call(p interface{}) {
  21. // 调用f函数本体
  22. f(p)
  23. }
  24. func main() {
  25. // 声明接口变量
  26. var invoker Invoker
  27. // 实例化结构体
  28. s := new(Struct)
  29. // 将实例化的结构体赋值到接口
  30. invoker = s
  31. // 使用接口调用实例化结构体的方法Struct.Call
  32. invoker.Call("hello")
  33. // 将匿名函数转为FuncCaller类型,再赋值给接口
  34. invoker = FuncCaller(func(v interface{}) {
  35. fmt.Println("from function", v)
  36. })
  37. // 使用接口调用FuncCaller.Call,内部会调用函数本体
  38. invoker.Call("hello")
  39. }

有如下一个接口:

  1. // 调用器接口
  2. type Invoker interface {
  3. // 需要实现一个Call()方法
  4. Call(interface{})
  5. }

这个接口需要实现 Call() 方法,调用时会传入一个 interface{} 类型的变量,这种类型的变量表示任意类型的值。

接下来,使用结构体进行接口实现。

结构体实现接口

结构体实现 Invoker 接口的代码如下:

  1. // 结构体类型
  2. type Struct struct {
  3. }
  4. // 实现Invoker的Call
  5. func (s *Struct) Call(p interface{}) {
  6. fmt.Println("from struct", p)
  7. }

代码说明如下:

  • 第 2 行,定义结构体,该例子中的结构体无须任何成员,主要展示实现 Invoker 的方法。
  • 第 6 行,Call() 为结构体的方法,该方法的功能是打印 from struct 和传入的 interface{} 类型的值。


将定义的 Struct 类型实例化,并传入接口中进行调用,代码如下:

  1. // 声明接口变量
  2. var invoker Invoker
  3. // 实例化结构体
  4. s := new(Struct)
  5. // 将实例化的结构体赋值到接口
  6. invoker = s
  7. // 使用接口调用实例化结构体的方法Struct.Call
  8. invoker.Call("hello")

代码说明如下:

  • 第 2 行,声明 Invoker 类型的变量。
  • 第 5 行,使用 new 将结构体实例化,此行也可以写为 s:=&Struct。
  • 第 8 行,s 类型为 *Struct,已经实现了 Invoker 接口类型,因此赋值给 invoker 时是成功的。
  • 第 11 行,通过接口的 Call() 方法,传入 hello,此时将调用 Struct 结构体的 Call() 方法。


接下来,对比下函数实现结构体的差异。

代码输出如下:

from struct hello

函数体实现接口

函数的声明不能直接实现接口,需要将函数定义为类型后,使用类型实现结构体,当类型方法被调用时,还需要调用函数本体。

  1. // 函数定义为类型
  2. type FuncCaller func(interface{})
  3. // 实现Invoker的Call
  4. func (f FuncCaller) Call(p interface{}) {
  5. // 调用f()函数本体
  6. f(p)
  7. }

代码说明如下:

  • 第 2 行,将 func(interface{}) 定义为 FuncCaller 类型。
  • 第 5 行,FuncCaller 的 Call() 方法将实现 Invoker 的 Call() 方法。
  • 第 8 行,FuncCaller 的 Call() 方法被调用与 func(interface{}) 无关,还需要手动调用函数本体。


上面代码只是定义了函数类型,需要函数本身进行逻辑处理,FuncCaller 无须被实例化,只需要将函数转换为 FuncCaller 类型即可,函数来源可以是命名函数、匿名函数或闭包,参见下面代码:

  1. // 声明接口变量
  2. var invoker Invoker
  3. // 将匿名函数转为FuncCaller类型, 再赋值给接口
  4. invoker = FuncCaller(func(v interface{}) {
  5. fmt.Println("from function", v)
  6. })
  7. // 使用接口调用FuncCaller.Call, 内部会调用函数本体
  8. invoker.Call("hello")

代码说明如下:

  • 第 2 行,声明接口变量。
  • 第 5 行,将 func(v interface{}){} 匿名函数转换为 FuncCaller 类型(函数签名才能转换),此时 FuncCaller 类型实现了 Invoker 的 Call() 方法,赋值给 invoker 接口是成功的。
  • 第 10 行,使用接口方法调用。


代码输出如下:

from function hello

HTTP包中的例子

HTTP 包中包含有 Handler 接口定义,代码如下:

  1. type Handler interface {
  2. ServeHTTP(ResponseWriter, *Request)
  3. }

Handler 用于定义每个 HTTP 的请求和响应的处理过程。

同时,也可以使用处理函数实现接口,定义如下:

  1. type HandlerFunc func(ResponseWriter, *Request)
  2. func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
  3. f(w, r)
  4. }

要使用闭包实现默认的 HTTP 请求处理,可以使用 http.HandleFunc() 函数,函数定义如下:

  1. func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
  2. DefaultServeMux.HandleFunc(pattern, handler)
  3. }

而 DefaultServeMux 是 ServeMux 结构,拥有 HandleFunc() 方法,定义如下:

  1. func (mux *ServeMux) HandleFunc(pattern string, handler func
  2. (ResponseWriter, *Request)) {
  3. mux.Handle(pattern, HandlerFunc(handler))
  4. }

上面代码将外部传入的函数 handler() 转为 HandlerFunc 类型,HandlerFunc 类型实现了 Handler 的 ServeHTTP 方法,底层可以同时使用各种类型来实现 Handler 接口进行处理。

Go语言闭包(Closure)——引用了外部变量的匿名函数

Go语言中闭包是引用了自由变量的函数,被引用的自由变量和函数一同存在,即使已经离开了自由变量的环境也不会被释放或者删除,在闭包中可以继续使用这个自由变量,因此,简单的说:

函数 + 引用环境 = 闭包

同一个函数与不同引用环境组合,可以形成不同的实例,如下图所示。


图:闭包与函数引用


一个函数类型就像结构体一样,可以被实例化,函数本身不存储任何信息,只有与引用环境结合后形成的闭包才具有“记忆性”,函数是编译期静态的概念,而闭包是运行期动态的概念。

其它编程语言中的闭包

闭包(Closure)在某些编程语言中也被称为 Lambda 表达式。

闭包对环境中变量的引用过程也可以被称为“捕获”,在 C++11 标准中,捕获有两种类型,分别是引用和复制,可以改变引用的原值叫做“引用捕获”,捕获的过程值被复制到闭包中使用叫做“复制捕获”。

在 Lua 语言中,将被捕获的变量起了一个名字叫做 Upvalue,因为捕获过程总是对闭包上方定义过的自由变量进行引用。

闭包在各种语言中的实现也是不尽相同的,在 Lua 语言中,无论闭包还是函数都属于 Prototype 概念,被捕获的变量以 Upvalue 的形式引用到闭包中。

C++ 与 C# 中为闭包创建了一个类,而被捕获的变量在编译时放到类中的成员中,闭包在访问被捕获的变量时,实际上访问的是闭包隐藏类的成员。

在闭包内部修改引用的变量

闭包对它作用域上部的变量可以进行修改,修改引用的变量会对变量进行实际修改,通过下面的例子来理解:

  1. // 准备一个字符串
  2. str := "hello world"
  3. // 创建一个匿名函数
  4. foo := func() {
  5. // 匿名函数中访问str
  6. str = "hello dude"
  7. }
  8. // 调用匿名函数
  9. foo()

代码说明如下:

  • 第 2 行,准备一个字符串用于修改。
  • 第 5 行,创建一个匿名函数。
  • 第 8 行,在匿名函数中并没有定义 str,str 的定义在匿名函数之前,此时,str 就被引用到了匿名函数中形成了闭包。
  • 第 12 行,执行闭包,此时 str 发生修改,变为 hello dude。


代码输出:

hello dude

示例:闭包的记忆效应

被捕获到闭包中的变量让闭包本身拥有了记忆效应,闭包中的逻辑可以修改闭包捕获的变量,变量会跟随闭包生命期一直存在,闭包本身就如同变量一样拥有了记忆效应。

累加器的实现:

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. // 提供一个值, 每次调用函数会指定对值进行累加
  6. func Accumulate(value int) func() int {
  7. // 返回一个闭包
  8. return func() int {
  9. // 累加
  10. value++
  11. // 返回一个累加值
  12. return value
  13. }
  14. }
  15. func main() {
  16. // 创建一个累加器, 初始值为1
  17. accumulator := Accumulate(1)
  18. // 累加1并打印
  19. fmt.Println(accumulator())
  20. fmt.Println(accumulator())
  21. // 打印累加器的函数地址
  22. fmt.Printf("%p\n", &accumulator)
  23. // 创建一个累加器, 初始值为1
  24. accumulator2 := Accumulate(10)
  25. // 累加1并打印
  26. fmt.Println(accumulator2())
  27. // 打印累加器的函数地址
  28. fmt.Printf("%p\n", &accumulator2)
  29. }

代码说明如下:

  • 第 8 行,累加器生成函数,这个函数输出一个初始值,调用时返回一个为初始值创建的闭包函数。
  • 第 11 行,返回一个闭包函数,每次返回会创建一个新的函数实例。
  • 第 14 行,对引用的 Accumulate 参数变量进行累加,注意 value 不是第 11 行匿名函数定义的,但是被这个匿名函数引用,所以形成闭包。
  • 第 17 行,将修改后的值通过闭包的返回值返回。
  • 第 24 行,创建一个累加器,初始值为 1,返回的 accumulator 是类型为 func()int 的函数变量。
  • 第 27 行,调用 accumulator() 时,代码从 11 行开始执行匿名函数逻辑,直到第 17 行返回。
  • 第 32 行,打印累加器的函数地址。


对比输出的日志发现 accumulator 与 accumulator2 输出的函数地址不同,因此它们是两个不同的闭包实例。

每调用一次 accumulator 都会自动对引用的变量进行累加。

示例:闭包实现生成器

闭包的记忆效应被用于实现类似于设计模式中工厂模式的生成器,下面的例子展示了创建一个玩家生成器的过程。

玩家生成器的实现:

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. // 创建一个玩家生成器, 输入名称, 输出生成器
  6. func playerGen(name string) func() (string, int) {
  7. // 血量一直为150
  8. hp := 150
  9. // 返回创建的闭包
  10. return func() (string, int) {
  11. // 将变量引用到闭包中
  12. return name, hp
  13. }
  14. }
  15. func main() {
  16. // 创建一个玩家生成器
  17. generator := playerGen("high noon")
  18. // 返回玩家的名字和血量
  19. name, hp := generator()
  20. // 打印值
  21. fmt.Println(name, hp)
  22. }

代码输出如下:

high noon 150

代码说明如下:

  • 第 8 行,playerGen() 需要提供一个名字来创建一个玩家的生成函数。
  • 第 11 行,声明并设定 hp 变量为 150。
  • 第 14~18 行,将 hp 和 name 变量引用到匿名函数中形成闭包。
  • 第 24 行中,通过 playerGen 传入参数调用后获得玩家生成器。
  • 第 27 行,调用这个玩家生成器函数,可以获得玩家的名称和血量。

闭包还具有一定的封装性,第 11 行的变量是 playerGen 的局部变量,playerGen 的外部无法直接访问及修改这个变量,这种特性也与面向对象中强调的封装性类似。

Go语言可变参数(变参函数)

在C语言时代大家一般都用过 printf() 函数,从那个时候开始其实已经在感受可变参数的魅力和价值,如同C语言中的 printf() 函数,Go语言标准库中的 fmt.Println() 等函数的实现也依赖于语言的可变参数功能。

本节我们将介绍可变参数的用法。合适地使用可变参数,可以让代码简单易用,尤其是输入输出类函数,比如日志函数等。

可变参数类型

可变参数是指函数传入的参数个数是可变的,为了做到这点,首先需要将函数定义为可以接受可变参数的类型:

  1. func myfunc(args ...int) {
  2. for _, arg := range args {
  3. fmt.Println(arg)
  4. }
  5. }

上面这段代码的意思是,函数 myfunc() 接受不定数量的参数,这些参数的类型全部是 int,所以它可以用如下方式调用:

myfunc(2, 3, 4)
myfunc(1, 3, 7, 13)

形如...type格式的类型只能作为函数的参数类型存在,并且必须是最后一个参数,它是一个语法糖(syntactic sugar),即这种语法对语言的功能并没有影响,但是更方便程序员使用,通常来说,使用语法糖能够增加程序的可读性,从而减少程序出错的可能。

Go语言defer(延迟执行语句)

Go语言的 defer 语句会将其后面跟随的语句进行延迟处理,在 defer 归属的函数即将返回时,将延迟处理的语句按 defer 的逆序进行执行,也就是说,先被 defer 的语句最后被执行,最后被 defer 的语句,最先被执行。

关键字 defer 的用法类似于面向对象编程语言 Java 和 C# 的 finally 语句块,它一般用于释放某些已分配的资源,典型的例子就是对一个互斥解锁,或者关闭一个文件。

多个延迟执行语句的处理顺序

当有多个 defer 行为被注册时,它们会以逆序执行(类似栈,即后进先出),下面的代码是将一系列的数值打印语句按顺序延迟处理,如下所示:

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. fmt.Println("defer begin")
  7. // 将defer放入延迟调用栈
  8. defer fmt.Println(1)
  9. defer fmt.Println(2)
  10. // 最后一个放入, 位于栈顶, 最先调用
  11. defer fmt.Println(3)
  12. fmt.Println("defer end")
  13. }

代码输出如下:

defer begin
defer end
3
2
1

结果分析如下:

  • 代码的延迟顺序与最终的执行顺序是反向的。
  • 延迟调用是在 defer 所在函数结束时进行,函数结束可以是正常返回时,也可以是发生宕机时。

使用延迟执行语句在函数退出时释放资源

处理业务或逻辑中涉及成对的操作是一件比较烦琐的事情,比如打开和关闭文件、接收请求和回复请求、加锁和解锁等。在这些操作中,最容易忽略的就是在每个函数退出处正确地释放和关闭资源。

defer 语句正好是在函数退出时执行的语句,所以使用 defer 能非常方便地处理资源释放问题。

1) 使用延迟并发解锁

在下面的例子中会在函数中并发使用 map,为防止竞态问题,使用 sync.Mutex 进行加锁,参见下面代码:

  1. var (
  2. // 一个演示用的映射
  3. valueByKey = make(map[string]int)
  4. // 保证使用映射时的并发安全的互斥锁
  5. valueByKeyGuard sync.Mutex
  6. )
  7. // 根据键读取值
  8. func readValue(key string) int {
  9. // 对共享资源加锁
  10. valueByKeyGuard.Lock()
  11. // 取值
  12. v := valueByKey[key]
  13. // 对共享资源解锁
  14. valueByKeyGuard.Unlock()
  15. // 返回值
  16. return v
  17. }

代码说明如下:

  • 第 3 行,实例化一个 map,键是 string 类型,值为 int。
  • 第 5 行,map 默认不是并发安全的,准备一个 sync.Mutex 互斥量保护 map 的访问。
  • 第 9 行,readValue() 函数给定一个键,从 map 中获得值后返回,该函数会在并发环境中使用,需要保证并发安全。
  • 第 11 行,使用互斥量加锁。
  • 第 13 行,从 map 中获取值。
  • 第 15 行,使用互斥量解锁。
  • 第 17 行,返回获取到的 map 值。


使用 defer 语句对上面的语句进行简化,参考下面的代码。

  1. func readValue(key string) int {
  2. valueByKeyGuard.Lock()
  3. // defer后面的语句不会马上调用, 而是延迟到函数结束时调用
  4. defer valueByKeyGuard.Unlock()
  5. return valueByKey[key]
  6. }

上面的代码中第 6~8 行是对前面代码的修改和添加的代码,代码说明如下:

  • 第 6 行在互斥量加锁后,使用 defer 语句添加解锁,该语句不会马上执行,而是等 readValue() 函数返回时才会被执行。
  • 第 8 行,从 map 查询值并返回的过程中,与不使用互斥量的写法一样,对比上面的代码,这种写法更简单。

2) 使用延迟释放文件句柄

文件的操作需要经过打开文件、获取和操作文件资源、关闭资源几个过程,如果在操作完毕后不关闭文件资源,进程将一直无法释放文件资源,在下面的例子中将实现根据文件名获取文件大小的函数,函数中需要打开文件、获取文件大小和关闭文件等操作,由于每一步系统操作都需要进行错误处理,而每一步处理都会造成一次可能的退出,因此就需要在退出时释放资源,而我们需要密切关注在函数退出处正确地释放文件资源,参考下面的代码:

  1. // 根据文件名查询其大小
  2. func fileSize(filename string) int64 {
  3. // 根据文件名打开文件, 返回文件句柄和错误
  4. f, err := os.Open(filename)
  5. // 如果打开时发生错误, 返回文件大小为0
  6. if err != nil {
  7. return 0
  8. }
  9. // 取文件状态信息
  10. info, err := f.Stat()
  11. // 如果获取信息时发生错误, 关闭文件并返回文件大小为0
  12. if err != nil {
  13. f.Close()
  14. return 0
  15. }
  16. // 取文件大小
  17. size := info.Size()
  18. // 关闭文件
  19. f.Close()
  20. // 返回文件大小
  21. return size
  22. }

代码说明如下:

  • 第 2 行,定义获取文件大小的函数,返回值是 64 位的文件大小值。
  • 第 5 行,使用 os 包提供的函数 Open(),根据给定的文件名打开一个文件,并返回操作文件用的句柄和操作错误。
  • 第 8 行,如果打开的过程中发生错误,如文件没找到、文件被占用等,将返回文件大小为 0。
  • 第 13 行,此时文件句柄 f 可以正常使用,使用 f 的方法 Stat() 来获取文件的信息,获取信息时,可能也会发生错误。
  • 第 16~19 行对错误进行处理,此时文件是正常打开的,为了释放资源,必须要调用 f 的 Close() 方法来关闭文件,否则会发生资源泄露。
  • 第 22 行,获取文件大小。
  • 第 25 行,关闭文件、释放资源。
  • 第 28 行,返回获取到的文件大小。


在上面的例子中,第 25 行是对文件的关闭操作,下面使用 defer 对代码进行简化,代码如下:

  1. func fileSize(filename string) int64 {
  2. f, err := os.Open(filename)
  3. if err != nil {
  4. return 0
  5. }
  6. // 延迟调用Close, 此时Close不会被调用
  7. defer f.Close()
  8. info, err := f.Stat()
  9. if err != nil {
  10. // defer机制触发, 调用Close关闭文件
  11. return 0
  12. }
  13. size := info.Size()
  14. // defer机制触发, 调用Close关闭文件
  15. return size
  16. }

代码中加粗部分为对比前面代码而修改的部分,代码说明如下:

  • 第 10 行,在文件正常打开后,使用 defer,将 f.Close() 延迟调用,注意,不能将这一句代码放在第 4 行空行处,一旦文件打开错误,f 将为空,在延迟语句触发时,将触发宕机错误。
  • 第 16 行和第 22 行,defer 后的语句(f.Close())将会在函数返回前被调用,自动释放资源。

Go语言递归函数

很对编程语言都支持递归函数,Go语言也不例外,所谓递归函数指的是在函数内部调用函数自身的函数,从数学解题思路来说,递归就是把一个大问题拆分成多个小问题,再各个击破,在实际开发过程中,递归函数可以解决许多数学问题,如计算给定数字阶乘、产生斐波系列等。

构成递归需要具备以下条件:

  • 一个问题可以被拆分成多个子问题;
  • 拆分前的原问题与拆分后的子问题除了数据规模不同,但处理问题的思路是一样的;
  • 不能无限制的调用本身,子问题需要有退出递归状态的条件。

注意:编写递归函数时,一定要有终止条件,否则就会无限调用下去,直到内存溢出。

下面通过几个示例来演示一下递归函数的使用。

斐波那契数列

下面我们就以递归函数的经典示例 —— 斐波那契数列为例,演示如何通过Go语言编写的递归函数来打印斐波那契数列。

数列的形式如下所示:

1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, …

使用Go语言递归函数实现斐波那契数列的具体代码如下所示:

  1. package main
  2. import "fmt"
  3. func main() {
  4. result := 0
  5. for i := 1; i <= 10; i++ {
  6. result = fibonacci(i)
  7. fmt.Printf("fibonacci(%d) is: %d\n", i, result)
  8. }
  9. }
  10. func fibonacci(n int) (res int) {
  11. if n <= 2 {
  12. res = 1
  13. } else {
  14. res = fibonacci(n-1) + fibonacci(n-2)
  15. }
  16. return
  17. }

输出结果为:

fibonacci(1) is: 1
fibonacci(2) is: 1
fibonacci(3) is: 2
fibonacci(4) is: 3
fibonacci(5) is: 5
fibonacci(6) is: 8
fibonacci(7) is: 13
fibonacci(8) is: 21
fibonacci(9) is: 34
fibonacci(10) is: 55

Go语言计算函数执行时间

函数的运行时间的长短是衡量这个函数性能的重要指标,特别是在对比和基准测试中,要得到函数的运行时间,最简单的办法就是在函数执行之前设置一个起始时间,并在函数运行结束时获取从起始时间到现在的时间间隔,这个时间间隔就是函数的运行时间。

在Go语言中我们可以使用 time 包中的 Since() 函数来获取函数的运行时间,Go语言官方文档中对 Since() 函数的介绍是这样的。

func Since(t Time) Duration

Since() 函数返回从 t 到现在经过的时间,等价于time.Now().Sub(t)

【示例】使用 Since() 函数获取函数的运行时间。

  1. package main
  2. import (
  3.     "fmt"
  4.     "time"
  5. )
  6. func test() {
  7.     start := time.Now() // 获取当前时间
  8.     sum := 0
  9.     for i := 0; i < 100000000; i++ {
  10.         sum++
  11.     }
  12.     elapsed := time.Since(start)
  13.     fmt.Println("该函数执行完成耗时:", elapsed)
  14. }
  15. func main() {
  16.     test()
  17. }

运行结果如下所示:

该函数执行完成耗时: 39.8933ms

上面我们提到了 time.Now().Sub() 的功能类似于 Since() 函数,想要使用 time.Now().Sub() 获取函数的运行时间只需要把我们上面代码的第 14 行简单修改一下就行。

【示例 2】使用 time.Now().Sub() 获取函数的运行时间。

  1. package main
  2. import (
  3.     "fmt"
  4.     "time"
  5. )
  6. func test() {
  7.     start := time.Now() // 获取当前时间
  8.     sum := 0
  9.     for i := 0; i < 100000000; i++ {
  10.         sum++
  11.     }
  12.     elapsed := time.Now().Sub(start)
  13.     fmt.Println("该函数执行完成耗时:", elapsed)
  14. }
  15. func main() {
  16.     test()
  17. }

运行结果如下所示:

该函数执行完成耗时: 36.8769ms

 

posted @ 2022-04-07 11:00  hanease  阅读(164)  评论(0编辑  收藏  举报