Необязательные аргументы в функциях Go

    В 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

    Выводы


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

    Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

    Что использовать для передачи необязательных аргументов?
    Поделиться публикацией
    Похожие публикации
    Ой, у вас баннер убежал!

    Ну. И что?
    Реклама
    Комментарии 15
    • +3
      Сахарок на мой взгляд выглядит скорее запутаннее, к тому же. Этот стек вызовов функций в собственной голове переполняется ради понимания примитивного процесса получения аргументов (:
      • 0
        Синтаксический сахар вроде должен уменьшать количество кода и делать его проще, а тут как раз наоборот.
        • –1
          Ну уж нет. Синтаксический сахар почти всегда увеличивает количества кода. Вот количество текста в исходном коде — он должен уменьшать… и уменьшает…
      • –4
        Используем метод Роба Пайка, он довольно удобен в повседневном использовании. В плане производительности поисками таких вот блох обычно занимаются, когда всё остальное в проекте уже вылизано до блеска (т.е. никогда). Всегда находятся другие, более очевидные места, где можно подкрутить производительность
        • +2
          Это всё психология. Тот факт, что «всегда находятся другие, более очевидные места, где можно подкрутить производительность» просто-напросто обозначает, что 90% всех ресурсов ваша программа тратит просто на нагрев воздуха.

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

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

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

          • +2
            Лишний malloc на такую мелочь — вредно. По мне, любой синтаксический сахар должен писаться так, чтобы даже если весь код будет его использовать вместо оптимизированной версии, потери производительности были бы достаточно малы, чтобы можно было ими пренебречь, выиграв в скорости разработки. И я сильно сомневаюсь, что этот «сахар» его вообще даст.
            • 0
              Не путайте C++ и Go/Java/PHP/JavaScript/etc. Это в C++ стараются сделать «сахар» таким, чтобы компилятор мог его полностью «растворить». В другия языках часто «сахар» замедляет исполнение в десятки раз — но люди с этим мирятся ради гибкости.
              • 0
                Я учился программировать во времена, когда не во всяком ПК было 64 КБ памяти, поэтому для меня сегодняшние тенденции в наворачивании фреймворков на фреймворки ради скорости разработки — страшное расстройство. Другое дело, что скорость разработки нынче действительно требуется огромная, потому что конкурентов под каждым забором по пятеро, и все делают что-то вроде твоей программы, и кто первым выпустил, тот и победил, даже если в программе сделан только green path. Печально, но такова жизнь.
            • –1
              У вас что-то не так в коде, если вам нужно 10 аргументов в функции, тем более, не обзательных.

              ЗЫ: не очень понятна постановка вопроса вообще, почему именно функциональные аргументы? Как же простота?
            • 0
              Например метод Dial из библиотеки grpc имеет почти 30 аргументов: godoc.org/google.golang.org/grpc#DialOption
              • 0

                Для каждой задачи свой инструмент. Второй способ (функциональные аргументы) подразумевает что будет инициализироваться "сложный" со множеством параметров или "тяжелый" на ресурсы объект, который будет существовать на протяжении всей жизни программы или библиотеки. Тот же метод Dial упомянутый вами, обычно вызывается один раз. Также обычно функциональные аргументы используются только в Публичных методах/функциях. Если у вас внутренний апи, который вы вызываете тысячу раз, то применять там функциональные аргументы всегда плохая идея.
                Кто-то выше писал что "Синтаксический сахар вроде должен уменьшать количество кода и делать его проще". Так и есть с точки зрения пользователя библиотеки, т.е. автору библиотеки таки да, надо написать чуть-чуть больше, в отличие от первого способа, чтобы конечному пользователю было легче жить.

              • 0

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

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

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

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое