《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 中的每个元素要删除的目录都是一样的,均是最后一次创建的目录。

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

《Go程序设计语言》学习笔记之函数变量和匿名函数的更多相关文章

  1. Go语言学习笔记二: 变量

    Go语言学习笔记二: 变量 今天又学了一招如何查看go的版本的命令:go version.另外上一个笔记中的代码还可以使用go run hello.go来运行,只是这种方式不会生成exe文件. 定义变 ...

  2. C语言学习笔记:12_变量的存储方式和生存期

    /* * 12_变量的存储方式和生存期.c * * Created on: 2015年7月5日 * Author: zhong */ #include <stdio.h> #include ...

  3. 吴裕雄--天生自然C++语言学习笔记:C++ 变量作用域

    作用域是程序的一个区域,一般来说有三个地方可以定义变量: 在函数或一个代码块内部声明的变量,称为局部变量. 在函数参数的定义中声明的变量,称为形式参数. 在所有函数外部声明的变量,称为全局变量. 局部 ...

  4. 吴裕雄--天生自然C++语言学习笔记:C++ 变量类型

    变量其实只不过是程序可操作的存储区的名称.C++ 中每个变量都有指定的类型,类型决定了变量存储的大小和布局,该范围内的值都可以存储在内存中,运算符可应用于变量上. 变量的名称可以由字母.数字和下划线字 ...

  5. C程序设计语言学习笔记

    在Windows下运行C语言程序 Windows下的编程工具使用 VC 6.0,下面讲解如何在VC 6.0下运行上节的"Hello, world"程序. 1) 新建Win32 Co ...

  6. C语言学习笔记:14_内部函数和外部函数

    /* * 14_内部函数和外部函数.c * * Created on: 2015年7月5日 * Author: zhong */ #include <stdio.h> #include & ...

  7. C语言学习笔记 (005) - 二维数组作为函数参数传递剖析

    前言 很多文章不外乎告诉你下面这几种标准的形式,你如果按照它们来用,准没错: //对于一个2行13列int元素的二维数组 //函数f的形参形式 f(int daytab[2][13]) {...} / ...

  8. R语言学习笔记(七): 排序函数:sort(), rank(), order()

    sort() sort()函数直接对函数进行排序,并返回排序结果. > a <- c(12,4,6,5) > sort(a) [1] 4 5 6 12 rank() rank()函数 ...

  9. Go语言学习笔记七: 函数

    Go语言学习笔记七: 函数 Go语言有函数还有方法,神奇不.这有点像python了. 函数定义 func function_name( [parameter list] ) [return_types ...

  10. 大一C语言学习笔记(5)---函数篇-定义函数需要了解注意的地方;定义函数的易错点;详细说明函数的每个组合部分的功能及注意事项

    博主学习C语言是通过B站上的<郝斌C语言自学教程>,对于C语言初学者来说,我认为郝斌真的是在全网C语言学习课程中讲的最全面,到位的一个,这个不是真不是博主我吹他哈,大家可以去B站去看看,C ...

随机推荐

  1. NC25084 [USACO 2006 Nov S]Bad Hair Day

    题目 题目描述 Some of Farmer John's N cows (1 ≤ N ≤ 80,000) are having a bad hair day! Since each cow is s ...

  2. NC15445 wyh的吃鸡

    题目链接 题目 题目描述 最近吃鸡游戏非常火,你们wyh学长也在玩这款游戏,这款游戏有一个非常重要的过程,就是要跑到安全区内,否则就会中毒持续消耗血量,我们这个问题简化如下 假设地图为n*n的一个图, ...

  3. Springboot中如何使用日志框架logback和log4j2?

    说明 在这个简短的教程中,我们将探索 Spring Boot 中可用的主要日志记录选项:logback和log4j2 初始设置 使用 starters 时,默认使用 Logback 进行日志记录. 让 ...

  4. ORACLE cannot fetch plan for SQL_ID

    今天做SQL执行计划测试的时候,发现sqlplus无法正常打印执行计划,根据网上资料整理如下: ..... SYS@orcl> select *   2     from table(   3 ...

  5. 【Android 逆向】【ARM汇编】 函数的栈帧

    1. 函数的调用约定 ARM32 参数1-4 放入r0-r3 剩下的入栈,函数返回值放入r0 ARM64 参数1-8 放入X0-X7 剩下的入栈,函数返回值放入X0 (浮点数是放入 Dn 或 Sn) ...

  6. BUUCTF [强网杯 2019]随便注 1

    1. 拿到题目,先输入一个1'试一下是否存在注入点 报错 error 1064 : You have an error in your SQL syntax; check the manual tha ...

  7. 【WiFi开发全攻略】WIFI基础知识大全

    [WiFi开发全攻略]WIFI基础知识大全 1. Wi-Fi起源 现在我们大家对Wi-Fi肯定都不陌生,无论是笔记本,手机,智能电视,都离不开Wi-Fi.目前我们一般用的都是Wi-Fi5,Wi-Fi6 ...

  8. vue 项目npm run dev(启动)时报错The service was stopped

    vue项目yarn upgrade后vite build报错,如何项目也运行不起来了. 报错截图: 解决办法: 删除node_modules文件夹,然后执行yarn install重新生成心的node ...

  9. VUE 腾讯云 web端上传视频SDK 上传进度无法显示

    上传视频官方文档:https://cloud.tencent.com/document/product/266/9239 错误信息 在本地调试可以显示视频上传进度,也可以打印到浏览器控制台.但是,发布 ...

  10. spark 下java list 或者scala list 转DataFrame or DataSet 总结

    一.JAVA list 转 DataFrame or DataSet case class CaseJava( var num: String, var id: String, var start_t ...