On funcs
Шаг 1. Избавляемся от интерфейсов
Интерфейсы с одним методом эквивалентны указателю на функцию:
type Reader interface {
Read([]byte) (int, error)
}
// becomes
type Reader = func([]byte) (int, error)
Интерфейсы с несколькими методами эквивалентны структуре с указателями на функции:
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}
// becomes
type Interface[Self any] struct {
Len func(Self) int
Less func(self Self, i, j int) bool
Swap func(self Self, i, j int)
}
Преимущества:
- можно юзать любые методы, лишь бы они совпадали по сигнатуре, необязательно по имени
- как следствие, можно реализовывать один и тот же интерфейс разными методами и разные интерфейсы одними функциями
- интерфейсы не нужны как часть языка
- понятно, что лежит в интерфейсе, в отличие от айтабов, указателей на типы и хз еще что
- понятно, что происходит при использовании интерфейса
- можно складывать не только указатели на функции и не только функции с одним и тем же ресивером
Недостатки:
- больше букв для вызова метода:
item.Less(slice, i, j)
вместоslice.Less(i, j)
- не хранится информация об исходном типе, точнее хранится в дженерике и фокусы с type-assertion-ами перестают работать, что как по мне к лучшему
- нельзя использовать как констрейнты в дженериках
Go
все еще позволяет не заполнять часть структуры если юзать explicit филды. Поэтому логичнее их не использовать.- нужно явно "приводить" к интерфейсу: вместо
sort.Sort(slice)
будет что-то типо:
Шаг 2. Избавляемся от методов
Методы нужны только чтобы сделать типы удовлетворяющим интерфейсам. Еще чтобы чуть меньше букв делать для вызовов функций, но это минорно.
// вместо
func (s *MySlice) Len() int {return len(s.slice)}
// пишем
func MySliceLen(s *MySlice) int {return len(s.slice)}
Преимущества:
- чуть меньше букв в "интерфейсах":
- делает концепт методов как часть языка ненужным
- при вызове
something.Do()
непонятно, будет ли передан указатель на ресивер или копия, и следовательно может ли ресивер измениться. С функцией имеем либоSomethingDo(something)
либоSomethingDo(&something)
явно указывая ресивер при вызове. Хоть и само понятие ресивера перестает иметь смысл. - пропадает вопрос, когда делать метод, а когда функцию
Недостатки:
- придется писать чуть больше букв при вызове
- пропадает смысл использовать констрейнты кроме
comparable
,any
и конкретных типов
Шаг 3. Избавляемся от именованных функций
По сути
это ничто иное, какПреимущества:
- делает ненужным такой концепт языка, как именованные функции, все выразимо через анонимные функции
- одной и той же функции можно тривиально дать несколько имен, сделать алиасы, подменить (это разберем чуть подробнее ниже)
Недостатки:
- нельзя сделать
const
чтобы гарантировать статический диспатч. это ограничение компилятора и никак не фиксится (вне компилятора) - лишние ключевые слова
var
иfunc
все еще существуют, хоть и не имеют смысла, то же самое (в теории) можно было бы выразить так:
Шаг 4. Манкипатчинг
Теперь нам доступен манкипатчинг любой функции:
oldLen := MySliceLen
defer func() {MySliceLen = oldLen}()
calls := 0
MySliceLen = func(s *MySlice) int {
calls++
return len(s.slice)
}
sort.Sort(
sort.Interface[*MySlice]{MySliceLen, MySliceLess, MySliceSwap},
slice,
)
assert(calls == 1)
Патчинг можно вынести в функцию вида
func UsePatch[F any](t *testing.T, orig *F, sub F) {
oldF := *orig
*orig = sub
t.Cleanup(func() {*orig = oldF})
}
// in test
calls := 0
UsePatch(t, &MySliceLen, func(s *MySlice) int {
calls++
return len(s.slice)
})
Или проще, если мы непосредственно тестим функцию с интерфейсом, а не функцию, которая использует функцию MySliceLen
где-то внутри
calls := 0
mySliceLen := func(s *MySlice) int {
calls++
return len(s.slice)
}
sort.Sort(
// используем патченую функцию длины
sort.Interface[*MySlice]{mySliceLen, MySliceLess, MySliceSwap},
slice,
)
assert(calls == 1)
Тем самым отпадает нужность в моках, т.к. моки заменяются на ручной временный патч нужных функций. Да и вообще становится возможным трекать любой вызов любой функции (публичной), а не только методы, которые используются через интерфейс. А еще можно патчить методы без интерфейса. А еще можно трекать поведение старой функции, не меняя ее поведение полностью:
calls := 0
oldLen := MySliceLen
UsePatch(t, &MySliceLen, func(s *MySlice) int {
calls++
return oldLen(s)
})
Преимущества:
- все описанные преимущества патчинга + функций-переменных + отсутствия интерфейсов
Недостатки:
- неконвенционально
- неприменимо к коду, который не использует такой стиль, например сторонней библиотеке
- (конкретно в
Go
) все вызовы потенциально становятся виртуальными, пока они не сделают нормальный оптимизатор, в частности инлайн и девиртуализацию и трекинг глобальных переменных