Rocky Runs Up The Stairs


Привет, Хабр. Вы, наверно, меня помните: я – Марко Кевац, системный программист в Badoo. Недавно я наткнулся на небольшой рассказ о том, как новичок сделал изменение в рантайме языка Go. Несмотря на то, что этот пост, наверное, довольно неожиданно встретить в хабраблоге Badoo, и многие могут сказать, что он банален и переполнен наивной радостью, я считаю, что такие истории демонстрируют, насколько сообщество Go доброжелательно и внимательно по отношению ко всем его участникам. Поэтому и перевел его.


А ещё в посте есть два интересных факта, связанных с внутренностями языка. Приятного чтения!


Я уже некоторое время пишу open-source-программы на Go. И вот только недавно у меня появилась возможность писать на Go и на работе. Я с радостью переключился и стал самым настоящим Go-разработчиком.


Всё было отлично, пока не случилась последняя конференция GopherCon, где проводили мастер-класс для тех, кто хочет делать вклад в развитие языка. И вот, видя, как все эти люди коммитят код в основной репозиторий, я тоже захотел что-то сделать. А буквально через пару дней Francesc Campoy записал прекрасное видео, в котором подробно рассказывает, как контрибьютить в Go.


Желание быть причастным охватило меня. Я понятия не имел, что я могу сделать, но решил скачать исходный код и скомпилировать его. Так и начался мой путь по дороге контрибьютора Go.


Я читал инструкцию для контрибьюторов и шаг за шагом выполнял её. Подписать CLA получилось не сразу, так как инструкция была немного корявой. Собственно, почему бы не указать на это и не предложить исправить её? Это может быть моим первым CL! Вдохновлённый, я создал тикет. Но оказалось, я наступил на стандартные грабли новичка. Проблема уже была решена в Tip, а я даже не догадался посмотреть.


Поскольку у меня уже всё было готово, я периодически просто гулял по стандартной библиотеке в поисках, что бы поправить. И поскольку я уже несколько месяцев программировал на Go на работе, я столкнулся с частями рантайма, которые постоянно всплывали во время профилирования. Одна из таких частей – пакет fmt. Я решил посмотреть на него внимательно и понять, можно ли что-то с этим сделать. Примерно через час я натолкнулся на кое-что интересное.


Функция fmt_sbx в файле fmt/format.go начинается следующим образом:


func (f *fmt) fmt_sbx(s string, b []byte, digits string) {
    length := len(b)
    if b == nil {
        // No byte slice present. Assume string s should be encoded.
        length = len(s)
    }

Было ясно, что len() вызывается два раза в случае, если b равно nil, хотя чаще всего достаточно одного. Но если передвинуть его в else, то len() вызовется всегда только один раз. Я решил отправить CL об этом, чтобы посмотреть, что скажут другие.


Буквально через пару минут Ian дал оценку +2, а затем Avelino – +1. Я не мог поверить в это!


Но потом что-то пошло не так. Dave поставил -1, и Martin – тоже. Он взял бинарный дамп кода и увидел, что между двумя вариантами нет никакой разницы. Компилятор был достаточно умным, чтобы сделать эту небольшую оптимизацию. Так что в итоге я оказался в минусе: else плохо сказывается на читабельности кода, а выигрыша в производительности никакого.


Этот CL пришлось забросить…


Но я узнал много нового. Например, о таких утилитах, как benchstat и benchcmp. Более того, теперь я был знаком со всем процессом и попробовать ещё раз мне ничего не стоило.


Вскоре я узнал, что простая конкатенация строк гораздо быстрее, чем fmt.Sprintf(). С этим знанием я пошёл искать «жертву», и это не заняло много времени. Я остановился на пакете archive/tar. Функция formatPAXRecord в файле archive/tar/strconv.go содержит следующий код:


size := len(k) + len(v) + padding
size += len(strconv.Itoa(size))
record := fmt.Sprintf("%d %s=%s\n", size, k, v)

После того как я поменял последнюю строчку на record := fmt.Sprint(size) + " " + k + "=" + v + "\n", я увидел значительное ускорение:


name             old time/op    new time/op    delta
FormatPAXRecord     683ns ± 2%     457ns ± 1%  -33.05%  (p=0.000 n=10+10)

name             old alloc/op   new alloc/op   delta
FormatPAXRecord      112B ± 0%       64B ± 0%  -42.86%  (p=0.000 n=10+10)

name             old allocs/op  new allocs/op  delta
FormatPAXRecord      8.00 ± 0%      6.00 ± 0%  -25.00%  (p=0.000 n=10+10)

Остальное, как говорится, уже история. На этот раз Joe отревьюил код, и после нескольких мелких исправлений его замёржили! Ура! Я внёс свой вклад в развитие Go. Я прошёл путь от средненького open-source-контрибьютора до контрибьютора в язык программирования Go.


Это далеко не конец. Я начинаю лучше понимать тонкости языка и буду продолжать создавать CL каждый раз, когда что-то найду. Десять чашек чая господам из Go-команды за то, что они так изящно и безустанно управляют разработкой такого сложного проекта.


P.S. Ну, и для справки:


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


  1. playnet
    29.08.2017 14:47
    +3

    >После того как я поменял последнюю строчку на record := fmt.Sprint(size) + " " + k + "=" + v + "\n", я увидел значительное ускорение

    … И существенно менее читаемый код. В модуль не смотрел, но если это работает в цикле и вызывается часто — быстродействие важнее, но если 1-2 раза на упаковку — это плохой, вредный код. Ну или хотя бы надо оставить старый код как комментарий, для понимания.


    1. 0xd34df00d
      30.08.2017 02:52
      +2

      Ну или хотя бы надо оставить старый код как комментарий, для понимания.

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


  1. laughman
    29.08.2017 15:09
    +9

    length := len(b)
    if b == nil {

    это теперь нормально — сначала использовать аргумент, а потом проверять его на пустое значение?


    1. myxo
      29.08.2017 15:19
      -2

      len(nil) в go возвращает ноль. Так записано для читаемости.
      Собственно это и хотел исправить автор вначале =)


      1. laughman
        29.08.2017 16:02
        +16

        len(nil) в go возвращает ноль


        и это сделал человек, утверждавший, что «errors are values»?


        1. creker
          29.08.2017 21:29
          -3

          Это сделали в языке, где нулевое значение максимально используется на полную катушку. Это важная часть языка. Поэтому nil массив это полезное значение, а не источник ошибок и постоянной необходимости проверок на nil. Конкретно код len(nil) не скомпилируется, а вот если в него передать переменную типа массив со значением nil, то действительно вернется 0. А еще при этом будет работать тот же cap() и append(). В итоге, и код чистый, и безопасностью никто не пожертвовал.


          1. TheShock
            29.08.2017 22:11
            +12

            Прелестное проявление стокгольмского синдрома.


          1. 0xd34df00d
            30.08.2017 02:54
            +1

            Конкретно код len(nil) не скомпилируется, а вот если в него передать переменную типа массив со значением nil, то действительно вернется 0.

            Чтобы уточнить,


            arr := nil
            len(arr)

            соберётся, а


            len(nil)

            нет?


            1. TheShock
              30.08.2017 04:02
              +1

              Ни первое, ни второе не соберется — необходимо указать тип (use of untyped nil):

              func main() {
              	var arr []int = nil
              	print(len(arr))
              }


              Второе — по аналогии можно собрать:

              func main() {
              	print(len([]int(nil)))
              }


              1. 0xd34df00d
                30.08.2017 04:54
                +5

                Я Go-то не знаю, это скорее псевдокод был. Спасибо за пояснение!


                Но вообще это, конечно, грустно.


      1. TheShock
        29.08.2017 16:25
        +13

        len(nil) в go возвращает ноль. Так записано для читаемости.

        Сложно назвать это читаемым. Говнокод как говнокод.


      1. iamwizard
        29.08.2017 23:49
        +3

        Я бы немного уточнил.
        Магия тут не в len() — len всегда делает одно и то-же: берет поле Len из слайса, который представляет из себя структуру вида.


        type Slice struct {
            Data uintptr
            Len  int
            Cap  int
        }

        Магия тут в "касте" nil в слайс ( при сравнение или присвоении ). Сравнение слайса с nil — это сравнение со Slice{0,0,0} что является значением по умолчанию при иницализации переменной слайса. В результате — только-что созданный слайс == nil


        Зачем так было сделано? Как уже было написано — возможность работать со слайсом без дополнительных проверок. Это из того-же порядка что и отсутсвие необходимости инициализировать переменную типа bool.


  1. laughman
    29.08.2017 16:00
    -1

    .


  1. mayorovp
    29.08.2017 16:28
    +17

    То есть считается, что код вида


    length := len(b)
    if b == nil {
        // No byte slice present. Assume string s should be encoded.
        length = len(s)
    }

    менее понятный, чем


    if b == nil {
        length := len(s)
    } else {
        length := len(b)
    }

    Кажется, я был прав когда не стал изучать этот язык...


    1. Bibainet
      29.08.2017 17:50
      +11

      Во втором примере вы на самом деле объявляете две разные переменные length, каждая в своей области {...}. Использовать их за пределами этой области не получится, там они не будут существовать. Такой код не скомпилится, потому что в Go нельзя объявлять локальную переменную и не использовать ее. Поэтому придется писать так:


      var length int // Объявление
      if b == nil {
          length = len(s)
      } else {
          length = len(b)
      }
      // Используем length

      Это значительно длинее первого варианта, хотя и понятнее.


      1. mayorovp
        29.08.2017 20:25
        +5

        Спасибо за исправление.


        Это значительно длинее первого варианта, хотя и понятнее.

        Ну так это же практически девиз языка go — "длиннее и понятнее". Но, видимо, даже у авторов стандартной библиотеки нету сил следовать ему полностью...


      1. TheShock
        29.08.2017 20:49
        +8

        Это значительно длинее первого варианта, хотя и понятнее.

        В любом адекватном языке:
        length := (b == nil) ? len(s) : len(b)

        Или, даже, так:
        length := len(b == nil ? s : b)

        А иногда и так::
        length := len(b || s)


        И коротко, и понятно. Но в гоу все через жопу.


        1. creker
          29.08.2017 21:31
          -10

          И все ваши примеры сложнее и менее читаемы, чем оригинал.


          1. TheShock
            29.08.2017 22:09
            +7

            Вы про тот оригинал, где берется длина nil, она оказывается равной 0 вместо ошибки, присваивается переменной, а потом внезапно передумывается и присваивается длина чего-то другого, да? Читаемее? Проще? Может еще и логичнее? Не смешите моего кота!

            В то время как len(b || s) — читается очень просто: дай длину b (если существует) или s


            1. Falstaff
              29.08.2017 22:54

              Можно я с вами поспорю? :) Я вот, не зная этой конструкции, прочитал "дай длину b или s… ну чего-нибудь из этих двоих, всё равно чего". Нет, серьёзно, вот этого "если существует" в этой записи нигде нет, руку на сердце положа. Это такая же особенность языка как length(nil) в Go, которую тоже надо знать, просто она для вас привычнее. Гхм, ну и length(nil) тоже в принципе логику некую имеет — чему равна длина чего-то несуществующего? Ничему, нулю. (Если что, я не пишу ни на Go, ни на JS, просто погулять вышел.)


              1. TheShock
                30.08.2017 00:28
                +3

                А если это специальный оператор, у которого только такая задача?

                Дело в том, что я знаю такую особенность Гоу (я на нем писал последнее время) и мне она кажется крайне нелогичной — это костыль в спецификации, потому что нету удобного инструмента.

                чему равна длина чего-то несуществующего?

                Ошибке (потому что в Гоу обычно возвращается ошибка в таких нелогичных случаях).

                Но проблема ведь не только в len(nil), но и в том, что сперва ты присваиваешь неправильное значение, а потом проверяешь условия и присваиваешь правильное! Бред ведь.

                Еще раз, я писал на Гоу, мне привычный этот синтаксис и я все-равно считаю его ущербным.


                1. Falstaff
                  30.08.2017 01:13

                  Возможно, в каком-то языке это такой специальный оператор, у которого именно такая задача. Возможно, в другом языке это вообще бы значило "дай длину true если b — true или a — true, или длину false если оба false" — и, нет, я не могу ответить, какой физический смысл у len(true). :) Хорошо, для тех, кто пишет на C#, этот код читается просто. Откуда следует, что он универсально простой для всех? Для его понимания как минимум надо знать особенность конкретного языка, вот я про что.


                  Про длину несуществующего — я говорю не с точки зрения соответствия вообще конвенциям языка (я так глубоко в Го не лез, не моя епархия), а просто с точки зрения логики, не привязанной к какому-то языку. У ничего и длина ничего.


                  Да, если что — я ни разу не защищаю код из статьи, присвоение а потом проверка (для меня, но я не пишу на Го) вызывает содрогание. Но вот для примера — если бы там дальше стоял цикл (гипотетически, в какой-нибудь другой функции) и он бы при b = nil корректно отрабатывал 0 раз (поскольку len(nil) = 0) и функция возвращала бы пустую коллекцию, то это могло бы читаться нормально. По Мартину прямо. Так что я не думаю, что len(nil) = 0 это так уж нелогично. Код из статьи — ну, да. Но если я скажу "но логичнее было бы возвращать ошибку" или "логичнее было бы выбрасывать исключение", то я просто буду примерять ко всему набор вывихов, которые у меня в мозгу для другого языка, и не факт, что они на самом деле логичнее, просто я к ним привык.


                  1. TheShock
                    30.08.2017 04:22
                    +5

                    Так в том же и проблема — это кривой код как с точки зрения Гоу, так и с точки зрения computer science в целом.

                    И если второе очевидно, то первое объясню — обычно (та почти всегда, наверное) Гоу не позволяет делать таких вещей и выбрасывает ошибку. Та он выбрасывает ошибку даже когда ее можно не выбрасывать, просто чтобы выбросить.

                    Временно закомментил код, но не закомментил импорт — ошибка «imported and not used». Блин, я же дебажу, я его потом разкоменчу! Но нет, го--- оправдывает свое название и вместо того, чтобы написать ворнинг — падает в панику

                    package main
                    
                    import "fmt"
                    
                    func main() {
                    	// fmt.Println("Hello, playground")
                    }


                    Разрабатываешь, пишешь логику, заранее объявил переменные, чтобы потом заюзать, решил проверить, как оно работает часть, которую написал, запускаешь. Фиг там — паника «declared and not used».
                    package main
                    
                    func main() {
                    	var arr []int = nil
                    }


                    Ну то есть этот код никак не влияет на производительность, компилятор знает, что он лишний (все-равно провел анализ) и может быть спокойно выброшен, но нет ведь — ошибка

                    Имеем две структуру, которые циклически ссылаются одна на другую. Все работает корректно. Рефакторим, разносим их по разным неймспейсам. Все упало, внезапно гоу осознал циклическую зависимость! И я понимаю, что иногда это свидетельствует о неудачном дизайне, но я хочу сам решать. Тем более, самое популярное решение — не изменить архитектуру, а забить на рефакторинг и все свалить в одну свалку неймспейс — там циклические зависимости можно.

                    Я уж молчу о тошнотворном повторении как мантры if err != nil (это настолько въедается, что ты просто раз за разом копипастишь, как в анекдоте про студента и гвозди). Раз гвоздь, два гвоздь, но у тебя уже их в заднице столько, что ты привык:
                    a, err := doA()
                    if err != nil {
                      return nil, err
                    }
                    b, err := doB(a)
                    if err != nil {
                      return nil, err
                    }
                    c, err := doC(b)
                    if err != nil {
                      return nil, err
                    }


                    Видите, как Go относится к таким ошибкам? Это и есть Go-way. И гоу-штунды кричат: «это не так плохо», «это даже клево», ведь это очевидно, ты должен контролировать свой код, ты должен писать свою архитектуру, ошибка должна быть явной, рекурсивная зависимость — плохой код, срочно рефакторите.

                    Но последовательность им не знакома, потому, на говнокод они кричат: «зато так покороче», ну и что, что он nil воспринимает как пустой массив, зато так удобно, нет ничего страшного, что я сначала присваиваю переменной неправильное значения, вызывая функцию len, когда ее не надо вызывать!

                    Зато неиспользуемые import'ы использовать нельзя — ведь Гоу следит за чистотой кода, видите ли.


                    1. Falstaff
                      30.08.2017 05:26

                      … это кривой код как с точки зрения Гоу, так и с точки зрения computer science в целом.

                      Это про len(nil), или именно про код из статьи? Если про код — да, я в принципе не спорю, можно было создать переменную без лишнего вызова len() с потенциальным мусором в аргументе. Я только про то, что контракт len(nil) -> 0 в принципе не криминал и может пригодиться в какой-нибудь другой ситуации.


                      С неиспользуемыми переменными и импортами да, тоже перегиб. Согласен — иногда хочется что-то набросать и проверить, или выкусить фрагмент на время. (Вроде goimports хотя бы об импортах позволяет забыть, но это если IDE.)


                      А что видится альтернативой цепочкам if-ов с проверками err? Исключения? Вообще — я отчасти понимаю, почему их нет. Язык бедный, он просто намеренно бедный — ему вроде бы как раз в заслугу ставят то, что команда вчерашних студентов может через неделю выдавать более или менее удобоваримый код, а не сидеть с опухшими головами и отвлекать сеньоров вопросами. Исключения всё-таки предполагают осознание того, что у нас в коде куча неявных точек возврата, кривая обучения сразу круче становится. (Если что, я за выразительные языки. Просто философски отношусь к тому, что рынок хочет толпу годных в дело стажёров.)


                      1. 0xd34df00d
                        30.08.2017 07:40
                        +2

                        А что видится альтернативой цепочкам if-ов с проверками err? Исключения?

                        Монадическая обработка ошибок на манер Either.


                      1. SirEdvin
                        30.08.2017 09:09
                        +3

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

                        Она в любом случае не может. Какие такие задачи они будут решать? Люди не спотыкаются, о синтаксис языка, если это только не какой-то haskell, люди страдают от сложности библиотек и фреймоворков, но тут то go даже хуже, чем другие языки, потому что выразительные конструкции заменяются на структуры и кучи шаблонного кода.


                        1. Falstaff
                          30.08.2017 19:40

                          Я не уверен, что это корректный аргумент. Вроде есть отзывы о том, что новички очень быстро становится продуктивным на Го (на бэкенде во всяком случае). И с другой стороны, выразительные конструкции могут, наоборот, усложнить понимание фреймворков и библиотек. Регулярно слышу, как народ стонет по поводу библиотек на Scala, которые без автора сопровождать невозможно.


                        1. 0xd34df00d
                          30.08.2017 19:48

                          Люди не спотыкаются, о синтаксис языка, если это только не какой-то haskell

                          В хаскеле, кстати, предельно простой синтаксис. Не лисп, конечно, но всё же.


                    1. 0xd34df00d
                      30.08.2017 07:39
                      +2

                      Я уж молчу о тошнотворном повторении как мантры if err != nil

                      Я в каком-то соседнем треде уже предлагал, но предложу ещё раз: давайте замутим стартап по продаже USB-донглов с кнопкой, которая по нажатию выдаёт if err != nil { return nil, err }.


                    1. laughman
                      30.08.2017 10:35
                      +1

                      a, err := doA()
                      if err != nil {
                        return nil, err
                      }
                      b, err := doB(a)
                      if err != nil {
                        return nil, err
                      }
                      c, err := doC(b)
                      if err != nil {
                        return nil, err
                      }


                      интересно, а как при этом принято логировать такие ошибки?


                      1. laughman
                        30.08.2017 10:41
                        +1

                        т.е. строчек по обработке ошибок должно бы быть больше, и смысловые строчки реально станут настолько редкими, что будет трудно со стороны отследить корректность алгоритма? или есть какой-то способ обойти проблему?


                        1. Crandel
                          30.08.2017 11:14

                          Я делал просто функцию для обработки ошибок в моем тестовом
                          проекте


                          1. mayorovp
                            30.08.2017 11:30

                            … и чем это принципиально отличается от исключений?


                            1. Crandel
                              30.08.2017 11:34

                              Я не говорю, что так надо делать, я просто сделал так для простоты, когда изучал Го


          1. splav_asv
            29.08.2017 22:41
            +1

            А так:

            let length = if let Some(s) = s {s.len()} else {d.len()};
            или так:
            let length = s.unwrap_or(d).len();
            ?


            1. 0xd34df00d
              30.08.2017 02:58
              +2

              len = length $ fromMaybe d s


              1. splav_asv
                30.08.2017 09:54

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


    1. Alexeyco
      29.08.2017 18:08
      +1

      Вы можете составить список языков программирования, фреймворков и фильмов, которые считаете негодными? Я чувствую, там много хорошего.


      1. mayorovp
        29.08.2017 20:23
        -4

        Языки программирования: Ada, Go и Papyrus Script
        Фреймворки: Angular, redux*


        * да, я знаю что это библиотека, а не фреймворк. Но к библиотеке у меня претензий как раз и нет.


  1. rraderio
    29.08.2017 17:28
    -1

    конкатенация строк гораздо быстрее, чем fmt.Sprintf().
    После того как я поменял последнюю строчку на record := fmt.Sprint(size) + " " + k + "=" + v + "\n", я увидел значительное ускорение

    Почему компилятор сам этого не делает?


    1. powerman
      29.08.2017 20:34
      +1

      Почему компилятор не знает всё про внутреннюю логику и побочные эффекты какой-то функции какого-то пакета и не заменяет её вызов на вызов другой функции? Ну, что Вам сказать… придётся признать, что разработчики компилятора невероятно ленивы и толком не умеют писать компиляторы.


      А если серьёзно, то, как уже упоминали выше, такого рода оптимизации во-первых зачастую преждевременные, во-вторых ухудшают читабельность, в-третьих в следующих версиях языка и/или пакета fmt более медленный вариант сейчас может оказаться более быстрым, в-четвёртых если уж очень хочется, то рекомендации по такого рода оптимизациям должны давать внешние утилиты вроде статических анализаторов кода (линтеров) или IDE.


      1. 0xd34df00d
        30.08.2017 03:01
        +1

        Почему компилятор не знает всё про внутреннюю логику и побочные эффекты какой-то функции какого-то пакета и не заменяет её вызов на вызов другой функции?

        Да не, почему. Авторы функции, предполагая, что sprintf используется часто, и вложиться в производительность имеет смысл, могли бы написать правила по компил-тайм-парсингу строки формата во что надо, получив требуемый результат. От языка требуется только предоставить доступ к AST, все такие и подобные случаи покроются с лихвой.


        А, в Go так нельзя? Ну сорян.


        1. Falstaff
          30.08.2017 04:01
          -1

          А часто ли? Я вот просто не уверен, что стоит огород городить для очень частного случая. Если у горстки человек sprintf() внезапно окажется одной из самых долгих вещей на горячем пути выполнения — ну, наверное они этот путь всё равно будут профилировать, и сделают то же, что сделал автор оригинальной статьи. А просто так, чтобы было… не знаю. Да даже подвиг автора взять — скажем, взять большущий архив, миллион файлов, по… скажем, пять записей в расширенном pax-заголовке для каждого из файлов. Двести наносекунд экономии на запись, и получится экономия аж в одну секунду на создание архива. Кошкины слёзы, а читаемость кода ухудшилась.


        1. aml
          30.08.2017 08:04
          -2

          Как бы мне этого ни хотелось, бюджет у go-team не бесконечный — все фичи не реализуешь. Я думаю, если напишете такой оптимизатор, вам только спасибо скажут.


          1. 0xd34df00d
            30.08.2017 08:13

            Это не оптимизатор, это некоторое свойство языка, позволяющее программе на этом языке, опять же, иметь доступ к AST и всяко им манипулировать на этапе компиляции.


            1. aml
              30.08.2017 08:14
              -2

              Да-да, я об этом. Допишите компилятор. Это ценнее тысячи слов :)


              1. 0xd34df00d
                30.08.2017 08:22
                +1

                Моё сердце уже занято компилятором ghc, увы.


              1. mayorovp
                30.08.2017 09:56

                Так не пропустят же. Девиз go — "никакой магии", а предлагаемая фича — именно что магия, притом самая сильная из известных.


            1. neolink
              30.08.2017 13:12

              а есть примеры кода как это выглядит в реальной жизни (я так понимаю речь про haskel)?


              1. 0xd34df00d
                30.08.2017 20:14
                +2

                Конкретно примеры со sprintf, чтобы с модификаторами форматирования и всем таким, мне неизвестны. Однако, вполне возможно написать библиотеку (заняться, что ли?), которая бы использовалась примерно так:


                import MyPrintf
                
                fmtSomeArgs :: Int -> String -> Double -> String
                fmtSomeArgs num str anotherNum = [p|%d %s %4.2f|] num str anotherNum

                с парсингом строки формата на этапе компиляции, проверкой типов и прочими ништяками.


                Если упрощать, вот эта вот конструкция [ident|...|] говорит компилятору, что надо передать функции ident (которая является специальной хреновиной — квазиквотатором) содержимое между скобок и практически синтаксически вставить в точку вызова результат этой функции.


                Как пример того, что уже есть — есть куча библиотек для интерполяции, вроде такого:


                import Data.String.Interpolate
                
                data Person = { name :: String, age :: Int, income :: Double }
                
                fmtPerson Person { .. } = [i|This is #{name}, #{age} years old, their income is #{income} and they #{if income > 100000 then "are" else "are not"} rich|]

                Аналогично делаются raw string literals. Есть библиотека для вызова шелла и сторонних процессов:


                import System.Command.QQ
                
                copySomeStuff = do
                    exitCode <- [sh|scp #{remoteHost}:#{remotePath} #{localPath}|]
                    guard $ exitCode == ExitSuccess
                    sha1 <- [sh|sha1sum #{localPath}|]
                    putStrLn sha1

                Тут, кстати, и прелести системы типов видно — sh умеет определять по контексту, хотят от него код возврата, stdout или что-то ещё.


                Есть библиотека для порождения типов dbus-функций:


                import DBus.QuasiQuoter
                
                [dbus| s -> s a{uv} |]

                опишет соответствующий тип хаскелевской функции.


                Можно этой ерундой парсить XML в компил-тайме, можно генерировать XML, можно вообще много чего делать. Самая прелесть — ничего этого не надо держать в ядре языка, всё это можно делать библиотеками.


    1. darth_dolphi
      29.08.2017 23:22

      а буффер быстрее конкатенации…


      1. neolink
        30.08.2017 13:03

        здесь в общем случае нет, так как компилятор сам посчитает длинну и выделит сразу память под конечную строку


        1. darth_dolphi
          31.08.2017 15:50

          это точно? что то я сомневаюсь, компилятор ничего не знает о переменных что будут в рантайме, как он может сразу под конечную строку выделить память?


  1. sand14
    29.08.2017 18:01
    +4

    Лучше было бы создать функцию вычисления длины (пишу в псевдокоде):


    int getLength(string b, s)
    {
      if (b != null)
        return len(b);
    
      return len(s);
    
      // return b != null ? len(b) : len(s);
    }

    А затем в основной функции вызвать ее так:


    length = getLength(b, s);

    Что получаем:


    1. Код основной функции становится чище — в нем показывается, что нужно делать, а не как делать.
      Во вспомогательной функции последовательно проверяем "особые" условия (которые теоретически могут добавляться), каждый раз return-имся, в конце возвращаем основное значение.


    2. Да, избавляемся от действительно не очень красивого "else" в коде основной функции.


    3. И самое главное — избавляемся от того, что код, приведенный в начальном варианте:


      length := len(b)
      if b == nil {
      // No byte slice present. Assume string s should be encoded.
      length = len(s)
      }

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


    4. А на возражение, что вызов функции — накладные расходы, ответить очень просто: а вот пусть в этом случае компилятор показывает чудеса оптимизации и инлайнит вызов этой функции.
      Если вспомогательная функция находится в том же модуле/пакете/сборке, и не видна наружу (internal), то ее вполне можно заинлайнить по умолчанию; хотя можно и помочь компилятору, указав соответствующий атрибут, если таковой есть в языке/платформе.


    1. Alexeyco
      29.08.2017 18:13
      -7

      С вашим подходом не получится хейтить go абсолютно без каких-либо на то оснований. Там выше уже много гигантов программизма, которым достаточно довода «а мне прост не нра», чтобы язык назвать плохим.


      1. SirEdvin
        30.08.2017 09:11
        +2

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


        1. Alexeyco
          30.08.2017 11:40
          +1

          Объективно было бы сравнить с другими языками, противопоставить одной точке зрения другую. Но как можно ожидать объективности от людей, которые как ошалелые минусуют комментарий, в котором констатируется факт: областей видимости в привычном виде в go нет? И побежали минус-минус-минус. Часто слышу от молодняка слово «совок». Ну так я в данный момент в нем. Вот он — совок-то.


          1. SirEdvin
            30.08.2017 11:57
            +2

            В привычном — это в каком? У каждого языка эта "привычность" довольно своя. Вот у python там один ад, в javа вот там все понятно.


            1. Alexeyco
              30.08.2017 12:12
              -1

              Хорошо, в явном. В таком, где нужно было бы писать private, public etc. В go области видимости — часть синтаксиса. Иногда это бесит. Но если поумерить свое эго, то нормально.


              1. SirEdvin
                30.08.2017 12:24

                private, public ect — это модификаторы доступа. Они не имеют отношения к области видимости.
                Ну, так в языках, с которыми я знаком (Java, C#), где-то по другому?


    1. Alexeyco
      29.08.2017 18:17
      -5

      В go, кстати, нет областей видимости, которые надо явно указывать. Если функция или метод структуры начинается со строчной буквы — она не видна извне. А если с заглавной — то видна.


    1. creker
      29.08.2017 21:40
      -4

      Вот когда этих условий станет слишком много, тогда и стоит выделять в функцию. У вас видно больше какое-то маниакальное стремление выделить ерунду в отдельную функцию, тем самым ухудшив читаемость. И не только потому, что эти несчастные несколько строк надо искать по коду. Как раз название getLength абсолютно не отражает сути того, что же сделает функция, да еще и написали вы ее неправильно — изначальный вариант содержит аргументы b и s разных типов. Какую длину, чего она вернет — сумму длин? Одной из двух? Если одну из двух, то по какому условию?


      1. sand14
        30.08.2017 00:13
        +3

        Представляется, что в этом в т.ч. и состоит искусство разработчика — исходя из опыта и знания предметной области уметь загодя определять, будут ли разрастаться в будущем "особые" условия, потребующие отдельной функции.


        И если "да" — то эту функцию лучше сделать сразу, т.к. практика показывает, что "потом", особенно при совместном владении кодом, никто не делает декомпозицию, необходимость которой появилась. В итоге разрастаются простыни кода.


        Также верно, что в случае, когда функция имеет значение только в контексте какой-либо другой функции, то ее имя требует осмысления (т.к. тот же getLength представляется именем функции общего назначения, а не контекстно-зависимой).
        На мой взгляд, хорошим решением здесь является механизм локальных функций или лямбд (насколько знаю, в Golang нет локальных функций, но есть лямбды).


        Но вы почему-то написали несколько придирок не по существу к моим тезисам, и проигнорировали основное — что речь шла о возможных способах замены этого кода:


        length := len(b)
        if b == nil {
            // No byte slice present. Assume string s should be encoded.
            length = len(s)
        }

        Это же никуда не годится.
        Во-первых, нужно помнить, что первая строчка не упадет с исключением.
        Точнее, в случае Go — что функция len имеет декларацию "func len(v Type) int", а не "func len(v Type) (int, error)".
        Но этот код может читать и неподготовленный человек.


        Во-вторых, это же хак — рассчитывать на то, что компилятор транслирует этот исходный код в бинарный так, как будто он написан через else.
        Нечитаемо, и кроме того, поведение компилятора ведь может и измениться.


        1. Alexeyco
          30.08.2017 11:47
          -3

          Сплошные «может быть» и «наверное». Мы вынуждены поверить, что написать так — это правильно просто потому, что «может быть». А «может быть» это ваше существует только потому, что аргументов нет. Есть аргумент — код стал короче. В данном конкретном случае короче он стал из-за упрощения конструкции. Но мы, конечно, должны исходить из того, что компилятор «может быть» такой код воспримет хуже. И конечно, варианта, что такой код он воспримет лучше, быть не может. Все правильно.

          В 1933 году Петр Борисович Ганнушкин ввел термин "салонное слабоумие". Символично, но я готов дополнить его теорию. Те, кто не хочет признавать истину авторитетов должны быть такими вот салонно слабоумными изгнаны.


          1. SirEdvin
            30.08.2017 11:59
            +3

            В 1933 году Петр Борисович Ганнушкин ввел термин "салонное слабоумие". Символично, но я готов дополнить его теорию. Те, кто не хочет признавать истину авторитетов должны быть такими вот салонно слабоумными изгнаны.

            Звучит иронично, потому что 90% golang построено на авторитетный решениях, с которыми далеко не все согласны.


            1. Alexeyco
              30.08.2017 12:16
              -3

              Правильно! И они что делают? Аргументируют. Может быть, я ошибаюсь, но как-то не припомню, чтобы те или иные решения были выкачены просто потому. Обычно аргументация присутствует. Либо говорят «мы считаем, что так и так — это правильно». Ок, я услышал — это точка зрения.

              Пример — отсутствие исключений. Долгий был срач на эту тему, но аргументация от разработчиков была. Разве нет? Если бы они заявили «исключений не будет потому, что они не нужны» без объяснения, почему не нужны или с неубедительной аргументацией, я бы так же точно ополчился на них.

              Но вообще-то надо отвечать за себя. Причем тут ваш комментарий?


              1. SirEdvin
                30.08.2017 12:27
                +2

                Аргументируют.

                Или делают вид, что аргументируют. С одной стороны "у нас логичный и строгий язык", а с другой "len(nil) == 0". С одной стороны "исключения плохо, потому что везде приходится фигашить проверки", а по факту стало еще хуже, так как вместо типизированных исключений мы получаем переменную err, которую так же надо обрабатывать, но она менее прозрачная. Шаблонные типы не нужны, потому что это "усложняет язык", а вот кодогенерация — нет.


                1. Alexeyco
                  30.08.2017 12:31
                  -2

                  С третьей стороны, когда анонсировали 2.0 (или рассуждали о том, каким он будет), я своими глазами помню как они написали что-то типа «да, мы говорили, что дженерики — гумно, но если надо, то давайте сразу подумаем, чего не хватает, чтобы это заранее заложить».


            1. Alexeyco
              30.08.2017 12:22
              -2

              Можно вас и вообще всех попросить слить мне карму окончательно, чтобы каждый раз, когда у меня возникнет желание что-то прокомментировать, я сразу же это видел и бросал это дело? Спасибо.


              1. SirEdvin
                30.08.2017 12:24

                У меня вообще не хватает рейтинга, что бы карму сливать :)


                1. Alexeyco
                  30.08.2017 12:27
                  -4

                  Прошу прощения. Я тогда дополню, чтобы лучше сливалось. Хуесосы. Вы все. Администрация — тоже. Я так не думаю, но мне поднадоело. Я знаю, что если мне не слить карму, я рано или поздно приду к мысли «да ладно, напишу комментик».


  1. ainoneko
    29.08.2017 18:22
    +2

    а вот пусть в этом случае компилятор показывает чудеса оптимизации и инлайнит вызов этой функции.
    Говорят, что функции «инлайнятся», но не слишком хорошо: только те, что не вызывают других функций.


  1. sand14
    30.08.2017 00:28
    +1

    По-моему, ваш первый пул-реквест нужно было принять (почему — написал выше), а вот второй как раз отклонить:


    • Конкатенация строк вместо форматирования очень сильно снижает читаемость кода.
    • Что касается снижения производительности в случае использования форматирования строк, то представляется, что функция, которая внутри себя форматирует строки, явно не предназначена для вызова в цикле из миллиона итераций.
      А в случае однократного (или, пусть несколько десятков) вызова — как раз тот случай, когда можно пожертвовать производительностью даже в несколько раз ради читаемости.
      Кроме того, если эта функция форматирует строки, вряд ли она предназначена для вызова рядом функциями, быстро вычисляющими какую-нибудь математику. Скорее всего, эта функция вызывает в контексте каких-то еще более медленных функций.


  1. bat
    30.08.2017 07:55

    size := len(k) + len(v) + padding
    size += len(strconv.Itoa(size))
    record := fmt.Sprintf("%d %s=%s\n", size, k, v)

    а что будет, если len(k) + len(v) + padding = 99 ??


    1. neolink
      30.08.2017 12:58

      и что же будет?


      1. bat
        30.08.2017 13:07

        будет баг нет?
        как я понял size так же учитывает и свою собственную длину в строковом представлении


      1. bat
        30.08.2017 13:11
        +2

        точно, в глянул в оригинале, там повторное кодирование строки, если длина не та
        это ж пипец

        // formatPAXRecord formats a single PAX record, prefixing it with the
        // appropriate length.
        func formatPAXRecord(k, v string) string {
        	const padding = 3 // Extra padding for ' ', '=', and '\n'
        	size := len(k) + len(v) + padding
        	size += len(strconv.Itoa(size))
        	record := fmt.Sprintf("%d %s=%s\n", size, k, v)
        
        	// Final adjustment if adding size field increased the record size.
        	if len(record) != size {
        		size = len(record)
        		record = fmt.Sprintf("%d %s=%s\n", size, k, v)
        	}
        	return record
        }
        


        ps
        формат PAX record весьма неудачен, имха

        pps
        CL автора такие не принят


        1. neolink
          30.08.2017 13:42
          +1

          ну его поправили чутка и приняли: github.com/golang/go/commit/23cd87eb0a2d49a3208824feaf34d8b852da422f

          ну

          func formatPAXRecord(k, v string) (string, error) {
          	const padding = 3 // Extra padding for ' ', '=', and '\n'
          	size := len(k) + len(v) + padding
          	sizeLen := len(strconv.Itoa(size))
          	size += sizeLen
          	sizeStr := strconv.Itoa(size)
          	if len(sizeStr) != sizeLen {
          		sizeStr = strconv.Itoa(size + len(sizeStr) - sizeLen)
          	}
          
          	record := sizeStr + " " + k + "=" + v + "\n"
          
          	return record, nil
          }
          

          не знаю насколько читаем этот вариант, но для начального кода:
          BenchmarkFib10-8 10000000 163 ns/op 32 B/op 2 allocs/op
          для этого
          BenchmarkFib10-8 20000000 94.9 ns/op 16 B/op 1 allocs/op
          тоже чтоли пул риквест сделать


          1. bat
            30.08.2017 15:17

            хотел придраться к тому что strconv.Itoa(size) вызывается дважды, но бенч показывает одну алокацию ))


            1. neolink
              30.08.2017 17:00

              ну size то меняется между ними поэтому тут одной никак
              по поводу аллока — это то что выделяется в куче, то что выделяется на стеке там не показывается (ибо быстро и так легко не отследишь)
              кстати это на 1.9 были результаты ради интереса проверил на 1.8:
              BenchmarkFib10-8 5000000 247 ns/op 32 B/op 5 allocs/op
              BenchmarkFib10-8 10000000 181 ns/op 16 B/op 4 allocs/op

              получается неплохо так доработали компилятор в 1.9 — как миниму он сумел все вызовы strconv.Itoa на стеке разместить


              1. bat
                30.08.2017 18:53

                была мысль что заинлайнил вызовы strconv.Itoa и разместил результат на стеке
                отличный результат на 1.9 надо переходить


            1. sand14
              30.08.2017 19:24

              там целом повторяется строка


              strconv.Itoa(size) + " " + k + "=" + v + "\n"

              как и исходная


              fmt.Sprintf("%d %s=%s\n", size, k, v)


  1. turbanoff
    30.08.2017 12:03
    +3

    Простите, а как расшифровывается CL?


    1. neolink
      30.08.2017 13:00

      Change List


  1. Lenz007
    31.08.2017 12:19
    +3

    Странная логика у автора, вместо того чтобы попытаться оптимизировать работу библиотечной функции fmt.Sprintf() он заменяет ее на черти что, и называет это оптимизацией.