当前位置:首页 > 文章列表 > Golang > Go教程 > 深入分析golang多值返回以及闭包的实现

深入分析golang多值返回以及闭包的实现

来源:脚本之家 2022-12-27 15:11:20 0浏览 收藏

怎么入门Golang编程?需要学习哪些知识点?这是新手们刚接触编程时常见的问题;下面golang学习网就来给大家整理分享一些知识点,希望能够给初学者一些帮助。本篇文章就来介绍《深入分析golang多值返回以及闭包的实现》,涉及到闭包、多返回值,有需要的可以收藏一下

一、前言

golang有很多新颖的特性,不知道大家的使用的时候,有没想过,这些特性是如何实现的?当然你可能会说,不了解这些特性好像也不影响自己使用golang,你说的也有道理,但是,多了解底层的实现原理,对于在使用golang时的眼界是完全不一样的,就类似于看过http的实现之后,再来使用http框架,和未看过http框架时的眼界是不一样的,当然,你如果是一名it爱好者,求知欲自然会引导你去学习。

二、这篇文章主要就分析两点:

     1、golang多值返回的实现;

     2、golang闭包的实现;

三、golang多值返回的实现

我们在学C/C++时,很多人应该有了解过C/C++函数调用过程,参数是通过寄存器di和si(假设就两个参数)传递给被调用的函数,被调用函数的返回结果只能是通过eax寄存器返回给调用函数,因此C/C++函数只能返回一个值,那么我们是不是可以想象,golang的多值返回是否可以通过多个寄存器来实现的,正如用多个寄存器来传参一样?

这也是一种办法,但是golang并没有采用;我的理解是引入多个寄存器来存储返回值,会引起多个寄存器用途的重新约定,这无疑增加了复杂度;可以这么说,golang的ABI与C/C++非常不一样;

在从汇编角度分析golang多值返回之前,需要先熟悉golang汇编代码的一些约定, golang官网 有说明,这里重点说明四个symbols,需要注意的是这里的寄存器是伪寄存器:

       1.FP 栈底寄存器,指向一个函数栈的顶部;

       2.PC 程序计数器,指向下一条执行指令;

       3.SB 指向静态数据的基指针,全局符号;

       4.SP 栈顶寄存器;

这里面最重要的就是FP和SP,FP寄存器主要用于取参数以及存返回值,golang函数调用的实现很大程度上都是依赖这两个寄存器,这里先给出结果,

+-----------+---\
| 返回值2 | \
+-----------+  \
| 返回值1 |  \
+---------+-+  
| 参数2 |  这些在调用函数中
+-----------+  
| 参数1 |   /
+-----------+  /
| 返回地址 | /
+-----------+--\/-----fp值
| 局部变量 | \
| ... | 被调用数栈祯
|   | /
+-----------+--/+---sp值

这个就是golang的一个函数栈,也是说函数传参是通过fp+offset来实现的,而多个返回值也是通过fp+offset存储在调用函数的栈帧中。

下面通过一个例子来分析

package main

import "fmt"

func test(i, j int) (int, int) {
a:=i+ j
b:=i- j
 return a,b
}

func main() {
a,b:= test(2,1)
 fmt.Println(a, b)
}

这个例子很简单,主要是为了说明golang多值返回的过程;我们通过下面命令编译该程序

go tool compile -S test.go > test.s

然后,就可以打开test.s,来看下这个小程序的汇编代码。首先来看下test函数的汇编代码

"".test t=1size=32value=0args=0x20locals=0x0
0x000000000(test.go:5) TEXT"".test(SB),$0-32//栈大小为32字节
0x000000000(test.go:5)NOP
0x000000000(test.go:5)NOP
0x000000000(test.go:5)MOVQ"".i+8(FP),CX//取第一个参数i
0x000500005(test.go:5)MOVQ"".j+16(FP),AX//取第二个参数j
0x000a00010(test.go:5) FUNCDATA$0, gclocals·a8eabfc4a4514ed6b3b0c61e9680e440(SB)
0x000a00010(test.go:5) FUNCDATA$1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x000a00010(test.go:6)MOVQCX,BX//将i放入bx
0x000d00013(test.go:6) ADDQAX,CX//i+j放入cx
0x001000016(test.go:7) SUBQAX,BX//i-j放入bx
 //将返回结果存入调用函数栈帧
0x001300019(test.go:8)MOVQCX,"".~r2+24(FP)
 //将返回结果存入调用函数栈帧
0x001800024(test.go:8)MOVQBX,"".~r3+32(FP)
0x001d00029(test.go:8)RET

由这个汇编代码可以看出来,在test函数内部,是通过fp+8取第一个参数,fp+16取第二个参数;然后将返回的第一个值存入fp+24,返回的第二个值存入fp+32,和我上述所说完全一致;golang函数调用过程,是通过fp+offset来实现传参和返回值,而不像C/C++都是通过寄存器实现传参和返回值;

但是,这里有个问题,我的变量都是int类型,为啥分配的都是8字节,这有待考证。

本来想通过查看main函数的栈帧来验证之前的结论,但是golang对小函数自动转为内联函数,因此你们可以自己编译出来看看,main函数内部是没有调用test函数的,而是将test函数的汇编代码直接拷贝进main函数执行了。

四、golang闭包的实现

之前有去看了下C++11的lambda函数的实现,其实实现原理就是仿函数;编译器在编译lambda函数时,会生成一个匿名的仿函数类,然后执行这个lambda函数时,会调用编译生成的匿名仿函数类重载函数调用方法,这个方法也就是lambda函数中定义的方法;其实golang闭包的实现和这个类似,我们通过例子来说明

packagemain

import"fmt"

functest(aint)func(iint)int{
returnfunc(iint)int{
 a = a + i
returna
 }
}

funcmain(){
 f := test(1)
 a := f(2)
 fmt.Println(a)
 b := f(3)
 fmt.Println(b)
}

这个例子程序很简单,test函数传入一个整型参数a,返回一个函数类型;这个函数类型传入一个整型参数以及返回一个整型值;main函数调用test函数,返回一个闭包函数。

来看下test函数的汇编代码:

"".test t=1size=160value=0args=0x10locals=0x20
0x000000000(test.go:5) TEXT"".test(SB),$32-16
0x000000000(test.go:5)MOVQ(TLS),CX
0x000900009(test.go:5) CMPQSP,16(CX)
0x000d00013(test.go:5) JLS142
0x000f00015(test.go:5) SUBQ$32,SP
0x001300019(test.go:5) FUNCDATA$0, gclocals·8edb5632446ada37b0a930d010725cc5(SB)
0x001300019(test.go:5) FUNCDATA$1, gclocals·008e235a1392cc90d1ed9ad2f7e76d87(SB)
0x001300019(test.go:5) LEAQ type.int(SB),BX
0x001a00026(test.go:5)MOVQBX, (SP)
0x001e00030(test.go:5) PCDATA$0,$0
 //生成一个int型对象,即a
0x001e00030(test.go:5)CALLruntime.newobject(SB)
 //8(sp)即生成的a的地址,放入AX
0x002300035(test.go:5)MOVQ8(SP),AX
 //将a的地址存入sp+24的位置
0x002800040(test.go:5)MOVQAX,"".&a+24(SP)
 //取出main函数传入的第一个参数,即a
0x002d00045(test.go:5)MOVQ"".a+40(FP),BP
 //将a放入(AX)指向的内存,即上述新生成的int型对象
0x003200050(test.go:5)MOVQBP, (AX)
0x003500053(test.go:6) LEAQ type.struct { F uintptr; a *int }(SB), BX
0x003c00060(test.go:6)MOVQBX, (SP)
0x004000064(test.go:6) PCDATA$0,$1
0x004000064(test.go:6)CALLruntime.newobject(SB)
 //8(sp)这就是上述生成的struct对象地址
0x004500069(test.go:6)MOVQ8(SP),AX
0x004a00074(test.go:6)NOP
 //test内部匿名函数地址存入BP
0x004a00074(test.go:6) LEAQ"".test.func1(SB),BP
 //将匿名函数地址放入(AX)指向的地址,即给上述
 //F uintptr赋值
0x005100081(test.go:6)MOVQBP, (AX)
0x005400084(test.go:6)MOVQAX,"".autotmp_0001+16(SP)
0x005900089(test.go:6)NOP
 //将上述生成的整型对象a的地址存入BP
0x005900089(test.go:6)MOVQ"".&a+24(SP),BP
0x005e00094(test.go:6) CMPB runtime.writeBarrier(SB),$0
0x006500101(test.go:6)JNE$0,117
 //将a地址存入AX指向内存+8,
 //即为上述结构体a *int赋值
0x006700103(test.go:6)MOVQBP,8(AX)
 //将上述结构体的地址存入main函数栈帧中;
0x006b00107(test.go:9)MOVQAX,"".~r1+48(FP)
0x007000112(test.go:9) ADDQ$32,SP
0x007400116(test.go:9)RET

之前有看到一句话,很形象地描述了闭包

类是有行为的数据,为闭包是有数据的行为;

也就是说闭包是有上下文的,我们以测试例子为例,通过test函数生成的闭包函数,都有各自的a,这个a就是闭包的上下文数据,而且这个a一直伴随着他的闭包函数,每调用一次,a都会发生变化;

我们分析了上述汇编代码,来看下闭包实现原理;在这个测试例子中,由于a是闭包的上下文数据,因此a必须在堆上分配,如果在栈上分配,函数结束,a也被回收了;然后会定义出一个匿名结构体:

type.struct{
 F uintptr//这个就是闭包调用的函数指针
 a *int//这就是闭包的上下文数据
}

接着生成一个该对象,并将之前在堆上分配的整型对象a的地址赋值给结构体中的a指针,接下来将闭包调用的func函数地址赋值给结构体中F指针;这样,每生成一个闭包函数,其实就是生成一个上述结构体对象,每个闭包对象也就有自己的数据a和调用函数F;最后将这个结构体的地址返回给main函数;

来看下main函数获取闭包的过程;

"".main t=1size=528value=0args=0x0locals=0x88
0x000000000(test.go:12) TEXT"".main(SB),$136-0
0x000000000(test.go:12)MOVQ(TLS),CX
0x000900009(test.go:12) LEAQ -8(SP),AX
0x000e00014(test.go:12) CMPQAX,16(CX)
0x001200018(test.go:12) JLS506
0x001800024(test.go:12) SUBQ$136,SP
0x001f00031(test.go:12) FUNCDATA$0, gclocals·f5be5308b59e045b7c5b33ee8908cfb7(SB)
0x001f00031(test.go:12) FUNCDATA$1, gclocals·9d868b227cedd8dd4b1bec8682560fff(SB)
 //将参数1(f:=test(1))放入main函数栈顶
0x001f00031(test.go:13)MOVQ$1, (SP)
0x002700039(test.go:13) PCDATA$0,$0
 //调用main函数生成闭包对象
0x002700039(test.go:13)CALL"".test(SB)
 //将闭包对象的地址放入DX
0x002c00044(test.go:13)MOVQ8(SP),DX
 //将参数2(a:=f(2))放入栈顶
0x003100049(test.go:14)MOVQ$2, (SP)
0x003900057(test.go:14)MOVQDX,"".f+56(SP)
 //将闭包对象的函数指针赋值给BX
0x003e00062(test.go:14)MOVQ(DX),BX
0x004100065(test.go:14) PCDATA$0,$1
 //这里调用闭包函数,并且将闭包对象的地址也传进
 //闭包函数,为了修改a嘛
0x004100065(test.go:14)CALLDX,BX
0x004300067(test.go:14)MOVQ8(SP),BX

很明显,main函数调用test函数获取的是闭包对象的地址,通过这个闭包对象地址找到闭包函数,然后执行这个闭包函数,并且把闭包对象的地址传进函数,这点和C++传this指针原理一样,为了修改成员变量a

最后看下test内部的匿名函数(闭包函数实现):

"".test.func1t=1size=32value=0args=0x10 locals=0x0
0x000000000(test.go:6) TEXT"".test.func1(SB), $0-16
0x000000000(test.go:6) NOP
0x000000000(test.go:6) NOP
0x000000000(test.go:6) FUNCDATA $0, gclocals·23e8278e2b69a3a75fa59b23c49ed6ad(SB)
0x000000000(test.go:6) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
//DX是闭包对象的地址,+8即a的地址
0x000000000(test.go:6) MOVQ8(DX), AX
//AX为a的地址,(AX)即为a的值
0x000400004(test.go:7) MOVQ (AX), BP
//将参数i存入R8
0x000700007(test.go:7) MOVQ"".i+8(FP), R8
//a+i的值存入BP
0x000c00012(test.go:7) ADDQ R8, BP
//将a+i存入a的地址
0x000f00015(test.go:7) MOVQ BP, (AX)
//将a地址最新数据存入BP
0x001200018(test.go:8) MOVQ (AX), BP
//将a最新值作为返回值放入main函数栈中
0x001500021(test.go:8) MOVQ BP,"".~r1+16(FP)
0x001a00026(test.go:8) RET

闭包函数的调用过程:

      1、通过闭包对象地址获取闭包上下文数据a的地址;

      2、接着通过a的地址获取到a的值,并与参数i相加;

      3、将a+i作为最新值存入a的地址;

      4、将a最新值返回给main函数;

五、总结

这篇文章简单地从汇编角度分析了golang多值返回和闭包的实现;

      多值返回主要是通过fp寄存器+offset获取参数以及存入返回值实现;

      闭包主要是通过在编译时生成包含闭包函数和闭包上下文数据的结构体实现;

以上就是本文的全部内容了,是否有顺利帮助你解决问题?若是能给你带来学习上的帮助,请大家多多支持golang学习网!更多关于Golang的相关知识,也可关注golang学习网公众号。

版本声明
本文转载于:脚本之家 如有侵犯,请联系study_golang@163.com删除
Go语言使用字符串的几个技巧分享Go语言使用字符串的几个技巧分享
上一篇
Go语言使用字符串的几个技巧分享
在Mac中搭建go语言开发环境的操作步骤
下一篇
在Mac中搭建go语言开发环境的操作步骤
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    542次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    508次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    497次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    484次学习
查看更多
AI推荐
  • PPTFake答辩PPT生成器:一键生成高效专业的答辩PPT
    PPTFake答辩PPT生成器
    PPTFake答辩PPT生成器,专为答辩准备设计,极致高效生成PPT与自述稿。智能解析内容,提供多样模板,数据可视化,贴心配套服务,灵活自主编辑,降低制作门槛,适用于各类答辩场景。
    14次使用
  • SEO标题Lovart AI:全球首个设计领域AI智能体,实现全链路设计自动化
    Lovart
    SEO摘要探索Lovart AI,这款专注于设计领域的AI智能体,通过多模态模型集成和智能任务拆解,实现全链路设计自动化。无论是品牌全案设计、广告与视频制作,还是文创内容创作,Lovart AI都能满足您的需求,提升设计效率,降低成本。
    14次使用
  • 美图AI抠图:行业领先的智能图像处理技术,3秒出图,精准无误
    美图AI抠图
    美图AI抠图,依托CVPR 2024竞赛亚军技术,提供顶尖的图像处理解决方案。适用于证件照、商品、毛发等多场景,支持批量处理,3秒出图,零PS基础也能轻松操作,满足个人与商业需求。
    28次使用
  • SEO标题PetGPT:智能桌面宠物程序,结合AI对话的个性化陪伴工具
    PetGPT
    SEO摘要PetGPT 是一款基于 Python 和 PyQt 开发的智能桌面宠物程序,集成了 OpenAI 的 GPT 模型,提供上下文感知对话和主动聊天功能。用户可高度自定义宠物的外观和行为,支持插件热更新和二次开发。适用于需要陪伴和效率辅助的办公族、学生及 AI 技术爱好者。
    28次使用
  • 可图AI图片生成:快手可灵AI2.0引领图像创作新时代
    可图AI图片生成
    探索快手旗下可灵AI2.0发布的可图AI2.0图像生成大模型,体验从文本生成图像、图像编辑到风格转绘的全链路创作。了解其技术突破、功能创新及在广告、影视、非遗等领域的应用,领先于Midjourney、DALL-E等竞品。
    53次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码