Golang原理分析:闭包及for range延迟绑定问题原理及解决

news2024/11/17 18:27:18

在这里插入图片描述

1.Golang中的闭包

1.1.什么是闭包

当一个函数引用了环境的变量,被引用的变量与该函数同时存在组成的系统,被称为闭包。
闭包 = 环境的变量 + 函数。
在这里插入图片描述

以下JavaScript代码展示了一个基础的闭包:

  • name是init函数中的内部变量
  • displayName()是init函数中定义的函数
  • displayName()函数引用了name变量
  • displayName() = name + func() = 环境的变量 + 函数,所以displayName是一个引用了name变量的闭包
function init() {
    var name = "something"; 
    function displayName() { 
        alert(name); 
    }
    displayName();
}
init();

1.2.Golang中使用闭包

Golang中天然支持闭包(函数是一等公民),下面看一个简单的case:

func someMethod() {
    // 准备一个字符串
    str := "hello world"
    // 创建一个匿名函数
    foo := func() {
        // 匿名函数中访问str
        str = "hello dude"
    }
    // 调用匿名函数
    foo()
}
  • 第3行,准备一个字符串用于修改
  • 第5行,创建一个匿名函数
  • 第8行,在匿名函数中并没有定义 str,str 的定义在匿名函数之前,此时,str 就被引用到了匿名函数中形成了闭包
  • 第10行,执行闭包,此时 str 发生修改,变为 hello dude。

1.3.闭包的特性

1.3.1.记忆特性(环境绑定)

闭包可以记忆创建时的环境,并且一直保持对该环境的绑定,直到闭包被销毁。

一个累加器的例子:

// 提供一个值, 每次调用函数会指定对值进行累加
func Accumulate(value int) func() int {
   return func() int {
      value++
      return value
   }
}
func testAccumulator() {
   // 创建一个累加器, 初始值为1
   accumulator := Accumulate(1)
   fmt.Printf("%p\n", &accumulator) // 0xc00000e028
   fmt.Println(accumulator())    // 2
   fmt.Println(accumulator())    // 3
   
   // 创建一个累加器, 初始值为10
   accumulator2 := Accumulate(10)
   fmt.Printf("%p\n", &accumulator2) // 0xc00000e038
   fmt.Println(accumulator2()) // 11
}
  • Accumulate函数会创建一个绑定value值的闭包
  • 第一次创建该闭包指定value为1,函数地址为0xc00000e028,进行两次闭包调用可以发现值依次增长为2和3
  • 第二次创建该闭包指定value为10,函数地址为0xc00000e038,进行一次闭包调用可以发现值变为11
  • 以上测试可见,两次生成的闭包依次绑定了自身的环境,并且一直保持绑定

生成指定后缀文件名称的例子:

func BuildFileSuffix(suffix string) func(fileName string) string {
   return func(fileName string) string {
      // 闭包绑定环境变量suffix
      // 闭包自身输入fileName,组成完成文件名
      return fmt.Sprintf("%v.%v", fileName, suffix)
   }
}

func testBuildFileSuffix() {
   // 后缀名java
   javaFunc := BuildFileSuffix("java")
   fmt.Println(javaFunc("file1")) // file1.java
   fmt.Println(javaFunc("file2")) // file2.java
   // 后缀名golang
   golangFunc := BuildFileSuffix("golang")
   fmt.Println(golangFunc("file3"))   // file3.golang
   fmt.Println(golangFunc("file4"))   // file4.golang
}

1.3.2.延迟绑定

闭包会在使用时实际读取环境变量值,而不是取生成闭包时的环境变量值。

下面的例子里在闭包f生成时x的值为1,然后将x值设置为2,此时返回闭包。闭包在使用时x值已经变为2,所以打印的值为2。

func DelayBidding() func() {
   x := 1
   f := func() {
      fmt.Println(x)
   }
   x = 2
   return f
}

func testDelayBidding() {
   DelayBidding()() // 2
}

2.for range延迟绑定问题

在Golang中使用for range时,会存在闭包的延迟绑定问题,该问题是Golang经典问题,在开发时也该格外注意。

2.1.for range复用

for range的惯用法是使用短变量声明方式(:=)在for的initStmt中声明迭代变量(iteration variable)。但需要注意的是,这些迭代变量在for range的每次循环中都会被重用,而不是重新声明:

func forRange() {
   list := []int{1, 2, 3, 4, 5}
   for i, v := range list {
      fmt.Printf("%v-%v\n", &i, &v)
   }
}

输出结果为:

0xc000094008-0xc000094010
0xc000094008-0xc000094010
0xc000094008-0xc000094010
0xc000094008-0xc000094010
0xc000094008-0xc000094010

可见,在使用for range时,生成的i和v都复用的是同一变量,所以for range表达式又可等价为:

func forRange() {
   list := []int{1, 2, 3, 4, 5}
   var i, v int
   for i, v = range list {
      fmt.Printf("%v-%v\n", &i, &v)
   }
}

2.2.for range延迟绑定

所以在for range中使用闭包时,会存在延迟绑定问题,因为i和v本质上是在被复用的,所以闭包被调用时会获取到i和v的最新值:

func TestForRangeDelayBidding() {
   list := []int{1, 2, 3, 4, 5}
   var funcList []func()
   for _, v := range list {
      f := func() {
         fmt.Println(v)
      }
      funcList = append(funcList, f)
   }
   for _, f := range funcList {
      f()
   }
}

以上结果预期为1,2,3,4,5,实际输出值为5,5,5,5,5。
再看一个使用go runtine的闭包的例子:

func TestForRangeGoRuntineDelayBidding() {
   list := []int{1, 2, 3, 4, 5}
   var wg sync.WaitGroup
   for _, v := range list {
      wg.Add(1)
      go func() {
         defer wg.Done()
         fmt.Println(v)
      }()
   }
   wg.Wait()
}

以上结果预期为1,2,3,4,5,实际输出值为5,5,5,5,5。

2.3.如何避免延迟绑定

for range本质上是复用了i和v,所以避免for range闭包延迟绑定的方式就是不让其复用i和v:

  • 第一种方式,在闭包声明前,再创建一个新值给闭包引用,不复用原有值
func TestForRange1() {
   list := []int{1, 2, 3, 4, 5}
   var wg sync.WaitGroup
   for _, v := range list {
      wg.Add(1)
      // 创建一个新值v2,v2不会被复用
      v2 := v
      go func() {
         defer wg.Done()
         // 闭包引用v2
         fmt.Println(v2)
      }()
   }
   wg.Wait()
}
  • 第二种方式,闭包声明时在指定传入参数,此时发生值拷贝,不复用原有值
func TestForRange2() {
   list := []int{1, 2, 3, 4, 5}
   var wg sync.WaitGroup
   for _, v := range list {
      wg.Add(1)
      // 将值拷贝传入闭包
      go func(v2 int) {
         defer wg.Done()
         fmt.Println(v2)
      }(v)
   }
   wg.Wait()
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/96455.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

机器学习 鸢尾花(Iris Flower)数据集分析

目录 一:加载数据 二:提取特征数据 三:提取标签数据 四:数据划分 一:加载数据 加载数据,查看数据特征 from sklearn.datasets import load_iris# 1 加载数据 鸢尾花load_iris iris_datasets load_iri…

数据校验及在数据校验的情况下增加headers拿回数据

什么是数据校验: 当你向一个数据端口请求数据时,如果这个数据端口没有设置:Access-Control-Allow-Origin:*,那就是存在跨域限制了,你是拿不回来数据的。图示: 但是有些数据端口是设置了 Access…

【JS ES6】了解学习set类型和weakset类型

✍️ 作者简介: 前端新手学习中。 💂 作者主页: 作者主页查看更多前端教学 🎓 专栏分享:css重难点教学 Node.js教学 从头开始学习 ajax学习 目录set类型与array和object的区别set元素检测与管理类型转换的重要性遍历set类型的方式使用set…

Spring - 事件监听机制 源码解析

文章目录Pre概述ApplicationEvent ------ 事件ApplicationListener ------ 事件监听器ApplicationEventPublisher ------ 事件发布者ApplicationEventMulticaster ------ 事件广播器spring主要的内置事件ContextRefreshedEventContextStartedEventContextStoppedEventContextCl…

设计模式概述之单例模式(四)

很多小伙伴,不知道设计模式是什么? 通常我们所说的设计模式是一种设计方案,是前人留下的经验及最佳实践。 想要学习设计模式,至少要把面向对象的基本结构全部了解。 设计模式,是建立在一定基础上的思维训练。 学习…

喜报 | 中关村发来贺电

2022年12月14日,由中关村金融科技产业发展联盟、中关村互联网金融研究院举办的“中关村金融科技系列活动——2023第十届中关村金融科技论坛年会暨2022“光大杯”中关村番钛客金融科技国际创新大赛颁奖典礼”已圆满落幕。本次会议为建设金融科技中心,共建…

【Pintos】实现自定义 UserProg 系统调用

💭 写在前面:本文讲解的内容不属于 Pintos 的 Project 项目,而是关于 userprog 如何添加系统调用的,学习如何额外实现一些功能到系统调用中以供用户使用。因为涉及到 src/example 下的Makefile 的修改、lib 目录下 syscall-nr 系统…

搜索二叉树详解

🧸🧸🧸各位大佬大家好,我是猪皮兄弟🧸🧸🧸 文章目录一、搜索二叉树框架二、搜索二叉树概念三、搜索二叉树操作①Erase②Find递归③Insert递归④Erase递归,比Erase更简洁⑤析构函数⑥…

unity学习笔记--day01

今天学习制作了一个简单的抽卡功能,学习结束后目录结构如下: .mate文件是unity生成的配置文件,不用管 制作第一张卡片 创建一个空物体,改名为Card。 在Card下挂载以下UI组件: 编写数据脚本并挂载,unity采用…

Nginx教程(4)—Keepalived

文章目录4.1 高可用集群架构Keepalived双机主备原理4.2 安装Keepalived4.3 Keepalived核心配置文件4.4 Keepalived实现双主机主备高可用测试4.5 高可用集群架构Keepalived双主热备原理Nginx教程一 Nginx教程二 Nginx教程三 4.1 高可用集群架构Keepalived双机主备原理 我们知道…

【计算机毕业设计】78.汽车租赁系统源码

一、系统截图(需要演示视频可以私聊) 目 录 摘 要 前 言 第1章 概述 1.1 研究背景 1.2 研究目的 1.3 研究内容 第二章 开发技术介绍 2.1 Java技术 2.2 Mysql数据库 2.3 B/S结构 2.4 SSM框架 第三章 系统分析 3.1 可行性分析 3.1.1 技术…

UnrealUBlueprintAsyncActionBase的使用

实现异步调用,之前我们介绍过一种FLatentActionInfo的方法,还有另外一种UBlueprintAsyncActionBase方法,可以实现异步节点,用于异步监听然后进行回调。按照如下步骤进行使用,我们同样以Delay一定帧数,并每帧…

面对新技术,必须找到与其发展相辅相成的长期主义的方法

从Meta股价的一路走低到扎克伯格发布的头显并不被用户买账,Facebook全力拥抱Meta正在经历一场过山车。   扎克伯格和他所带领下的Meta遭遇到的如此多的困境和难题,越来越多地让我们开始相信:所谓的元宇宙并非是一蹴而就的,它是一…

【云原生 Kubernetes】基于 Minikube 搭建第一个k8s集群

一、前言 对于k8s来说,搭建方式有多种,如果是生产环境,一般来说,至少需要3台节点确保服务的高可用性,常用的搭建方式列举如下(提供参考): kubeadm搭建(推荐) …

postman测试环境的创建及发送请求方式

目录 一、创建工作环境 1、打开postman,点击工作区 2、点击新建 3、添加名字,点击创建 4、工作区可以自由切换工作区 5、点击创建发送请求 6、更换请求方式 7、保存测试 二、测试发送请求,使用的时候服务一定要启动 1、普通传参&…

C++ 类型转换

目录 C语言中的类型转换 为什么C需要四种类型转换 C:命名的强制类型转换 static_cast reinterpret_cast const_cast dynamic_cast C语言中的类型转换 在C语言中,如果赋值运算符左右两侧类型不同,或者形参与实参类型不匹配&#xff0c…

信息学奥赛一本通——1163:阿克曼(Ackmann)函数

文章目录1163:阿克曼(Ackmann)函数【题目描述】【输入】【输出】【输入样例】【输出样例】分析代码1163:阿克曼(Ackmann)函数 时间限制:1000ms内存限制:65536KB提交数:24804通过数:20247时间限制: 1000 ms 内存限制: 65536 KB 提交数: 24804 通过数: 202…

第三十章 linux-模块的文件格式与EXPORT_SYMBOL的实现

第三十章 linux-模块的文件格式与EXPORT_SYMBOL的实现 文章目录第三十章 linux-模块的文件格式与EXPORT_SYMBOL的实现模块的文件格式EXPORT_SYMBOL的实现模块的文件格式 以内核模块形式存在的驱动程序,比如demodev.ko,其在文件的数据组织形式上是ELF&am…

数据结构---快速排序

快速排序分治法思想基准元素的选择元素交换双边循环法JAVA实现单边循环法JAVA实现快速排序也是从冒泡排序演化而来使用了 分治法(快的原因)快速排序和冒泡排序共同点:通过元素之间的比较和交换位置来达到排序的目的。 快速排序和冒泡排序不同…

JavaWeb核心:HTTPTomcatServlet

HTTP 概念: Hyper Text Transfer Protocol,超文本传输协议,规定了浏览器和服务器之间数据传输的规则。 HTTP-请求数据格式 HTTP-响应数据格式 响应状态码的大的分类 常见的响应状态码 Tomcat 简介 概念: Tomcat是Apache 软件基金会一个核心项目&#…