Это перевод поста одного из главных разработчиков языка 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 в этом году, но я хотел бы разобраться в этом вопросе.
Автор: Badoo