《Go程序设计语言》学习笔记之函数变量和匿名函数

《Go程序设计语言》学习笔记之函数变量和匿名函数

 

一. 环境

  Centos8.5, go1.17.5 linux/amd64

 

二. 函数变量

1. 概念

像其它的值一样,函数变量也有类型,而且它们可以赋给变量或者传递或者从其它函数中返回。函数变量可以像其它函数一样调用。

函数本身不可比较,所以不可以互相进行比较,或者作为键值出现在 map 中。

函数变量使得函数不仅将数据进行参数化,还将函数的行为当作参数进行传递。

 

2. 声明

第 14 行,声明了一个函数变量 f,在第 18 行,对函数变量  f 进行了重新赋值。注意,此时前提是函数 negative 与 函数变量 f 的类型或签名一致,或者更进一步说他们的形参列表和返回列表是相同的。第 16 行、第 20 行,通过打印函数变量 f 的类型,可以证实。

第 22 行,声明了一个函数变量 f2。如果此时,将函数 negative 赋值给它,则会提示 "IncompatibleAssign",即不合适的赋值。

  9 func square(n int) int     { return n * n }
 10 func negative(n int) int   { return -n }
 11 func product(m, n int) int { return m * n }
 12
 13 func main() {
 14     f := square
 15     fmt.Printf("%d\n", f(3))
 16     fmt.Printf("%T\n", f)
 17
 18     f = negative
 19     fmt.Printf("%d\n", f(3))
 20     fmt.Printf("%T\n", f)
 21
 22     f2 := product
 23     fmt.Printf("%d\n", f2(5, 4))
 24     fmt.Printf("%T\n", f2)
 25

  运行结果如下

 

 3. 零值

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

从第 15 行打印结果,可证实函数变量 f 是个空值。第 17 行进行调用,结果就挂了。所以,声明一个空值的函数变量后,记得进行初始化下。

 13 func main() {
 14     var f func(int) int
 15     fmt.Printf("%T\n", f)
 16     fmt.Printf("%t\n", nil == f)
 17     f(3)
 18 }

  运行结果如下

 

 4. 比较

函数变量可以和空值直接比较,但是函数变量之间不可比较。

1) 与空值直接比较

 13 func main() {
 14     var f func(int) int
 15     fmt.Printf("%t\n", nil == f)
 16
 17     if nil != f {
 18         f(3)
 19     } else {
 20         fmt.Printf("f is nil\n")
 21     }
 22 }

  运行结果如下

 

  2) 函数变量之间比较

编译会报错,提示:"函数变量 f 只能与 nil 比较"

   13 func main() {
   14     f := square
   15     f2 := negative
   16
   17     fmt.Printf("%T\n", f == f2)
   18 }

  编译报错

invalid operation: f == f2 (func can only be compared to nil)

  

三. 匿名函数

1. 概念

 命名函数只能在包级别的作用域进行声明,但我们能够使用函数字面量在任何表达式内指定函数变量。函数字面量就像函数声明,但在 func 关键字后面没有函数的名称。它是一个表达式,它的值称作匿名函数。

 

2. 特点

1) 函数字面量在我们需要使用的时候才定义

内置包 strings 中有几个这样的例子如 strings.Map()等,这里以 strings.IndexFunc() 为例,它返回在字符串中第一个符合匿名函数的码点值的索引。

 n := strings.IndexFunc("Hello, 世界", func(c rune) bool { return unicode.Is(unicode.Han, c) })

  

2) 以这种方式定义的函数能够获取到整个词法环境,因此里层的函数可以使用外层函数中的变量。

这里在书中的例子基础上增加了点内容,用于对比。

函数 Increase() 返回了一个匿名函数。而在匿名函数中,递增了 x 。每次调用函数 Increase() ,即执行 f(),f 是有状态的,它保存了 x 的当前的最新值,这通过打印 x 值可知。里层的匿名函数能够获取和更新外层函数 Increase() 函数的局部变量。当然,也可以改变包的全局变量 y 。

个人感觉,使用匿名函数的时候,匿名函数外层函数中的变量对于匿名函数来讲,就像它的全局变量一样。这样,就不用声明那么多包级别的全局变量了,感觉还挺方便。

  8 var y int = 10
  9
 10 func Increase() func() int {
 11     var x int
 12     return func() int {
 13         x++
 14         y++
 15         fmt.Printf("x: %d, y: %d\n", x, y)
 16
 17         return x * x
 18     }
 19 }
 20
 21 func main() {
 22     f := Increase()
 23     fmt.Printf("%d\n", f())
 24     fmt.Printf("----------\n")
 25     fmt.Printf("%d\n", f())
 26     fmt.Printf("----------\n")
 27     fmt.Printf("%d\n", f())
 28     fmt.Printf("----------\n")
 29     fmt.Printf("%d\n", f())
 30     fmt.Printf("----------\n")
 31 }

  运行结果如下

 

3. 注意

捕获迭代变量

 为了观察现象,笔者在书中的例子上修改了下。将 tempDirs 直接修改为了一个 slice ,并明指明出了文件名。在每次遍历 tempDirs 时,中间停顿了3秒(当然,也可以修改更大的)。这样在这个间隙时,就可以去指定目录下观察是否创建了相应目录,是否将该目录删除了。

  4 import (
  5     "os"
  6     "time"
  7 )
  8
  9 func main() {
 10     tempDirs := []string{"./aa/aaa", "./bb/bbb", "./cc/ccc"}
 11
 12     var rmdirs []func()
 13
 14     for _, d := range tempDirs {
 15         dir := d
 16         os.MkdirAll(dir, 0755)
 17         time.Sleep(3 * time.Second)
 18         rmdirs = append(rmdirs, func() { os.RemoveAll(dir) })
 19
 20     }
 21
 22     for _, rmdir := range rmdirs {
 23         rmdir()
 24     }
 25
 26 }
~

  运行结果如下

没有打印结果。但是可以在另一个命令行窗口,可以看到 aa目录下有创建和删除aaa目录,另外对 bb、cc目录也是一样地进行观察,结果均符合预期。

第15 行代码,每次循环时,将遍历到的元素 d 赋值给了一个局部变量 dir,后面对局部变量 dir 进行操作。试试,如果直接对遍历到的元素 d 进行操作呢?感觉那样代码会更简洁。

调整代码如下:

  9 func main() {
 10     tempDirs := []string{"./aa/aaa", "./bb/bbb", "./cc/ccc"}
 11
 12     var rmdirs []func()
 13
 14     for _, d := range tempDirs {
 15         //dir := d
 16         //os.MkdirAll(dir, 0755)
 17         os.MkdirAll(d, 0755)
 18         time.Sleep(3 * time.Second)
 19         //rmdirs = append(rmdirs, func() { os.RemoveAll(dir) })
 20         rmdirs = append(rmdirs, func() { os.RemoveAll(d) })
 21
 22     }
 23
 24     for _, rmdir := range rmdirs {
 25         rmdir()
 26     }
 27
 28 }

  运行结果如下

没有打印结果。但是在另一个命令行窗口,可以观察到 aa 目录下仍有 目录 aaa,bb 目录下仍有目录 bbb,只有 cc 目录下的目录删除了,是空的。

此时,程序并没有按照预期进行。

d 是在for循环引入的一个块作用域内进行声明的。在 for 循环中创建的所有函数变量共享相同的变量 -- 一个可访问的储存位置,而不是固定的值。变量 d 在每次迭代中都在不断更新。当在最后删除创建的目录时,实际上删除的都是最后一次创建的目录。也就是说 rmdirs 中的每个元素要删除的目录都是一样的,均是最后一次创建的目录。

即使使用下标也是会存在这样的问题。

posted @ 2022-01-23 23:11  bruce628  阅读(72)  评论(0编辑  收藏  举报