image
Фото Roman Pronskiy


Это перевод поста одного из главных разработчиков языка Go, Расса Кокса, где он в традиционном для новогоднего времени формате дает себе обещания и планирует выполнить их.


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


Каждый год я ставлю перед собой цель — помочь Go-разработчикам. Я хочу быть уверен, что то, что делают создатели Go, оказывает положительное влияние на всех Go-разработчиков. Потому что у них есть масса способов совершить ошибку: например, можно потратить слишком много времени на очистку или оптимизацию кода, которому это не требуется; откликаться только на самые распространенные или недавние жалобы и запросы; излишне сосредотачиваться на краткосрочных улучшениях. Поэтому так важно взглянуть на все со стороны и заняться тем, что принесет больше всего пользы для Go-сообщества.


В этой статье я опишу несколько основных задач, на которых я сосредоточусь в этом году. Это мой собственный список, а не всей команды создателей Go.


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


Алиасы типов


У нас есть постоянно всплывающая проблема с перемещением типов из одного пакета в другой при масштабных рефакторингах кодовой базы. Мы пытались решить ее в прошлом году с помощью общих алиасов, но это не сработало: мы слишком плохо объяснили суть сделанных изменений, да и сами изменения запоздали, код не был готов к выходу Go 1.8. Извлекая уроки из этого опыта, я выступил и написал статью о лежащей в основе проблеме, и это породило продуктивную дискуссию в баг-трекере Go, посвященную возможным решениям. Похоже, что внедрение более ограниченных алиасов типов будет правильным следующим шагом. Надеюсь, что они появятся в Go 1.9. #18130


Управление пакетами


В феврале 2010 я разработал для Go поддержку скачивания опубликованных пакетов (goinstall, ставшую go get). С тех пор многое произошло. В частности, экосистемы других языков серьезно подняли планку ожиданий для систем управления пакетами, а OpenSource-сообщество по большей части согласно на семантическое версионирование, дающее базовое понимание о совместимости разных версий. Здесь Go нуждается в улучшениях, и группа людей уже работает над решением. Я хочу удостовериться, что эти идеи будут грамотно интегрированы в стандартный инструментарий Go. Хочу, чтобы управление пакетами стало еще одним достоинством Go.


Улучшения в системе сборки


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


Сборки могут быть очень медленными, потому что утилита go недостаточно активно кеширует результаты сборки. Многие не понимают, что go install сохраняет результаты своей работы, а go build — нет, поэтому раз за разом запускают go build и сборка проходит ожидаемо медленно. То же самое относится к повторяющимся go test без go test –i при наличии модифицированных зависимостей. По мере возможности все типы сборок должны быть инкрементальными. #4719


Результаты тестов тоже нужно кешировать: если входные данные не менялись, то обычно нет нужды перезапускать сам тест. Это сильно удешевит запуск «всех тестов» при условии незначительных изменений или их отсутствии. #11193


Работа вне GOPATH должна поддерживаться почти так же, как и работа внутри нее. В частности, должна быть возможность запустить git clone, войти в директорию с помощью cd, а затем запустить команду go, и чтобы все это прекрасно работало. Управление пакетами лишь повышает важность этой задачи: вы должны иметь возможность работать с разными версиями пакета (скажем, v1 и v2) без необходимости поддерживать для них отдельные GOPATH. #17271


Основной набор кода


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


Сейчас не существует общепринятого характерного набора кода, чтобы проводить подобный анализ: каждый должен сначала создать свой проект, а это слишком большой объем работы. Я хотел бы собрать единый, автономный Git-репозиторий, содержащий наш официальный базовый набор кода для проведения анализа, с которым может сверяться сообщество. Возможная отправная точка — топ-100 Go-репозиториев на GitHub по количеству звезд, форков или обоих параметров.


Автоматический vet


Go-дистрибутив поставляется с мощным инструментом — go vet, который указывает на распространенные ошибки. Планка для этих ошибок высокая, так что к его сообщениям нужно прислушиваться. Но главное — не забывать запустить vet. Хотя было бы удобнее об этом не помнить. Я думаю, что мы могли бы запускать vet параллельно с финальным компилированием и линковкой бинарника, которые происходят при выполнении go test без какого-либо замедления. Если у нас это получится и если мы ограничим разрешенные vet-проверки выборкой, которая на 100% точна, то мы вообще сможем превратить передачу vet в предварительное условие для запуска теста. Тогда разработчикам не понадобится помнить о том, что нужно запустить go vet. Они запустят go test, и vet изредка будет сообщать что-то важное, избегая лишней отладки. #18084 #18085


Ошибки и общепринятые методики


Частью общепринятой практики по сообщениям об ошибках в Go является то, что функции включают в себя релевантный доступный контекст, в том числе и информацию о том, попытка какой операции была осуществлена (имя функции и ее аргументы). Например, эта программа


err := os.Remove("/tmp/nonexist")
fmt.Println(err)

выводит


remove /tmp/nonexist: no such file or directory

Далеко не весь Go-код поступает так же, как это делает os.Remove. Очень много кода делает просто


if err != nil {
    return err
}

по всему стеку вызовов и выкидывает полезный контекст, который стоило бы показывать (например, как remove /tmp/nonexist: выше). Я хотел бы понять, не ошибаемся ли мы в наших ожиданиях по включению контекста, и можем ли сделать что-нибудь, что облегчит написание кода, возвращающего более информативные ошибки.


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


Контекст и лучшие методики


В Go 1.7 мы добавили новый пакет context для хранения информации, которая как-либо связана с запросом (например, о таймаутах, о том, отменен ли запрос и об авторизационных данных). Индивидуальный контекст неизменяем (как строки или целочисленные значения): можно получить только новый, обновленный контекст и передать его явным образом вниз по стеку вызовов, либо (что реже встречается) обратно наверх. Контекст сегодня передается через API (например, database/sql и net/http) в основном для того, чтобы они могли остановить обработку запроса, когда вызывающему больше не нужен результат обработки. Информация о таймаутах вполне подходит для передачи в контексте, но совершенно не подходит для опций базы данных, например, потому что они вряд ли будут так же хорошо применяться ко всем возможным БД-операциям в ходе выполнения запроса. А что насчет источника времени или логгера? Можно ли хранить их в контексте? Я попытаюсь понять и описать критерии того, что можно использовать в контексте, а что нельзя.


Модель памяти


В отличие от других языков, модель памяти в Go намеренно сделана скромной, не дающей пользователям много обещаний. На самом в деле в документе сказано, что особо-то и читать его не стоит. В то же время она требует от компилятора больше, чем в других языках: в частности, гонка на целочисленных значениях не является оправданием для произвольного поведения вашей программы. Есть и полные пробелы: например, не упоминается пакет sync/atomic. Думаю, разработчики основного компилятора и runtime-системы согласятся со мной в том, что эти атомики должны вести себя так же, как seqcst-атомики в С++ или volatile в Java. Но при этом мы должны аккуратно вписать это в модель памяти и в длинный-предлинный пост в блоге. #5045 #7948 #9442


Неизменяемость


Race detector — одна из самых любимых возможностей Go. Но не иметь race вообще было бы еще лучше. Мне бы очень хотелось, чтобы существовал разумный способ интегрировать в Go неизменяемость ссылок, чтобы программисты могли делать ясные, проверенные суждения о том, что может и что не может быть написано, тем самым предотвращая определенные race condition на этапе компиляции. В Go уже есть один неизменяемый тип— string; было бы хорошо задним числом определять, что string — это именованный тип (или алиас) для неизменяемого []byte. Не думаю, что это удастся реализовать в этом году, но я хочу разобраться в возможных решениях. Javari, Midori, Pony и Rust уже обозначили интересные варианты, к тому же есть еще целый ряд исследований на эту тему.


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


Дженерики


Самые жаркие споры между Go-разработчиками и программистами на других языках разгораются по поводу того, следует ли Go поддерживать дженерики (или как давно это должно было произойти). Насколько я знаю, команда создателей Go никогда не говорила, что в Go не нужны дженерики. Мы говорили, что существуют более важные задачи, требующие решения. Например, я считаю, что улучшение поддержки управления пакетами окажет куда более сильное положительное влияние на большинство Go-разработчиков, чем внедрение дженериков. Но в то же время мы осознаём, что в ряде случаев нехватка параметрического полиморфизма является серьезным препятствием.


Лично я хотел бы иметь возможность писать общие функции для работы с каналами, например:


// Join делает все полученные во входных каналах сообщения 
// доступными для получения из возвращаемого канала.
func Join(inputs ...<-chan T) <-chan T

// Dup дублирует сообщени из c и в c1 и в c2
func Dup(c <-chan T) (c1, c2 <-chan T)

Также я хотел бы иметь возможность написать на Go более высокоуровневые абстракции обработки данных, аналогичные FlumeJava или LINQ, чтобы ошибки несоответствия типов ловились при компиляции, а не при выполнении. Можно написать еще кучу структур данных или алгоритмов с использованием дженериков, но я считаю, что такие высокоуровневые инструменты более интересны.


На протяжении нескольких лет мы старались найти правильный способ внедрения дженериков в Go. Последние несколько предложений касались разработки решения, которое обеспечивало бы общий параметрический полиморфизм (как chan T) и унификацию string и []byte. Если последнее решается параметризацией на основе неизменяемости, как описано в предыдущей части, тогда, возможно, это упростит требования к архитектуре дженериков.


Когда в 2008 я впервые задумался о дженериках в Go, главными примерами были C#, Java, Haskell и ML. Но ни один из подходов, реализованных в этих языках, не выглядел идеально подходящим для Go. Сегодня есть более свежие решения, например, в Dart, Midori, Rust и Swift.


Прошло несколько лет с тех пор, как мы отважились взяться за эту тему и рассмотреть возможные варианты. Вероятно, пришло время снова оглядеться по сторонам, особенно в свете информации об изменяемости/неизменяемости и решениях из новых языков. Не думаю, что дженерики появятся в Go в этом году, но я хотел бы разобраться в этом вопросе.

Поделиться с друзьями
-->

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


  1. sshikov
    29.01.2017 15:29
    +1

    >Когда в 2008 я впервые задумался о дженериках в Go, главными примерами были C#, Java, Haskell и ML.

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


    1. SerCe
      29.01.2017 15:52

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

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


      1. sshikov
        29.01.2017 17:09
        +1

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


        1. jetu
          30.01.2017 02:51

          А где сказано, что выбор, который был сделан в Java, хороший?


          1. sshikov
            30.01.2017 09:32

            >главными примерами были C#, Java, Haskell и ML. Но ни один из подходов, реализованных в этих языках, не выглядел идеально подходящим для Go.

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


            1. Shtucer
              30.01.2017 11:31

              Как это так ловко получилось "не выглядел идеально подходящим для Go" == "не идеальный, но подходящий"?


              1. sshikov
                30.01.2017 12:06

                А как по вашему-то? Если он совсем не подходит — то зачем его рассматривать, как будто других мало? Причем других успешных (ну да, с поправкой на то, что это было в 2008 — все может быть было и не так радужно — но все равно уже была скала, например, где с этим все гораздо лучше).

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


                1. Shtucer
                  30.01.2017 12:15
                  +1

                  Чтобы понять, что технология не подходит, нужно её рассмотреть. А не строить теории о заговоре байткода.


                1. Sirikid
                  31.01.2017 03:00

                  Потому что дженерики через стирание типов это вполне рабочая реализация, например в C точно так же реализована функция сортировки произвольных массивов, вот её заголовок:


                  void qsort(void *base, size_t nitems, size_t size, int (*compare)(const void *, const void*)); 

                  Все отличие от Java только в том что типы нужно самому приводить.


  1. ertaquo
    29.01.2017 16:00
    +1

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

    func Qwerty(a string, b string = "zxc") string {
      return a + b;
    }
    


    1. SamKrew
      29.01.2017 16:25
      -9

      Это как раз про дженерики:
      Qwerty(«hello»)
      Qwerty(«hello», «world»)


      1. ertaquo
        29.01.2017 19:44

        Нет, с дженериками, насколько я понимаю, было бы как-то так:

        func <T> Qwerty(a, b T) T {
          return a + b;
        }
        


  1. GreenStore
    29.01.2017 18:27
    +3

    > Результаты тестов тоже нужно кешировать: если входные данные не менялись

    Сомнительное предложение. Повторение тестов может выявить плавающие ошибки.


  1. Alexeyco
    29.01.2017 19:55
    -2

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


    1. rustler2000
      29.01.2017 23:12

      Так если на звезды у "генераторов генериков" посмотреть, то понятно чего их откладывают


  1. vasiliysenin
    30.01.2017 16:04

    Когда в 2008 я впервые задумался о дженериках в Go, главными примерами были C#, Java, Haskell и ML. Но ни один из подходов, реализованных в этих языках, не выглядел идеально подходящим для Go. Сегодня есть более свежие решения, например, в Dart, Midori, Rust и Swift.


    Что такое Midori? От какого года оригинальная статья?


    1. mkevac
      30.01.2017 16:05

      Нажал на ссылку для вас: January 18, 2017


      1. vasiliysenin
        30.01.2017 17:22
        +1

        Спасибо. С вашей помощью я всё таки нашел эту ссылку.
        https://research.swtch.com/go2017

        По поводу Midori я нашел только
        https://ru.wikipedia.org/wiki/Midori_(операционная_система)
        а Go язык программирования, не понятно какие дженерики в операционной системе Midori?
        Может быть есть язык программирования с названием Midori?


        1. mkevac
          30.01.2017 17:23

          Я, увы, тоже не знаю о какой Midori говорит Russ Cox.


          1. romangoward
            31.01.2017 00:12

            http://joeduffyblog.com/2015/11/03/blogging-about-midori/


            Эта ссылка есть у твиттере у Russ.