我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Go 中闭包的底层原理

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Go 中闭包的底层原理

1. 什么是闭包?

一个函数内引用了外部的局部变量,这种现象,就称之为闭包。

例如下面的这段代码中,adder 函数返回了一个匿名函数,而该匿名函数中引用了 adder 函数中的局部变量 sum ,那这个函数就是一个闭包。


package main 
 
import "fmt" 
 
func adder() func(int) int { 
    sum := 0 
    return func(x int) int { 
        sum += x 
        return sum 
    } 
} 

而这个闭包中引用的外部局部变量并不会随着 adder 函数的返回而被从栈上销毁。

我们尝试着调用这个函数,发现每一次调用,sum 的值都会保留在 闭包函数中以待使用。


func main() { 
     valueFunc:= adder() 
     fmt.Println(valueFunc(2))     // output: 2 
     fmt.Println(valueFunc(2))   // output: 4 
} 

2. 复杂的闭包场景

写一个闭包是比较容易的事,但单单会写简单的闭包函数,还远远不够,如果不搞清楚闭包真正的原理,那很容易在一些复杂的闭包场景中对函数的执行逻辑进行误判。

别的不说,就拿下来这个例子来说吧?

你觉得它会打印什么呢?

是 6 还是 11 呢?


import "fmt" 
 
func func1() (i int) { 
    i = 10 
    defer func() { 
        i += 1 
    }() 
    return 5 
} 
 
func main() { 
    closure := func1() 
    fmt.Println(closure) 
} 

3. 闭包的底层原理?

还是以最上面的例子来分析


package main 
 
import "fmt" 
 
func adder() func(int) int { 
    sum := 0 
    return func(x int) int { 
        sum += x 
        return sum 
    } 
} 
 
func main() { 
    valueFunc:= adder() 
    fmt.Println(valueFunc(2))     // output: 2 
} 

我们先对它进行逃逸分析,很容易发现 sum 作为 adder 函数局部变量,并不是分配在栈上,而是分配在堆上的。

这就解决了第一个疑惑:为什么 adder 函数返回后, sum 不会随之销毁?


$ go build -gcflags="-m -m -l" demo.go 
# command-line-arguments 
./demo.go:8:3: adder.func1 capturing by ref: sum (addr=true assign=true width=8) 
./demo.go:7:9: func literal escapes to heap: 
./demo.go:7:9:   flow: ~r0 = &{storage for func literal}: 
./demo.go:7:9:     from func literal (spill) at ./demo.go:7:9 
./demo.go:7:9:     from return func literal (return) at ./demo.go:7:2 
./demo.go:6:2: sum escapes to heap: 
./demo.go:6:2:   flow: {storage for func literal} = &sum: 
./demo.go:6:2:     from func literal (captured by a closure) at ./demo.go:7:9 
./demo.go:6:2:     from sum (reference) at ./demo.go:8:3 
./demo.go:6:2: moved to heap: sum 
./demo.go:7:9: func literal escapes to heap 
./demo.go:15:23: valueFunc(2) escapes to heap: 
./demo.go:15:23:   flow: {storage for ... argument} = &{storage for valueFunc(2)}: 
./demo.go:15:23:     from valueFunc(2) (spill) at ./demo.go:15:23 
./demo.go:15:23:   flow: {heap} = {storage for ... argument}: 
./demo.go:15:23:     from ... argument (spill) at ./demo.go:15:13 
./demo.go:15:23:     from fmt.Println(valueFunc(2)) (call parameter) at ./demo.go:15:13 
./demo.go:15:13: ... argument does not escape 
./demo.go:15:23: valueFunc(2) escapes to heap 

可另一个问题,又浮现出来了,就算它不会销毁,那闭包函数若是存储的若是 sum 拷贝后的值,那每次调用闭包函数,里面的 sum 应该都是一样的,调用两次都应该返回 2,而不是可以累加记录。

因此,可以大胆猜测,闭包函数的结构体里存储的是 sum 的指针。

为了验证这一猜想,只能上汇编了。

通过执行下面的命令,可以输出对应的汇编代码


go build -gcflags="-S" demo.go  

输出的内容相当之多,我提取出下面最关键的一行代码,它定义了闭包函数的结构体。

其中 F 是函数的指针,但这不是重点,重点是 sum 存储的确实是指针,验证了我们的猜。


type.noalg.struct { F uintptr; "".sum *int }(SB), CX 

4. 迷题揭晓

有了上面第三节的背景知识,那对于第二节给出的这道题,想必你也有答案了。

首先,由于 i 在函数定义的返回值上声明,因此根据 go 的 caller-save 模式, i 变量会存储在 main 函数的栈空间。

然后,func1 return 重新把 5 赋值给了 i ,此时 i = 5

由于闭包函数存储了这个变量 i 的指针。

因此最后,在 defer 中对 i 进行自增,是直接更新到 i 的指针上,此时 i = 5+1,所以最终打印出来的结果是 6


import "fmt" 
 
func func1() (i int) { 
    i = 10 
    defer func() { 
        i += 1 
    }() 
    return 5 
} 
 
func main() { 
    closure := func1() 
    fmt.Println(closure) 
} 

5. 再度变题

上面那题听懂了的话,再来看看下面这道题。

func1 的返回值我们不写变量名 i 了,然后原先返回具体字面量,现在改成变量 i ,就是这两小小小的改动,会导致运行结果大大不同,你可以思考一下结果。


import "fmt" 
 
func func1() (int) { 
    i := 10 
    defer func() { 
        i += 1 
    }() 
    return i 
} 
 
func main() { 
    closure := func1() 
    fmt.Println(closure) 
} 

如果你在返回值里写了变量名,那么该变量会存储 main 的栈空间里,而如果你不写,那 i 只能存储在 func1 的栈空间里,与此同时,return 的值,不会作用于原变量 i 上,而是会存储在该函数在另一块栈内存里。

因此你在 defer 中对原 i 进行自增,并不会作用到 func1 的返回值上。

所以打印的结果,只能是 10。

你答对了吗?

6. 最后一个问题

不知道你有没有发现,在第一节示例中的 sum 是存储在堆内存中的,而后面几个示例都是存储在栈内存里。

这是为什么呢?

仔细对比,不难发现,示例一返回的是闭包函数,闭包函数在 adder 返回后还要在其他地方继续使用,在这种情况下,为了保证闭包函数的正常运行,无论闭包函数在哪里,i 都不能回收,所以 Go 编译器会智能地将其分配在堆上。

而后面的其他示例,都只是涉及了闭包的特性,并不是直接把闭包函数返回,因此完全可以将其分配在栈上,非常的合理。

到此这篇关于Go 中闭包的底层原理的文章就介绍到这了,更多相关Go 闭包底层原理内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Go 中闭包的底层原理

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Go中闭包的底层原理是什么

这篇文章将为大家详细讲解有关Go中闭包的底层原理是什么,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。1. 什么是闭包?一个函数内引用了外部的局部变量,这种现象,就称之为闭包。例如下面的这段代码中,adde
2023-06-25

go语言中slice,map,channl底层原理

目录0. 前序1. slice1.1 slice的创建1.2 数据结构1.3 扩容机制2. map2.1 map创建2.2 数据结构2.3 扩容机制3. channl3.1 数据结构3.2 过程详解0. 前序 slice,map,chann
2022-06-07

Go 中函数类型的底层原理是什么?

go 中的函数类型是一个具有输入参数类型和输出返回类型的元组。函数类型可以作为值或引用传递,默认情况下作为值传递,显式作为引用传递需要使用 *。在实战中,函数类型可用于创建可重用的函数,例如将函数作为参数传递给其他函数。Go 中函数类型的底
Go 中函数类型的底层原理是什么?
2024-04-19

Go语言底层编程原理解析

Go语言底层编程原理解析Go语言作为一门快速发展的编程语言,越来越受到开发者的青睐。虽然Go语言以其简洁、高效的特性而闻名,但是很多开发者对于Go语言底层的编程原理并不是非常了解。本文将从Go语言底层编程的角度出发,解析一些底层编程原理,
Go语言底层编程原理解析
2024-03-13

Go语言底层实现原理揭秘

Go语言作为一种高效、简洁且易于学习的编程语言,受到了许多开发者的青睐。但是,作为使用者,了解底层实现原理往往能够让我们更好地应对各种情况,优化代码性能。本文将深入探讨Go语言的底层实现原理,并结合具体的代码示例来解释相关概念。首先,需要
Go语言底层实现原理揭秘
2024-03-13

go语言中slice,map,channl底层原理是什么

今天小编给大家分享一下go语言中slice,map,channl底层原理是什么的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。
2023-06-30

深入了解Go的interface{}底层原理实现

目录1. interface{}初探2. eface3. iface4. 接口转化1. interface{}初探 Go是强类型语言,各个实例变量的类型信息正是存放在interface{}中的,Go中的反射也与其底层结构有关。 iface
2022-06-07

探索Go语言底层原理:底层语言的真实身份揭晓!

探索Go语言底层原理:底层语言的真实身份揭晓!Go语言作为一门高效、简洁、快速发展的编程语言,备受程序员们的喜爱。但是,对于许多开发者来说,Go语言的底层实现和原理仍然是一个神秘的领域。本文将带领大家深入探索Go语言底层的真实身份,揭开底
探索Go语言底层原理:底层语言的真实身份揭晓!
2024-03-08

Go语言中并发goroutine底层原理的示例分析

小编给大家分享一下Go语言中并发goroutine底层原理的示例分析,希望大家阅读完这篇文章之后都有所收获,下面让我们一起去探讨吧!一、基本概念①并发、并行区分1.概念并发:同一时间段内一个对象执行多个任务,充分利用时间并行:同一时刻,多个
2023-06-29

HashMap的底层实现原理

这篇文章主要介绍“HashMap的底层实现原理”,在日常操作中,相信很多人在HashMap的底层实现原理问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”HashMap的底层实现原理”的疑惑有所帮助!接下来,请跟
2023-06-04

Go语言底层实现原理揭秘:底层语言究竟是什么?

Go语言底层实现原理揭秘:底层语言究竟是什么?在计算机科学领域中,底层语言通常指的是可以直接与硬件交互的编程语言,它可以更加精细地控制计算机的底层资源,包括内存、寄存器等。作为一种高级编程语言,Go语言在应用层提供了强大简洁的特性,但是G
Go语言底层实现原理揭秘:底层语言究竟是什么?
2024-03-07

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录