Фото 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)
ertaquo
29.01.2017 16:00+1А мне очень хотелось бы иметь в Go задаваемые по умолчанию значения аргументов функций и элементов структур (насколько понимаю код компилятора, там аргументы функций хранятся как структуры как раз). Чтобы написать как-то так:
func Qwerty(a string, b string = "zxc") string { return a + b; }
GreenStore
29.01.2017 18:27+3> Результаты тестов тоже нужно кешировать: если входные данные не менялись
Сомнительное предложение. Повторение тестов может выявить плавающие ошибки.
Alexeyco
29.01.2017 19:55-2Молодцы. Если не зафейлят дженерики, создадут новый виток хайпа вокруг Go. Ибо пока что дженерики — один из камней преткновения между людьми, которым некогда заниматься глупостями, пока они пишут код и людьми, которым некогда писать код, пока они занимаются глупостями.
rustler2000
29.01.2017 23:12Так если на звезды у "генераторов генериков" посмотреть, то понятно чего их откладывают
vasiliysenin
30.01.2017 16:04Когда в 2008 я впервые задумался о дженериках в Go, главными примерами были C#, Java, Haskell и ML. Но ни один из подходов, реализованных в этих языках, не выглядел идеально подходящим для Go. Сегодня есть более свежие решения, например, в Dart, Midori, Rust и Swift.
Что такое Midori? От какого года оригинальная статья?mkevac
30.01.2017 16:05Нажал на ссылку для вас: January 18, 2017
vasiliysenin
30.01.2017 17:22+1Спасибо. С вашей помощью я всё таки нашел эту ссылку.
https://research.swtch.com/go2017
По поводу Midori я нашел только
https://ru.wikipedia.org/wiki/Midori_(операционная_система)
а Go язык программирования, не понятно какие дженерики в операционной системе Midori?
Может быть есть язык программирования с названием Midori?mkevac
30.01.2017 17:23Я, увы, тоже не знаю о какой Midori говорит Russ Cox.
romangoward
31.01.2017 00:12
sshikov
>Когда в 2008 я впервые задумался о дженериках в Go, главными примерами были C#, Java, Haskell и ML.
Вы это серьезно? Java никогда не была образцом того, как надо делать дженерики. Тут чисто консервативный подход, выбранный с целью сохранить максимальную совместимость — и в итоге, с большим числом сильных ограничений, которые уже много лет аукаются там и тут.
SerCe
Вполне себе образец принятого решения при наличии определенных условий. В Go даже если не гарантируется обратная бинарная совместимость, совместимость на уровне исходных кодов все равно является приоритетом. Вся жизнь про разрешение трейдофов.
sshikov
Ну, дело в том, что то решение оказалось не очень. Понятно что лучше иметь такие дженерики, как в Java, чем никаких, но считать это хорошим образцом наравне с хаскелем — это какой-то явный перебор.
jetu
А где сказано, что выбор, который был сделан в Java, хороший?
sshikov
>главными примерами были C#, Java, Haskell и ML. Но ни один из подходов, реализованных в этих языках, не выглядел идеально подходящим для Go.
Я не знаю, как это еще можно понимать? Хороший, не хороший — но его зачем-то рассматривали как пример для подражания — не идеальный, но подходящий.
Shtucer
Как это так ловко получилось "не выглядел идеально подходящим для Go" == "не идеальный, но подходящий"?
sshikov
А как по вашему-то? Если он совсем не подходит — то зачем его рассматривать, как будто других мало? Причем других успешных (ну да, с поправкой на то, что это было в 2008 — все может быть было и не так радужно — но все равно уже была скала, например, где с этим все гораздо лучше).
Я наоборот пытаюсь понять, зачем они рассматривали реализацию в Java, при том что у Go очевидно другая ситуация. Как минимум — совсем нет никакого байткода, и никаких проблем обратной совместимости, с ним связанных.
Shtucer
Чтобы понять, что технология не подходит, нужно её рассмотреть. А не строить теории о заговоре байткода.
Sirikid
Потому что дженерики через стирание типов это вполне рабочая реализация, например в C точно так же реализована функция сортировки произвольных массивов, вот её заголовок:
Все отличие от Java только в том что типы нужно самому приводить.