Необязательные аргументы в функциях 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

    Выводы


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

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

    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 14
    • +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

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

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

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

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