Необязательные аргументы в функциях Go
В Go нет синтаксиса для определения необязательных аргументов в функциях, поэтому приходится использовать обходные пути. Я знаю 2:
- Передавать структуру, содержащую все необязательные аргументы в полях:
funcStructOpts(Opts{p1: 1, p2: 2, p8: 8, p9: 9, p10: 10})
- Способ предложенный Робом Пайком с использованием функциональных аргументов:
funcWithOpts(WithP1(1), WithP2(2), WithP8(8), WithP9(9), WithP10(10))
Второй способ в принципе делает тоже самое, но с синтаксическим сахаром. Мне не давала покоя мысль, а сколько же стоит этот сахар, кому ещё интересно прошу под кат.
Для тестов я использовал структуру с 10 опциями:
type Opts struct {
p1, p2, p3, p4, p5, p6, p7, p8, p9, p10 int
}
и 2 пустые функции:
func funcStructOpts(o Opts) {
}
func funcWithOpts(opts ...OptsFunc) {
o := &Opts{}
for _, opt := range opts {
opt(o)
}
}
Для тех, кто не работал с функциональными аргументами немного расскажу как они работают. Каждая опция описывается в виде функции, которая возвращает функцию, которая изменяет структуру с параметрами, например:
func WithP1(v int) OptsFunc {
return func(opts *Opts) {
opts.p1 = v
}
}
где OptsFunc
— это type OptsFunc func(*Opts)
При вызове функции их передают в качестве аргументов, а внутри функции в цикле заполняют структуру с аргументами:
o := &Opts{}
for _, opt := range opts {
opt(o)
}
Здесь магия и заканчивается, теперь у нас есть заполненная структура, осталось только выяснить, сколько стоит сахар. Для этого я написал простой benchmark:
func BenchmarkStructOpts(b *testing.B) {
for i := 0; i < b.N; i++ {
funcStructOpts(Opts{
p1: i,
p2: i + 2,
p3: i + 3,
p4: i + 4,
p5: i + 5,
p6: i + 6,
p7: i + 7,
p8: i + 8,
p9: i + 9,
p10: i + 10,
})
}
}
func BenchmarkWithOpts(b *testing.B) {
for i := 0; i < b.N; i++ {
funcWithOpts(WithP1(i), WithP2(i+2), WithP3(i+3), WithP4(i+4), WithP5(i+5), WithP6(i+6), WithP7(i+7),
WithP8(i+8), WithP9(i+9), WithP10(i+10))
}
}
Для тестирования я использовал Go 1.9 на Intel® Core™ i7–4700HQ CPU @ 2.40GHz.
Результаты:
BenchmarkStructOpts-8 100000000 10.7 ns/op 0 B/op 0 allocs/op
BenchmarkWithOpts-8 3000000 399 ns/op 240 B/op 11 allocs/op
Результаты противоречивые, с одной стороны разница почти в 40 раз, с другой — это сотни наносекунд.
Мне стало интересно, а на что же тратится время, ниже вывод pprof:
Всё логично, время тратится на выделение памяти под анонимные функции, а как известно malloc
— это время, много времени…
Для чистоты эксперимента я проверил, что происходит при вызове без аргументов:
func BenchmarkEmptyStructOpts(b *testing.B) {
for i := 0; i < b.N; i++ {
funcStructOpts(Opts{})
}
}
func BenchmarkEmptyWithOpts(b *testing.B) {
for i := 0; i < b.N; i++ {
funcWithOpts()
}
}
Здесь разница немного меньше, примерно в 20 раз:
BenchmarkEmptyStructOpts-8 1000000000 2.75 ns/op 0 B/op 0 allocs/op
BenchmarkEmptyWithOpts-8 30000000 57.0 ns/op 80 B/op 1 allocs/op
Выводы
Для себя я так и не решил, что же лучше. Предлагаю похоливарить в комментариях, а для сбора статистики опрос ниже.