SoFunction
Updated on 2025-03-05

In-depth analysis of golang multi-value return and closure implementation

1. Preface

Golang has many novel features. I wonder if you have thought about how these features are implemented when you use them? Of course, you may say that not understanding these features does not seem to affect your use of golang, and what you said makes sense, but understanding the underlying implementation principles will have a completely different vision when using golang. It is similar to using http framework after seeing the implementation of http, which is different from the vision when you have not seen the http framework. Of course, if you are an IT enthusiast, your desire for knowledge will naturally guide you to learn.

2. This article mainly analyzes two points:

1. Implementation of golang multi-value return;

2. Implementation of golang closure;

Implementation of golang multi-value return

When we are learning C/C++, many people should have understood the C/C++ function calling process. Parameters are passed to the called function through registers di and si (assuming that there are two parameters). The return result of the called function can only be returned to the called function through the eax register. Therefore, the C/C++ function can only return one value. So can we imagine that golang's multi-value return can be achieved through multiple registers, just like using multiple registers to pass parameters?

This is also a way, but golang did not adopt it; my understanding is that introducing multiple registers to store the return value will cause reconciliation of the uses of multiple registers, which undoubtedly increases the complexity; it can be said that golang's ABI is very different from C/C++;

Before analyzing golang multi-value return from an assembly perspective, you need to be familiar with some conventions of golang assembly code. Golang official website has an explanation. Here we focus on explaining four symbols. It should be noted that the registers here are pseudo-registers:

The bottom register of the stack pointing to the top of a function stack;

Program counter, pointing to the next execution instruction;

Base pointer to static data, global symbol;

Top-stack register;

The most important ones here are FP and SP. FP registers are mainly used to retrieve parameters and store return values. The implementation of golang function calls depends largely on these two registers. Here is the result first.

+-----------+---\
| Return value2 | \
+-----------+  \
| Return value1 |  \
+---------+-+  
| parameter2 |  These are in the calling function
+-----------+  
| parameter1 |   /
+-----------+  /
| Return to address | /
+-----------+--\/-----fpvalue
| Local variables | \
| ... | Called number stack
|   | /
+-----------+--/+---spvalue

This is a function stack of golang, which means that the function parameter is passed throughfp+offsetto achieve, and multiple return values ​​are also achieved throughfp+offsetStored in the stack frame of the calling function.

The following is an example to analyze

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)
 (a, b)
}

This example is very simple, mainly to illustrate the process of golang multi-value return; we compile the program through the following command

go tool compile -S >

Then, you can open it and take a look at the assembly code of this applet. First, let’s look at the assembly code of the test function

"".test t=1size=32value=0args=0x20locals=0x0
0x000000000(:5) TEXT"".test(SB),$0-32//The stack size is 32 bytes0x000000000(:5)NOP
0x000000000(:5)NOP
0x000000000(:5)MOVQ"".i+8(FP),CX//Please the first parameter i0x000500005(:5)MOVQ"".j+16(FP),AX//Take the second parameter j0x000a00010(:5) FUNCDATA$0, gclocals·a8eabfc4a4514ed6b3b0c61e9680e440(SB)
0x000a00010(:5) FUNCDATA$1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x000a00010(:6)MOVQCX,BX//Put i into bx0x000d00013(:6) ADDQAX,CX//I+j put cx0x001000016(:7) SUBQAX,BX//I-j put bx //Save the return result into the call function stack frame0x001300019(:8)MOVQCX,"".~r2+24(FP)
 //Save the return result into the call function stack frame0x001800024(:8)MOVQBX,"".~r3+32(FP)
0x001d00029(:8)RET

It can be seen from this assembly code thattestInside the function, the first parameter is taken through fp+8.fp+16Take the second parameter; then save the returned first value infp+24, the second value returned is stored infp+32, exactly the same as what I said above; the golang function call process is throughfp+offsetTo implement parameters and return values, unlike C/C++, which implement parameters and return values ​​through registers;

However, there is a question here. My variables are all int type, so why are all allocated 8 bytes? This remains to be verified.

I originally wanted to verify the previous conclusion by checking the stack frame of the main function, but golang automatically converts the small function into an inline function, so you can compile it yourself. The main function does not call the test function, but directly copy the assembly code of the test function into the main function for execution.

IV. Implementation of golang closure

I've seen C++11 beforelambdaThe implementation principle of function is actually functors; the compiler is compilinglambdaWhen a function is used, an anonymous functor class will be generated and then this is executed.lambdaWhen a function is used, the compiled and generated anonymous functor class overload function call method will be called, which is the methodlambdaMethods defined in functions; in fact, the implementation of golang closure is similar to this, we will illustrate through examples

packagemain

import"fmt"

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

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

This example program is very simple.testPass an integer parameter to the functiona, return a function type; this function type passes an integer parameter and returns an integer value;mainFunction Callstestfunction, return a closure function.

Let's take a looktestAssembly code of function:

"".test t=1size=160value=0args=0x10locals=0x20
0x000000000(:5) TEXT"".test(SB),$32-16
0x000000000(:5)MOVQ(TLS),CX
0x000900009(:5) CMPQSP,16(CX)
0x000d00013(:5) JLS142
0x000f00015(:5) SUBQ$32,SP
0x001300019(:5) FUNCDATA$0, gclocals·8edb5632446ada37b0a930d010725cc5(SB)
0x001300019(:5) FUNCDATA$1, gclocals·008e235a1392cc90d1ed9ad2f7e76d87(SB)
0x001300019(:5) LEAQ (SB),BX
0x001a00026(:5)MOVQBX, (SP)
0x001e00030(:5) PCDATA$0,$0
 // Generate an int-type object, i.e. a0x001e00030(:5)(SB)
 //8(sp) is the address of the generated a, put into AX0x002300035(:5)MOVQ8(SP),AX
 //Save the address of a into the location of sp+240x002800040(:5)MOVQAX,"".&a+24(SP)
 //Take out the first parameter passed in the main function, that is, a0x002d00045(:5)MOVQ"".a+40(FP),BP
 //Put a into the memory pointed to by (AX), that is, the newly generated int object mentioned above0x003200050(:5)MOVQBP, (AX)
0x003500053(:6) LEAQ  { F uintptr; a *int }(SB), BX
0x003c00060(:6)MOVQBX, (SP)
0x004000064(:6) PCDATA$0,$1
0x004000064(:6)(SB)
 //8(sp) This is the struct object address generated above0x004500069(:6)MOVQ8(SP),AX
0x004a00074(:6)NOP
 //Storing the anonymous function address inside the test into BP0x004a00074(:6) LEAQ"".test.func1(SB),BP
 //Put the anonymous function address into the address pointed to by (AX), that is, give the above //F uintptr assignment0x005100081(:6)MOVQBP, (AX)
0x005400084(:6)MOVQAX,"".autotmp_0001+16(SP)
0x005900089(:6)NOP
 //Storage the address of the integer object a generated above into BP0x005900089(:6)MOVQ"".&a+24(SP),BP
0x005e00094(:6) CMPB (SB),$0
0x006500101(:6)JNE$0,117
 //Save address a into AX and point to memory +8, // That is, assign value to the above structure a *int0x006700103(:6)MOVQBP,8(AX)
 //Save the address of the above structure into the main function stack frame;0x006b00107(:9)MOVQAX,"".~r1+48(FP)
0x007000112(:9) ADDQ$32,SP
0x007400116(:9)RET

I saw a sentence before, which vividly described the closure

Classes are behavioral data, and closures are behavioral data-containing behavior;

In other words, the closure has a context. Let's take the test example as an example and passtestThe closure functions generated by the function have their own a, thisaIt is the context data of the closure, and thisaIt is always accompanied by its closure function, every time it is called,aIt will change;

We analyzed the above assembly code to see the closure implementation principle; in this test example, sinceais the context data of the closure, soaMust be allocated on the heap, if allocated on the stack, the function ends,aIt is also recycled; an anonymous structure will then be defined:

{
 F uintptr//This is the function pointer for the closure call a *int//This is the context data of the closure}

Then generate a single object and the integer object that was previously allocated on the heapaAssign the address of the a pointer in the structure, and then the closure is calledfuncAssign function address to structureFPointer; in this way, each closure function is generated, which is actually a structure object mentioned above, and each closure object has its own dataaand call functionsF; Finally return the address of this structure tomainfunction;

Let's take a lookmainThe process of function obtaining closures;

"".main t=1size=528value=0args=0x0locals=0x88
0x000000000(:12) TEXT"".main(SB),$136-0
0x000000000(:12)MOVQ(TLS),CX
0x000900009(:12) LEAQ -8(SP),AX
0x000e00014(:12) CMPQAX,16(CX)
0x001200018(:12) JLS506
0x001800024(:12) SUBQ$136,SP
0x001f00031(:12) FUNCDATA$0, gclocals·f5be5308b59e045b7c5b33ee8908cfb7(SB)
0x001f00031(:12) FUNCDATA$1, gclocals·9d868b227cedd8dd4b1bec8682560fff(SB)
 //Put parameter 1 (f:=test(1)) into the top of the main function stack0x001f00031(:13)MOVQ$1, (SP)
0x002700039(:13) PCDATA$0,$0
 //Calling the main function to generate the closure object0x002700039(:13)CALL"".test(SB)
 //Put the address of the closure object into DX0x002c00044(:13)MOVQ8(SP),DX
 //Put parameter 2 (a:=f(2)) on top of the stack0x003100049(:14)MOVQ$2, (SP)
0x003900057(:14)MOVQDX,"".f+56(SP)
 // Assign the function pointer of the closure object to BX0x003e00062(:14)MOVQ(DX),BX
0x004100065(:14) PCDATA$0,$1
 //Closed function is called here and the address of the closure object is also passed into //Closure function, in order to modify a0x004100065(:14)CALLDX,BX
0x004300067(:14)MOVQ8(SP),BX

Obviously,mainFunction CallstestThe function obtains the address of the closure object. It finds the closure function through the address of this closure object, then executes the closure function, and passes the address of the closure object into the function. This is the same as the principle of passing this pointer in C++. In order to modify member variablesa

FinallytestInternal anonymous functions (closure function implementation):

"".test.func1t=1size=32value=0args=0x10 locals=0x0
0x000000000(:6) TEXT"".test.func1(SB), $0-16
0x000000000(:6) NOP
0x000000000(:6) NOP
0x000000000(:6) FUNCDATA $0, gclocals·23e8278e2b69a3a75fa59b23c49ed6ad(SB)
0x000000000(:6) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
//DX is the address of the closure object, +8 is the address of a0x000000000(:6) MOVQ8(DX), AX
//AX is the address of a, (AX) is the value of a0x000400004(:7) MOVQ (AX), BP
//Save parameter i in R80x000700007(:7) MOVQ"".i+8(FP), R8
//The value of a+i is stored in BP0x000c00012(:7) ADDQ R8, BP
//Save a+i into the address of a0x000f00015(:7) MOVQ BP, (AX)
//Save the latest data of address a to BP0x001200018(:8) MOVQ (AX), BP
//Put the latest value of a as the return value into the main function stack0x001500021(:8) MOVQ BP,"".~r1+16(FP)
0x001a00026(:8) RET

The calling process of closure function:

1. Obtain the address of closure context data a through the closure object address;

2. Then obtain the value of a through the address of a and add it to parameter i;

3. Store a+i as the latest value into the address of a;

4. Return the latest value of a to the main function;

5. Summary

This article simply analyzes the implementation of golang multi-valued return and closure from the assembly perspective;

Multi-value return is mainly achieved by obtaining parameters through the fp register + offset and storing the return value;

Closures are mainly implemented by generating structures containing closure functions and closure context data at compile time;

The above is the entire content of this article. I hope it will be of some help to everyone’s study or just use golang. If you have any questions, you can leave a message to communicate.