[TOC]
# 闭包
## 简介
**作用:缩小变量作用域,减少对全局变量的污染**
闭包又是什么?你可以想象一下,在一个函数中存在对外来标识符的引用。所谓的外来标识符,既不代表当前函数的任何参数或结果,也不是函数内部声明的,它是直接从外边拿过来的
![](https://box.kancloud.cn/ac6a5657ad6e6e2edbae512c7788dea6_1342x662.png)
一个函数捕获了和他在同一个作用域的其他常量和变量.这就意味着当闭包被调用的时候,不管在程序什么地方调用,闭包能够使用这些常量或者变量.
它不关心这些捕获了的变量和常量是否已经超出了作用域,所以只有闭包还在使用他,这些变量就还会存在.
在go里面,所有的匿名函数都是闭包
~~~
func main() {
a := 10
str := "mike"
//匿名函数,没有函数名字,函数定义没有调用
f1 := func() {
fmt.Println("a = ", a)
fmt.Println("str = ", str)
}
//调用
f1()
//给一个函数类型起别名
type FuncType func() //函数没有参数没有返回值
//声明变量
var f2 FuncType
f2 = f1
f2()
//定义匿名函数,同时调用
func() {
fmt.Printf("a = %d, str = %s\n", a, str)
}() //后面的()代表调用此匿名函数
//带参数的匿名函数
f3 := func(i, j int) {
fmt.Printf("a = %d, str = %s\n", a, str)
}
f3(1, 2)
//有参数有返回值
x, y := func(i, j int) (max, min int) {
if i > j {
max = i
min = j
} else {
max = j
min = i
}
return
}(10, 20)
fmt.Print(x, y)
}
~~~
**闭包以引用的方式捕获外部变量**
~~~
func main() {
a := 10
str := "nike"
func() {
a = 666
str = "go"
fmt.Printf("内部: a = %d, str = %s\n", a, str)
}() //()代表直接调用
fmt.Printf("外部: a = %d, str =%s\n", a, str)
}
~~~
输出
~~~
内部: a = 666, str = go
外部: a = 666, str =go
~~~
**闭包保存变量**
~~~
func test02() func() int {
var x int //没有初始化,值为0
return func() int {
x++
return x * x
}
}
func main() {
//返回函数类型
f := test02()
fmt.Println(f())
fmt.Println(f())
fmt.Println(f())
}
~~~
## 使用
> Go 函数可以是一个闭包。闭包是一个函数值,它引用了函数体之外的变量。 这个函数可以对这个引用的变量进行访问和赋值;换句话说这个函数被“绑定”在这个变量上。
例如,函数 adder 返回一个闭包。每个返回的闭包都被绑定到其各自的 sum 变量上。
在上面例子中(这里重新贴下代码,和上面代码一样):
~~~
package main
import "fmt"
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
func main() {
pos, neg := adder(), adder()
for i := 0; i < 10; i++ {
fmt.Println(
pos(i),
neg(-2*i),
)
}
}
~~~
如pos := adder()的adder()表示返回了一个闭包,并赋值给了pos,同时,这个被赋值给了pos的闭包函数被绑定在sum变量上,因此pos闭包函数里的变量sum和neg变量里的sum毫无关系。
`func adder() func(int) int`的`func(int) int`表示adder()的输出值的类型是func(int) int这样一个函数
没有闭包的时候,函数就是一次性买卖,函数执行完毕后就无法再更改函数中变量的值(应该是内存释放了);有了闭包后函数就成为了一个变量的值,只要变量没被释放,函数就会一直处于存活并独享的状态,因此可以后期更改函数中变量的值(因为这样就不会被go给回收内存了,会一直缓存在那里)。
比如,实现一个计算功能:一个数从0开始,每次加上自己的值和当前循环次数(当前第几次,循环从0开始,到9,共10次),然后\*2,这样迭代10次:
没有闭包的时候这么写:
~~~
func abc(x int) int {
return x * 2
}
func main() {
var a int
for i := 0; i < 10; i ++ {
a = abc(a+i)
fmt.Println(a)
}
}
~~~
如果用闭包可以这么写:
~~~
func abc() func(int) int {
res := 0
return func(x int) int {
res = (res + x) * 2
return res
}
}
func main() {
a := abc()
for i := 0; i < 10; i++ {
fmt.Println(a(i))
}
}
~~~
2种写法输出值都是:
~~~
0
2
8
22
52
114
240
494
1004
2026
~~~
从上面例子可以看出闭包的3个好处:
1. 不是一次性消费,被引用声明后可以重复调用,同时变量又只限定在函数里,同时每次调用不是从初始值开始(函数里长期存储变量)
这有点像使用面向对象的感觉,实例化一个类,这样这个类里的所有方法、属性都是为某个人私有独享的。但比面向对象更加的轻量化
2. 用了闭包后,主函数就变得简单了,把算法封装在一个函数里,使得主函数省略了a=abc(a+i)这种麻烦事了
3. 变量污染少,因为如果没用闭包,就会为了传递值到函数里,而在函数外部声明变量,但这样声明的变量又会被下面的其他函数或代码误改。
关于闭包的第一个好处,再啰嗦举个例子
1. 若不用闭包,则容易对函数外的变量误操作(误操作别人),例:
~~~
var A int = 1
func main() {
foo := func () {
A := 2
fmt.Println(A)
}
foo()
fmt.Println(A)
}
~~~
输出:
~~~
2
1
~~~
如果手误将A := 2写成了A = 2,那么输出就是:
~~~
2
2
~~~
即会影响外部变量A
2. 为了将某一个私有的值传递到某个函数里,就需要在函数外声明这个值,但是这样声明会导致这个值在其他函数里也可见了(别人误操作我),例:
~~~
func main() {
A := 1
foo := func () int {
return A + 1
}
B := 1
bar := func () int {
return B + 2
}
fmt.Println(foo())
fmt.Println(bar())
}
~~~
输出:
~~~
2
3
~~~
在bar里是可以对变量A做操作的,一个不小心就容易误修改变量A
**结论:函数外的变量只能通过参数传递进去,不要通过全局变量的方式的渠道传递进去,当函数内能读取到的变量越多,出错概率(误操作)也就越高。**
- 基础
- 简介
- 主要特征
- 变量和常量
- 编码转换
- 数组
- byte与rune
- big
- sort接口
- 和mysql类型对应
- 函数
- 闭包
- 工作区
- 复合类型
- 指针
- 切片
- map
- 结构体
- sync.Map
- 随机数
- 面向对象
- 匿名组合
- 方法
- 接口
- 权限
- 类型查询
- 异常处理
- error
- panic
- recover
- 自定义错误
- 字符串处理
- 正则表达式
- json
- 文件操作
- os
- 文件读写
- 目录
- bufio
- ioutil
- gob
- 栈帧的内存布局
- shell
- 时间处理
- time详情
- time使用
- new和make的区别
- container
- list
- heap
- ring
- 测试
- 单元测试
- Mock依赖
- delve
- 命令
- TestMain
- path和filepath包
- log日志
- 反射
- 详解
- plugin包
- 信号
- goto
- 协程
- 简介
- 创建
- 协程退出
- runtime
- channel
- select
- 死锁
- 互斥锁
- 读写锁
- 条件变量
- 嵌套
- 计算单个协程占用内存
- 执行规则
- 原子操作
- WaitGroup
- 定时器
- 对象池
- sync.once
- 网络编程
- 分层模型
- socket
- tcp
- udp
- 服务端
- 客户端
- 并发服务器
- Http
- 简介
- http服务器
- http客户端
- 爬虫
- 平滑重启
- context
- httptest
- 优雅中止
- web服务平滑重启
- beego
- 安装
- 路由器
- orm
- 单表增删改查
- 多级表
- orm使用
- 高级查询
- 关系查询
- SQL查询
- 元数据二次定义
- 控制器
- 参数解析
- 过滤器
- 数据输出
- 表单数据验证
- 错误处理
- 日志
- 模块
- cache
- task
- 调试模块
- config
- 部署
- 一些包
- gjson
- goredis
- collection
- sjson
- redigo
- aliyunoss
- 密码
- 对称加密
- 非对称加密
- 单向散列函数
- 消息认证
- 数字签名
- mysql优化
- 常见错误
- go run的错误
- 新手常见错误
- 中级错误
- 高级错误
- 常用工具
- 协程-泄露
- go env
- gometalinter代码检查
- go build
- go clean
- go test
- 包管理器
- go mod
- gopm
- go fmt
- pprof
- 提高编译
- go get
- 代理
- 其他的知识
- go内存对齐
- 细节总结
- nginx路由匹配
- 一些博客
- redis为什么快
- cpu高速缓存
- 常用命令
- Go 永久阻塞的方法
- 常用技巧
- 密码加密解密
- for 循环迭代变量
- 备注
- 垃圾回收
- 协程和纤程
- tar-gz
- 红包算法
- 解决golang.org/x 下载失败
- 逃逸分析
- docker
- 镜像
- 容器
- 数据卷
- 网络管理
- 网络模式
- dockerfile
- docker-composer
- 微服务
- protoBuf
- GRPC
- tls
- consul
- micro
- crontab
- shell调用
- gorhill/cronexpr
- raft
- go操作etcd
- mongodb