А конкретно их хранение в переменной/слайсе и вызов в виде callback. Суть в том, что замыкание имеет доступ не только с своим аргументам, но и данным, которые оно замкнуло из внешного окружения. И при вызове такого callback, в функцию-замыкание, нужно передать ей еще и этот контекст. А для вызова обычной функции достаточно ее адреса. Однако, в коде на Go такие callback выглядят одинаково.
Пояснение что такое замыкание? Моя версия замыкание - это функция, которая замыкает данные из внешнего окружения. Например, анонимная функция, которая замыкает локальную переменную фукнции, в которой ее создали. Или фокус с методом объекта, который замыкает ссылку на объект.
func newClosure() func() {
i := someValue
return func() {
println(i)
}
}
closure := newClosure() // тут фукнция становится совсем не анонимной, но остается замыканием
obj := SomeObject{}
method := obj.Method // как я понимаю, тут method - это замыкание
Што-ж, расскажу про замыкания в Go, когда они используются для callback. Это просто. В Go все callback будут замыканиями с данными (или пустым контекстом, если это обычная функция).
type Void func()
var CBlist []Void // <- тут будут хранится callback'и, точнее адреса структур
// type funcval struct {
// fn uintptr
// // variable-size, данные замыкания из внешнего окружения
// }
// go:noinline
func AddCB(cb Void) { // добавляем callback в список
CBlist = append(CBlist, cb)
}
// go:noinline
func CallCBs() { // вызываем все callback'и
for _, cb := range CBlist {
cb()
}
}
При вызове callback'а, из слайса будет извлекаться адрес контекста/funcval (именно он лежит в слайсе коллбеков), из него адрес функции и в эту функцию будет передаваться контекст (в регистре DX). Одинаково как для обычных функций, так и для замыканий.
- В результате вызов callback'а будет дважды косвенным (slice -> context/funcval -> адрес фунции -> косвенный вызов функции с передачей funcval в DX).
- Похоже, регистр DX не будет использоваться под аргументы, потому что любую функцию можно вызвать через callback, а в таком случае в DX будет адрес контекста (нулевого для функции, но регистр все равно будет использоваться).
- доклад (не мой), в котором все это рассказано: https://www.youtube.com/watch?v=Vjo7Tmj3DnI (ну, почти все)
- слайды этого доклада: https://docs.google.com/presentation/d/1jjajI3lwhZ9xYpKlPOuu3tmFmM-YXlrSpSHh2ay1ou0/edit#slide=id.g129ca4019df_1_161
- Песочница, в которой можно поиграться с кодом: https://play.golang.com/p/-mMiVQ7NFUG (там даже есть команда для генерации ассемблерного кода)
package main
// сгенерировать ассемблерный код на локальной машине: go build -gcflags -S x.go
// для тех, кто не верит и хочет проверить
import (
"runtime"
)
type Void func()
var CBlist []Void // <- тут будут храниться не адреса функций, а адреса funcval,
// в которых будут адреса функций и контекст, если фукнция - замыкание
// go:noinline
func AddCB(cb Void) {
CBlist = append(CBlist, cb)
// слайс состоит из указателя на буфер,
// индекса последнего элемента (в штуках) и емкости (в штуках)
// MOVQ main.CBlist+16(SB), CX <- емкость слайса
// MOVQ main.CBlist+8(SB), BX <- текущий занятый индекс элемента
// INCQ BX <- следующий элемент
// MOVQ main.CBlist(SB), DX <- указатель на буфер
// ... тут был код проверки на переполнение слайса
// MOVQ AX, -8(DX)(BX*8) <- по адресу DX + BX*8 записывается новый элемент
// т.е. в коде нет разделения на обычные функции и замыкания,
// в слайс кладется какой-то указатель (единственный)
}
// go:noinline
func CallCBs() {
// MOVQ main.CBlist(SB), AX <- указатель на слайс
// MOVQ AX, main..autotmp_4+16(SP)
// MOVQ main.CBlist+8(SB), CX <- текущий занятый индекс элемента (конец итерации)
// MOVQ CX, main..autotmp_5+8(SP)
// XORL DX, DX <- текущий индекс итерации DX = 0
for _, cb := range CBlist {
cb()
// MOVQ DX, main..autotmp_6(SP)
// MOVQ (AX)(DX*8), DX <- DX = AX + DX*8, т.е. берем элемент слайса с индексом DX
// MOVQ (DX), AX <- AX = *DX (из слайса взяли адрес и разыменовали его в AX)
// DX указывает на структуру funcval, в которой есть адрес функции и контекст
// Там первый элемент - адрес функции, далее контекст произвольной длины,
// но контекст обрабатывает само замыкание - вызывающая сторона про него не знает
// CALL AX <- вызов функции, в DX указатель на контекст
// суть в том, что все элемены слайса вызываются одинаково,
// как замыкания, так и функции
// MOVQ main..autotmp_6(SP), DX
// инкремент текущего индекса итерации и другие команды для цикла
// INCQ DX
// MOVQ main..autotmp_4+16(SP), AX
// MOVQ main..autotmp_5+8(SP), CX
// CMPQ DX, CX
}
}
//go:noinline
func vfunc() {
println("vfunc")
}
func main() {
AddCB(vfunc)
// тут уже привычный код добавления элемента в слайс
// MOVQ main.CBlist+16(SB), CX
// MOVQ main.CBlist+8(SB), BX
// INCQ BX
// XCHGL AX, AX
// MOVQ main.CBlist(SB), AX
// ... код проверки на переполнение слайса
// LEAQ main.vfunc·f(SB), CX <- адрес контекста, а не функции,
// потому что main.vfunc·f, а не main.vfunc
// MOVQ CX, -8(AX)(BX*8) <- запись в слайс адреса контекста
var i int = 7
AddCB(func() {
// дальше для анонимной функции создается контекст
// в динмаической памяти (runtime.newobject)
// и также помещается в тот же слайс
print("i = ", i, "\n")
runtime.Caller(0)
})
CallCBs()
}
- участникам чатика https://t.me/thank_go, которые помогли разобраться в теме,