Изучение языка Go - досконально поймите тонкую связь между возвратом и отсрочкой

Go

сомневаться

В предыдущей главе, посвященной функциям, было сказано, что функции языка Go поддерживают множественные возвращаемые значения.

Пока возвращаемое значение присваивается в теле функции, а return добавляется в конце, все возвращаемые значения могут быть возвращены.

При написании кода в последнее время часто приходится сталкиваться с тем, что после возврата некоторые завершающие работы должны быть выполнены в отсрочке, например, отправка транзакции или откат. Итак, я хочу выяснить, какова связь между возвратом и отсрочкой, кто на первом месте и как это влияет на итоговое возвращаемое значение?

практическая проверка

После понимания проблема оказалась сложнее, чем я думал.Если вы мне не верите, взгляните на вывод следующего кода.

package main

import "fmt"

func main()  {
	fmt.Println("f1 result: ", f1())
	fmt.Println("f2 result: ", f2())
}

func f1() int {
	var i int
	defer func() {
		i++
		fmt.Println("f11: ", i)
	}()

	defer func() {
		i++
		fmt.Println("f12: ", i)
	}()

	i = 1000
	return i
}

func f2() (i int) {
	defer func() {
		i++
		fmt.Println("f21: ", i)
	}()

	defer func() {
		i++
		fmt.Println("f22: ", i)
	}()

	i = 1000
	return i
}

Окончательный результат выполнения следующий

f12:  1001
f11:  1002
f1 result:  1000
f22:  1001
f21:  1002
f2 result:  1002

функция f1:

При входе в эту функцию, поскольку переменная возвращаемого значения не указана, необходимо сначала объявить переменную i, так как она имеет тип int.Если присваивания нет, переменная инициализируется значением 0, а затем выполняется операция присваивания i= 1000 выполняется, а затем выполняется оператор return для возврата значения i value.

Перед фактическим возвратом также выполняется часть функции отсрочки Две функции отсрочки выполняют операции автоинкремента для i соответственно, а значения i равны 1001 и 1002 по очереди.

функция f2:

Введите эту функцию, потому что переменная возвращаемого значения была определена как i, а затем напрямую присвойте i=1000, а затем верните значение i.

Точно так же перед фактическим возвратом i выполняются две функции отсрочки, и i увеличивается до 1001 и 1002 по очереди.

Суть проблемы в том, почему значение, возвращаемое безымянным параметром, равно 1000, на которое не влияет функция отсрочки на автоинкремент i, а названная функция возвращает 1002 после выполнения функции отсрочки.

Я нашел несколько причин в Интернете и упомянул вывод

原因就是return会将返回值先保存起来,对于无名返回值来说,
保存在一个临时对象中,defer是看不到这个临时对象的;
而对于有名返回值来说,就保存在已命名的变量中。

Увидев этот вывод, я хочу попытаться посмотреть, можно ли увидеть некоторые подсказки и подсказки, распечатав значение адреса i

Для этого к двум функциям добавляется информация об адресе печати i.

package main

import "fmt"

func main()  {
	fmt.Println("f1 result: ", f1())
	fmt.Println("f2 result: ", f2())
}

func f1() int {
	var i int
	fmt.Printf("i: %p \n", &i)
	defer func() {
		i++
		fmt.Printf("i: %p \n", &i)
		fmt.Println("f11: ", i)
	}()

	defer func() {
		i++
		fmt.Printf("i: %p \n", &i)
		fmt.Println("f12: ", i)
	}()

	i = 1000
	return i
}

func f2() (i int) {
	fmt.Printf("i: %p \n", &i)
	defer func() {
		i++
		fmt.Printf("i: %p \n", &i)
		fmt.Println("f21: ", i)
	}()

	defer func() {
		i++
		fmt.Printf("i: %p \n", &i)
		fmt.Println("f22: ", i)
	}()
    i = 1000
	return i
}

Вывод программы

i: 0xc000090000 
i: 0xc000090000 
f12:  1001
i: 0xc000090000 
f11:  1002
f1 result:  1000
i: 0xc00009a008 
i: 0xc00009a008 
f22:  1001
i: 0xc00009a008 
f21:  1002
f2 result:  1002

Из этого результата видно, что адрес переменной i не изменился за весь процесс ни функции f1, ни функции f2.

Так что вышеприведённый вывод я вроде понял, но всё же немного расплывчато, return хранится во временном объекте, и defer не может видеть эту временную переменную, но почему значение i может накапливаться на основе 1000?

очистить облака

Если вы хотите принципиально решить этот вопрос, лучше всего посмотреть, как выделяется память для выполнения этой программы.

В это время я вспомнил тот факт, что несколько дней назад упоминал в книге, что язык go можно преобразовать в язык ассемблера с помощью команд.

Чтобы упростить задачу, измените исходный код на

package main

import "fmt"

func main()  {
	fmt.Println("f1 result: ", f1())
	fmt.Println("f2 result: ", f2())
}

func f1() int {
	var i int
	defer func() {
		i++
		fmt.Println("f11: ", i)
	}()

	i = 1000
	return i
}

func f2() (i int) {
	defer func() {
		i++
		fmt.Println("f21: ", i)
	}()
	i = 1000
	return i
}

Код сборки, полученный при выполнении команды go tool compile -S test.go, выглядит следующим образом

os.(*File).close STEXT dupok nosplit size=26 args=0x18 locals=0x0
	...
	0x0000 00000 (test.go:5)	TEXT	"".main(SB), ABIInternal, $136-0
	0x0000 00000 (test.go:5)	MOVQ	(TLS), CX
	0x0009 00009 (test.go:5)	LEAQ	-8(SP), AX
	0x000e 00014 (test.go:5)	CMPQ	AX, 16(CX)
	0x0012 00018 (test.go:5)	JLS	315
	0x0018 00024 (test.go:5)	SUBQ	$136, SP
	0x001f 00031 (test.go:5)	MOVQ	BP, 128(SP)
	0x0027 00039 (test.go:5)	LEAQ	128(SP), BP
	0x002f 00047 (test.go:5)	FUNCDATA	$0, gclocals·7d2d5fca80364273fb07d5820a76fef4(SB)
	...
"".f1 STEXT size=145 args=0x8 locals=0x28
	0x0000 00000 (test.go:10)	TEXT	"".f1(SB), ABIInternal, $40-8
	0x0000 00000 (test.go:10)	MOVQ	(TLS), CX
	0x0009 00009 (test.go:10)	CMPQ	SP, 16(CX)
	0x000d 00013 (test.go:10)	JLS	135
	0x000f 00015 (test.go:10)	SUBQ	$40, SP
	0x0013 00019 (test.go:10)	MOVQ	BP, 32(SP)
	0x0018 00024 (test.go:10)	LEAQ	32(SP), BP
	0x001d 00029 (test.go:10)	FUNCDATA	$0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x001d 00029 (test.go:10)	FUNCDATA	$1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x001d 00029 (test.go:10)	FUNCDATA	$3, gclocals·9fb7f0986f647f17cb53dda1484e0f7a(SB)
	0x001d 00029 (test.go:10)	PCDATA	$2, $0
	0x001d 00029 (test.go:10)	PCDATA	$0, $0
	0x001d 00029 (test.go:10)	MOVQ	$0, "".~r0+48(SP)
	0x0026 00038 (test.go:11)	MOVQ	$0, "".i+24(SP)
	0x002f 00047 (test.go:12)	MOVL	$8, (SP)
	0x0036 00054 (test.go:12)	PCDATA	$2, $1
	0x0036 00054 (test.go:12)	LEAQ	"".f1.func1·f(SB), AX
	0x003d 00061 (test.go:12)	PCDATA	$2, $0
	0x003d 00061 (test.go:12)	MOVQ	AX, 8(SP)
	0x0042 00066 (test.go:12)	PCDATA	$2, $1
	0x0042 00066 (test.go:12)	LEAQ	"".i+24(SP), AX
	0x0047 00071 (test.go:12)	PCDATA	$2, $0
	0x0047 00071 (test.go:12)	MOVQ	AX, 16(SP)
	0x004c 00076 (test.go:12)	CALL	runtime.deferproc(SB)
	0x0051 00081 (test.go:12)	TESTL	AX, AX
	0x0053 00083 (test.go:12)	JNE	119
	0x0055 00085 (test.go:17)	MOVQ	$1000, "".i+24(SP)
	0x005e 00094 (test.go:18)	MOVQ	$1000, "".~r0+48(SP)
	0x0067 00103 (test.go:18)	XCHGL	AX, AX
	0x0068 00104 (test.go:18)	CALL	runtime.deferreturn(SB)
	0x006d 00109 (test.go:18)	MOVQ	32(SP), BP
	0x0072 00114 (test.go:18)	ADDQ	$40, SP
	0x0076 00118 (test.go:18)	RET
	0x0077 00119 (test.go:12)	XCHGL	AX, AX
	0x0078 00120 (test.go:12)	CALL	runtime.deferreturn(SB)
	0x007d 00125 (test.go:12)	MOVQ	32(SP), BP
	0x0082 00130 (test.go:12)	ADDQ	$40, SP
	0x0086 00134 (test.go:12)	RET
	0x0087 00135 (test.go:12)	NOP
	0x0087 00135 (test.go:10)	PCDATA	$0, $-1
	0x0087 00135 (test.go:10)	PCDATA	$2, $-1
	0x0087 00135 (test.go:10)	CALL	runtime.morestack_noctxt(SB)
	0x008c 00140 (test.go:10)	JMP	0
	...
	0x0000 00000 (test.go:21)	TEXT	"".f2(SB), ABIInternal, $32-8
	0x0000 00000 (test.go:21)	MOVQ	(TLS), CX
	0x0009 00009 (test.go:21)	CMPQ	SP, 16(CX)
	0x000d 00013 (test.go:21)	JLS	117
	0x000f 00015 (test.go:21)	SUBQ	$32, SP
	0x0013 00019 (test.go:21)	MOVQ	BP, 24(SP)
	0x0018 00024 (test.go:21)	LEAQ	24(SP), BP
	0x001d 00029 (test.go:21)	FUNCDATA	$0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x001d 00029 (test.go:21)	FUNCDATA	$1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x001d 00029 (test.go:21)	FUNCDATA	$3, gclocals·9fb7f0986f647f17cb53dda1484e0f7a(SB)
	0x001d 00029 (test.go:21)	PCDATA	$2, $0
	0x001d 00029 (test.go:21)	PCDATA	$0, $0
	0x001d 00029 (test.go:21)	MOVQ	$0, "".i+40(SP)
	0x0026 00038 (test.go:22)	MOVL	$8, (SP)
	0x002d 00045 (test.go:22)	PCDATA	$2, $1
	0x002d 00045 (test.go:22)	LEAQ	"".f2.func1·f(SB), AX
	0x0034 00052 (test.go:22)	PCDATA	$2, $0
	0x0034 00052 (test.go:22)	MOVQ	AX, 8(SP)
	0x0039 00057 (test.go:22)	PCDATA	$2, $1
	0x0039 00057 (test.go:22)	LEAQ	"".i+40(SP), AX
	0x003e 00062 (test.go:22)	PCDATA	$2, $0
	0x003e 00062 (test.go:22)	MOVQ	AX, 16(SP)
	0x0043 00067 (test.go:22)	CALL	runtime.deferproc(SB)
	0x0048 00072 (test.go:22)	TESTL	AX, AX
	0x004a 00074 (test.go:22)	JNE	101
	0x004c 00076 (test.go:26)	MOVQ	$1000, "".i+40(SP)
	0x0055 00085 (test.go:27)	XCHGL	AX, AX
	0x0056 00086 (test.go:27)	CALL	runtime.deferreturn(SB)
	0x005b 00091 (test.go:27)	MOVQ	24(SP), BP
	0x0060 00096 (test.go:27)	ADDQ	$32, SP
	0x0064 00100 (test.go:27)	RET
	0x0065 00101 (test.go:22)	XCHGL	AX, AX
	0x0066 00102 (test.go:22)	CALL	runtime.deferreturn(SB)
	0x006b 00107 (test.go:22)	MOVQ	24(SP), BP
	0x0070 00112 (test.go:22)	ADDQ	$32, SP
	0x0074 00116 (test.go:22)	RET
	0x0075 00117 (test.go:22)	NOP
	0x0075 00117 (test.go:21)	PCDATA	$0, $-1
	0x0075 00117 (test.go:21)	PCDATA	$2, $-1
	0x0075 00117 (test.go:21)	CALL	runtime.morestack_noctxt(SB)
	0x007a 00122 (test.go:21)	JMP	0
	...                    ........
	rel 16+8 t=1 type.[2]interface {}+0

Я чувствую, что это всего лишь один шаг от истины. Только после прочтения этого ассемблерного кода я могу понять, что делает этот возврат, когда он возвращает безымянное и именованное возвращаемое значение. Как распределяется так называемая переменная нулевого времени? Меня немного возбуждает мысль об этом.

Однако самое сложное в том, что я не изучил ассемблер -_-!

Но опять же, какое это имеет значение, поскольку результаты выполнения двух функций разные, должны быть различия на уровне сборки, поэтому я начал искать различия и, наконец, нашел ключевую информацию в приведенном выше ассемблерном коде следующим образом.

"".f2 STEXT size=124 args=0x8 locals=0x20
	0x0000 00000 (test.go:21)	TEXT	"".f2(SB), ABIInternal, $32-8
	0x0000 00000 (test.go:21)	MOVQ	(TLS), CX
	0x0009 00009 (test.go:21)	CMPQ	SP, 16(CX)
	0x000d 00013 (test.go:21)	JLS	117
	0x000f 00015 (test.go:21)	SUBQ	$32, SP
	0x0013 00019 (test.go:21)	MOVQ	BP, 24(SP)
	0x0018 00024 (test.go:21)	LEAQ	24(SP), BP
	0x001d 00029 (test.go:21)	FUNCDATA	$0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x001d 00029 (test.go:21)	FUNCDATA	$1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x001d 00029 (test.go:21)	FUNCDATA	$3, gclocals·9fb7f0986f647f17cb53dda1484e0f7a(SB)
	0x001d 00029 (test.go:21)	PCDATA	$2, $0
	0x001d 00029 (test.go:21)	PCDATA	$0, $0
	0x001d 00029 (test.go:21)	MOVQ	$0, "".i+40(SP)
	0x0026 00038 (test.go:22)	MOVL	$8, (SP)
	0x002d 00045 (test.go:22)	PCDATA	$2, $1
	0x002d 00045 (test.go:22)	LEAQ	"".f2.func1·f(SB), AX
	0x0034 00052 (test.go:22)	PCDATA	$2, $0
	0x0034 00052 (test.go:22)	MOVQ	AX, 8(SP)
	0x0039 00057 (test.go:22)	PCDATA	$2, $1
	0x0039 00057 (test.go:22)	LEAQ	"".i+40(SP), AX
	0x003e 00062 (test.go:22)	PCDATA	$2, $0
	0x003e 00062 (test.go:22)	MOVQ	AX, 16(SP)
	0x0043 00067 (test.go:22)	CALL	runtime.deferproc(SB)
	0x0048 00072 (test.go:22)	TESTL	AX, AX
	0x004a 00074 (test.go:22)	JNE	101
	0x004c 00076 (test.go:26)	MOVQ	$1000, "".i+40(SP)
	0x0055 00085 (test.go:27)	XCHGL	AX, AX
	0x0056 00086 (test.go:27)	CALL	runtime.deferreturn(SB)
	0x005b 00091 (test.go:27)	MOVQ	24(SP), BP
	0x0060 00096 (test.go:27)	ADDQ	$32, SP
	0x0064 00100 (test.go:27)	RET
	0x0065 00101 (test.go:22)	XCHGL	AX, AX
	0x0066 00102 (test.go:22)	CALL	runtime.deferreturn(SB)
	0x006b 00107 (test.go:22)	MOVQ	24(SP), BP
	0x0070 00112 (test.go:22)	ADDQ	$32, SP
	0x0074 00116 (test.go:22)	RET
	0x0075 00117 (test.go:22)	NOP
	0x0075 00117 (test.go:21)	PCDATA	$0, $-1
	0x0075 00117 (test.go:21)	PCDATA	$2, $-1
	0x0075 00117 (test.go:21)	CALL	runtime.morestack_noctxt(SB)
	0x007a 00122 (test.go:21)	JMP	0

Это ключевая информация известного возвращаемого значения f2, в основном см.

	0x004c 00076 (test.go:26)	MOVQ	$1000, "".i+40(SP)

Вероятно, это означает поставить 1000 на адрес памяти "".i+40(SP), а затем будет возвращена следующая операция.

"".f1 STEXT size=145 args=0x8 locals=0x28
	0x0000 00000 (test.go:10)	TEXT	"".f1(SB), ABIInternal, $40-8
	0x0000 00000 (test.go:10)	MOVQ	(TLS), CX
	0x0009 00009 (test.go:10)	CMPQ	SP, 16(CX)
	0x000d 00013 (test.go:10)	JLS	135
	0x000f 00015 (test.go:10)	SUBQ	$40, SP
	0x0013 00019 (test.go:10)	MOVQ	BP, 32(SP)
	0x0018 00024 (test.go:10)	LEAQ	32(SP), BP
	0x001d 00029 (test.go:10)	FUNCDATA	$0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x001d 00029 (test.go:10)	FUNCDATA	$1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x001d 00029 (test.go:10)	FUNCDATA	$3, gclocals·9fb7f0986f647f17cb53dda1484e0f7a(SB)
	0x001d 00029 (test.go:10)	PCDATA	$2, $0
	0x001d 00029 (test.go:10)	PCDATA	$0, $0
	0x001d 00029 (test.go:10)	MOVQ	$0, "".~r0+48(SP)
	0x0026 00038 (test.go:11)	MOVQ	$0, "".i+24(SP)
	0x002f 00047 (test.go:12)	MOVL	$8, (SP)
	0x0036 00054 (test.go:12)	PCDATA	$2, $1
	0x0036 00054 (test.go:12)	LEAQ	"".f1.func1·f(SB), AX
	0x003d 00061 (test.go:12)	PCDATA	$2, $0
	0x003d 00061 (test.go:12)	MOVQ	AX, 8(SP)
	0x0042 00066 (test.go:12)	PCDATA	$2, $1
	0x0042 00066 (test.go:12)	LEAQ	"".i+24(SP), AX
	0x0047 00071 (test.go:12)	PCDATA	$2, $0
	0x0047 00071 (test.go:12)	MOVQ	AX, 16(SP)
	0x004c 00076 (test.go:12)	CALL	runtime.deferproc(SB)
	0x0051 00081 (test.go:12)	TESTL	AX, AX
	0x0053 00083 (test.go:12)	JNE	119
	0x0055 00085 (test.go:17)	MOVQ	$1000, "".i+24(SP)
	0x005e 00094 (test.go:18)	MOVQ	$1000, "".~r0+48(SP)
	0x0067 00103 (test.go:18)	XCHGL	AX, AX
	0x0068 00104 (test.go:18)	CALL	runtime.deferreturn(SB)
	0x006d 00109 (test.go:18)	MOVQ	32(SP), BP
	0x0072 00114 (test.go:18)	ADDQ	$40, SP
	0x0076 00118 (test.go:18)	RET
	0x0077 00119 (test.go:12)	XCHGL	AX, AX
	0x0078 00120 (test.go:12)	CALL	runtime.deferreturn(SB)
	0x007d 00125 (test.go:12)	MOVQ	32(SP), BP
	0x0082 00130 (test.go:12)	ADDQ	$40, SP
	0x0086 00134 (test.go:12)	RET
	0x0087 00135 (test.go:12)	NOP
	0x0087 00135 (test.go:10)	PCDATA	$0, $-1
	0x0087 00135 (test.go:10)	PCDATA	$2, $-1
	0x0087 00135 (test.go:10)	CALL	runtime.morestack_noctxt(SB)
	0x008c 00140 (test.go:10)	JMP	0

Это ключевая информация о безымянном возвращаемом значении f1, в основном см.

	0x0055 00085 (test.go:17)	MOVQ	$1000, "".i+24(SP)
	0x005e 00094 (test.go:18)	MOVQ	$1000, "".~r0+48(SP)

Вероятно, это означает, что 1000 помещается в адрес памяти ".i+24(SP), а затем 1000 присваивается "".~r0+48(SP), что отличается от f1. Соответствующее предыдущему заключению, мы находим здесь подтверждение. Общий процесс - это случай безымянного возвращаемого значения. При возврате открывается новое пространство памяти. Последующая отсрочка считывает адрес памяти, такой как "".i+24(SP) и не может прочитать значение временного пространства. . Возврат в конце функции также является значением, соответствующим "".~r0+48(SP), что равно 1000. (Поскольку компиляция не изучалась, некоторые детали могут нуждаться в проверке)

в заключении

На этом этапе мы можем понять тонкую взаимосвязь между return и defer в языке Go и увидеть разницу между безымянным возвращаемым значением и именованным возвращаемым значением, возвращаемым с уровня сборки.