В Go нет синтаксиса для определения необязательных аргументов в функциях, поэтому приходится использовать обходные пути. Я знаю 2:

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

    funcStructOpts(Opts{p1: 1, p2: 2, p8: 8, p9: 9, p10: 10})
  2. Способ предложенный Робом Пайком с использованием функциональных аргументов:

    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(TM) 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

Выводы


Для себя я так и не решил, что же лучше. Предлагаю похоливарить в комментариях, а для сбора статистики опрос ниже.

Комментарии (14)


  1. Foxcool
    10.12.2017 23:24

    Сахарок на мой взгляд выглядит скорее запутаннее, к тому же. Этот стек вызовов функций в собственной голове переполняется ради понимания примитивного процесса получения аргументов (:


    1. Symphel
      11.12.2017 10:58

      Синтаксический сахар вроде должен уменьшать количество кода и делать его проще, а тут как раз наоборот.


      1. khim
        11.12.2017 19:45

        Ну уж нет. Синтаксический сахар почти всегда увеличивает количества кода. Вот количество текста в исходном коде — он должен уменьшать… и уменьшает…


  1. Scratch
    10.12.2017 23:59

    Используем метод Роба Пайка, он довольно удобен в повседневном использовании. В плане производительности поисками таких вот блох обычно занимаются, когда всё остальное в проекте уже вылизано до блеска (т.е. никогда). Всегда находятся другие, более очевидные места, где можно подкрутить производительность


    1. khim
      11.12.2017 19:51

      Это всё психология. Тот факт, что «всегда находятся другие, более очевидные места, где можно подкрутить производительность» просто-напросто обозначает, что 90% всех ресурсов ваша программа тратит просто на нагрев воздуха.

      Если писать сразу с учётом всех этих «краевых» эффектов, то можно, как правило, создать программу, которая будет в 5-10 раз быстрее, но оно вам надо? Как правило ответ — «нет, не надо», возможность быстро менять программу при изменении требований важнее.

      Но, собственно, Go на выжимание «всех соков» из процессора и не претендует — для этого C++ есть (хотя, в последнее время, rust начал на ту же нишу претендовать).


  1. ngalayko
    11.12.2017 04:47

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


  1. vesper-bot
    11.12.2017 09:41

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


    1. khim
      11.12.2017 19:54

      Не путайте C++ и Go/Java/PHP/JavaScript/etc. Это в C++ стараются сделать «сахар» таким, чтобы компилятор мог его полностью «растворить». В другия языках часто «сахар» замедляет исполнение в десятки раз — но люди с этим мирятся ради гибкости.


      1. vesper-bot
        12.12.2017 09:38

        Я учился программировать во времена, когда не во всяком ПК было 64 КБ памяти, поэтому для меня сегодняшние тенденции в наворачивании фреймворков на фреймворки ради скорости разработки — страшное расстройство. Другое дело, что скорость разработки нынче действительно требуется огромная, потому что конкурентов под каждым забором по пятеро, и все делают что-то вроде твоей программы, и кто первым выпустил, тот и победил, даже если в программе сделан только green path. Печально, но такова жизнь.


  1. andboson
    11.12.2017 21:57

    У вас что-то не так в коде, если вам нужно 10 аргументов в функции, тем более, не обзательных.

    ЗЫ: не очень понятна постановка вопроса вообще, почему именно функциональные аргументы? Как же простота?


    1. svistunov Автор
      11.12.2017 22:02

      Промахнулся, ответ ниже habrahabr.ru/post/344352/#comment_10562112


  1. svistunov Автор
    11.12.2017 22:01

    Например метод Dial из библиотеки grpc имеет почти 30 аргументов: godoc.org/google.golang.org/grpc#DialOption


  1. el777
    11.12.2017 22:05

    Интересная статья одного из "столпов" Go — Dave Cheney https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis где он приводит третий вариант и подробно все разбирает с плюсами и минусами.


    1. vesper-bot
      12.12.2017 09:45

      Вроде как «третий вариант» это примерно то же, что в этой статье вариант с функциями, только функции у него принимают в первом аргументе этакий this для изменения, но при этом не находятся в кодовом пространстве класса, который призваны изменять. Да и не отражены там вопросы производительности вообще никак, только вопросы расширяемости, и в условиях динамически расширяемого API функции от this выглядят действительно удобнее — при добавлении фичи не нужно переписывать структуру *opts и код конструктора, а достаточно написать функцию для конфигурирования конкретного параметра.

      PS: а что, если передавать в такой конструктор функцию от (*класс, ...int) или в крайнем случае ...string? Ещё гибче получается, причем второй аргумент функции оказывается опциональным списком — надо тебе, чтобы у фичи было много параметров, все запихиваешь в строки и передаешь, надо ноль — пишешь функцию от одного аргумента *класс, и хватит.