Перевод блог поста и доклада Russ Cox с GopherCon 2017, с обращением ко всему Go сообществу помочь в обсуждении и планировании Go 2. Видео доклада будет добавлено сразу после опубликования.
25 сентября 2007 года, после того как Роб Пайк, Роберт Грисмайер и Кен Томпсон несколько дней обсуждали идею создания нового языка, Роб предложил имя "Go".
В следующем году, Ян Лэнс Тейлор и я присоединились к команде и мы впятером создали два компилятора и стандартную библиотеку, которые были публично открыты 10 ноября 2009.
В следующие два года, с помощью нового open-source сообщества гоферов, мы экспериментировали и пробовали различные идеи, улучшая Go и ведя его к запланированному релизу Go 1, предложенному 5 октября 2011.
С ещё более активной помощью Go сообщества, мы пересмотрели и реализовали этот план, в итоге выпустив Go 1 28 марта 2012.
Релиз Go 1 ознаменовал кульминацию почти пяти полных лет креативных и неистовых усилий, которые привели нас от выбора имени и обсуждения идей до стабильного готового языка. Он также знаменовал явный переход от изменений и непостоянства к стабильности.
В годы, предшествующие Go 1, мы меняли язык ломая чужие Go программы практически еженедельно. Мы понимали, что это удерживало Go от использования в продакшене, где программы никто не будет переписывать каждую неделю, синхронизируя с изменениями в языке. Как написано в блог посте с анонсом Go 1, главной мотивацией языка было предоставить стабильный фундамент для создания надёжных продуктов, проектов и публикаций (блогов, туториалов, докладов и книг), дав пользователям уверенность, что их программы будут компилироваться и работать без необходимости их менять даже через много лет.
После того, как Go 1 был выпущен, мы знали, что должны провести какое-то время в реальном использовании Go в продакшн среде, для которой он и был создан. Мы явно перешли от изменений языка к использованию Go в наших проектах и улучшения реализации: мы портировали Go на множество новых систем, мы переписали практически все критичные к производительности части, чтобы сделать Go ещё более эффективным и добавили ключевые инструменты вроде race-детектора.
На сегодня у нас есть 5 лет реального опыта использования Go для создания огромных, качественных продакшн-систем. Это дало нам чувство того, что работает, а что нет. И сейчас самое время начать новый этап в эволюции и развитии Go. Сегодня я прошу вас всех, сообщество Go разработчиков, будь вы сейчас тут в зале GopherCon или смотрите видео или читаете это в Go блоге, работать вместе с нами по мере того, как мы будем планировать и реализовывать Go 2.
Далее я расскажу и объясню задачи, которые стоят перед Go 2; наши ограничения и препятствия; сам процесс в целом; важность описания вашего опыта с Go, особенно если он относится к проблемам которые мы можем пытаться решить; возможные варианты решений; как мы будем внедрять Go 2 и как вы все можете в этом помочь.
Задачи
Задачи перед Go сегодня стоят точно такие же, какими были в 2007 году. Мы хотим сделать программистов более эффективными в управлении двумя видами масштабируемости: масштабируемости систем, особенно многопоточных(concurrent) систем, взаимодействующих со многими другими серверами — широко представленными в виде серверов для облака, и масштабируемость разработки, особенно большие кодовые базы, над которыми работают множество программистов, часто удалённо — как, например, современная open-source модель разработки.
Эти виды масштабируемости сегодня присутствуют в компаниях всех размеров. Даже стартап из 5 человек может использовать большие облачные API сервисы, предоставленные другими компаниями и использовать больше open-source софта, чем софта, который они пишут сами. Масштабируемость систем и масштабируемость разработки также актуальны для стартапа, как и для Google.
Наша цель для Go 2 — исправить основные недочёты в Go, мешающие масштабируемости.
(Если вы хотите больше узнать про эти задачи, посмотрите статью Роба Пайка 2012 года “Go at Google: Language Design in the Service of Software Engineering” и мой доклад с GopherCon 2015 “Go, Open Source, Community”.)
Препятствия
Наши задачи перед Go не изменились, но изменились препятствия. Главное из них это уже существующее использование Go. По нашим оценкам, сейчас в мире есть, как минимум, пол миллиона Go программистов, что означает порядка миллиона файлов с исходным кодом на Go и не менее миллиарда строк Go кода. Эти программисты и этот исходный код представляет собой успех Go, но в тоже время является главным препятствием для Go 2.
Go 2 должен способствовать всем этим разработчикам. Мы должны просить их забыть старые привычки и выучить новые только если выгода от этого действительно того стоит. Например, перед Go 1, метод интерфейсного типа error
назывался String
. В Go 1 мы переименовали его в Error
, чтобы отличить типы для ошибок от других типов, который просто могут иметь отформатированное строчное представление. Однажды я реализовывал тип, удовлетворяющий error
интерфейсу, и, не думая, называл метод String
, вместо Error
, что, конечно же, не скомпилировалось. Даже через 5 лет я всё ещё не до конца забыл старый способ. Этот пример проясняющего переименования было важным и полезным изменением для Go 1, но был бы слишком разрушительным для Go 2 без действительно очень весомой причины.
Go 2 должен также хорошо дружить с существующим Go 1 кодом. Мы не должны расколоть Go экосистему. Смешанные программы, в которых пакеты написаны на Go 2 и импортируют пакеты на Go 1 или наоборот, должны безпрепятственно работать в течении переходного периода в несколько лет. Нам ещё предстоит придумать, как именно этого достичь; инструментарий для автоматического исправления и статического анализа вроде go fix определённо сыграют тут свою роль.
Чтобы уменьшить разрушительный эффект, каждое изменение потребует очень бережного обдумывания и планирования, также как и инструментария, что, в результате, ограничит количество изменений, которые мы вообще сможем сделать. Возможно мы можем сделать два или три, но точно не больше пяти.
При этом я не считаю мелкие вспомогательные изменения, как, возможно, разрешение идентификаторов на большем количестве натуральных языков или добавления литералов для чисел в двоичной форме. Подобные мелкие изменения также важны, но их гораздо проще сделать правильно. Сегодня я буду концентрироваться на возможных крупных изменениях, таких как дополнительная поддержка обработки ошибок или добавление неизменяемых (immutable) или read-only значений, или добавления какой-нибудь формы generics, или ещё какой-нибудь важной пока не озвученной темы. Мы сможем сделать только несколько таких крупных изменений. И мы должны будем выбрать их очень внимательно.
Процесс
Это поднимает важный вопрос. Какой процесс разработки Go в целом?
В ранние дни Go, когда нас было всего пятеро, мы работали в паре смежных офисов, разделённых стеклянной стеной. Было очень просто собрать всех в одной комнате, обсудить какую-то проблему, вернуться на свои места и тут же реализовать решение. Если во время реализации возникало какое-то затруднение, было легко снова собраться и обсудить. В офисе Роба и Роберта был маленький диванчик и белая доска, и обычно кто-то из нас заходил и начинал писать пример на доске. Как правило к моменту, когда пример был написан, все остальные находили момент, на котором можно было сделать паузу в текущей задаче, и были готовы сесть и обсудить код. Такой неформальный подход, само собой, невозможно масштабировать на размер Go сообщества сегодня.
Частью нашей работы после релиза Go в open-source было портирование этого неформального процесса в более формальный мир почтовых рассылок и трекеров задач для полумиллиона пользователей, но мне кажется, мы никогда явно не рассказывали, как устроен весь процесс. Возможно, мы даже никогда полностью сознательно не думали об этом. Впрочем, оглядываясь назад, я думаю, что базовый план процесса, которому Go следовал с самого зарождения выглядит примерно так:
Первый шаг — использовать Go, чтобы наработать опыт работы с ним.
Второй шаг — идентифицировать проблему в Go, которая, вероятно, нуждается в решении и выразить её, объяснить другим, представить её в письменном виде.
Третий шаг — предложить решение проблемы, обсудить его с другими и пересмотреть решение, основываясь на этом обсуждении.
Четвертый шаг — реализовать решение, проверить его и улучшить, основываясь на результатах проверки.
И, наконец, пятый шаг — внедрить решение, добавив его в язык или стандартную библиотеку или в набор инструментов, которые люди используют каждый день.
Один и тот же человек не обязательно должен делать все эти шаги сам. На самом деле, обычно как раз на каждом шагу вовлечено много разных людей и много решений может быть предложено для одной и той же проблемы. Также, на каждом этапе мы можем решить не идти дальше и вернуться на шаг назад.
И хотя я не думаю, что мы когда-либо рассказывали про этот процесс целиком, но мы объясняли его по частям. В 2012, когда мы выпустили Go 1 и сказали, что настало время начать использовать Go и перестать изменять, мы объясняли первый шаг. В 2015, когда мы представили изменения в процесс предложений (proposals) для Go, мы объясняли шаги 3, 4 и 5. Но мы никогда не объясняли второй шаг подробно, и я бы хотел сделать это сейчас.
(Более подробно про разработку Go 1 и про прекращение изменений в языке, посмотрите доклад Роба Пайка и Эндрю Герранда на OSCON в 2012 году “The Path to Go 1.”. Более детально про процесс предложений можно посмотреть в докладе Эндрю Герранда на GopherCon в 2015 “How Go was Made” и в документации к самому процессу)
Объяснение проблем
Объяснение проблемы состоит из двух частей. Первая часть — лёгкая — это просто озвучить, в чём, собственно, проблема заключается. Мы, разработчики, в целом достаточно хорошо это умеем. В конце концов, каждый тест, который мы пишем это формулировка проблемы, которая должна быть решена, причём написанная на таком точном языке, который поймёт даже компьютер. Вторая часть — сложная — заключается в том, чтобы описать важность проблемы достаточно хорошо, чтобы все остальные поняли, почему мы должны тратить время на её решение и его поддержку. В отличие от точной формулировки проблемы, мы не так часто описываем их важность и мы не слишком это хорошо умеем. Компьютер никогда нас не спросит “Почему этот случай для теста важен? А ты уверен, что это именно та проблема, которую ты должен решать? Точно ли решение этой проблемы это самая важная задача, которой ты должен заниматься?”. Возможно, однажды так и будет, но точно не сегодня.
Давайте взглянем на старый пример из 2011. Вот, что я написал про переименование os.Error
в error.Value
, когда мы планировали Go 1.
error.Value
(rsc) Проблема, которую мы имеем в низкоуровневых библиотеках заключается в том, что всё зависит от “os” из-за os.Error, поэтому сложно делать вещи, которые пакет os сам мог бы использовать (как пример с time.Nano ниже). Если бы не os.Error, не было было бы столько других пакетов, которые зависят от пакета os. Сугубо вычислительные пакеты вроде hash/* или strconv или strings или bytes могли бы обойтись без него, к примеру. Я планирую исследовать (пока что ничего не предлагая) определить пакет error примерно с таким API:
package error
type Value interface { String() string }
func New(s string) Value
Он начинается с краткой однострочной формулировки проблемы: в низкоуровневых библиотеках всё импортирует “os” ради os.Error
. Далее идут 5 строк, которые я подчеркнул, описывающие значимость проблемы: пакеты, которые “os” использует не могут использовать тип error в своих API, и другие пакеты зависят от os
по причинам никак не связанным с работой операционной системы.
Убедят ли вас эти 5 строк, что проблема стоит внимания? Это зависит от того, насколько хорошо вы можете заполнить контекст, который я оставил за рамками: чтобы быть понятым, нужно уметь предугадать, что другие люди знают. Для моей аудитории в то время — десять других людей в команде Google работающей над Go, которые читали этот документ — этих 50 слов было достаточно. Чтобы представить ту же самую проблему аудитории на конференции GothamGo прошлой осенью — аудитории с гораздо более разнообразным опытом — я должен быть предоставить больше контекста, и я использовал уже 200 слов, плюс примеры реального кода и диаграмму. И это факт, что современное Go сообщество, которое пытается объяснить важность какой-либо проблемы, должно добавлять контекст, причём проиллюстрированный конкретными примерами, который можно было бы исключить в беседе с вашими коллегами, например.
Убедить других, что проблема действительно важна — это ключевой шаг. Если проблема кажется не такой уж важной, то практически любое решение выглядит слишком дорогостоящим. Но для действительно важной проблемы, почти всегда есть несколько не таких уж и дорогих решений. Когда мы расходимся во мнениях о том, принимать или нет какое-то решение, обычно это означает, что мы расходимся в оценке важности решаемой проблемы. Это настолько важный момент, что я хочу показать два недавних примера, хорошо иллюстрирующих его, по крайней мере, в ретроспективе.
Пример: високосные секунды
Мой первый пример связан с временем.
Представьте, что вы хотите замерить, сколько времени занимает некое событие. Вы сначала запоминаете время начала, запускаете событие, записываете время конца и затем вычитаете время начала из времени конца. Если событие заняло 10 миллисекунд, операция вычитания вернёт вам ровно 10 миллисекунд, возможно плюс-минус маленькую погрешность измерения.
start := time.Now() // 3:04:05.000
event()
end := time.Now() // 3:04:05.010
elapsed := end.Sub(start) // 10 ms
Эта очевидная процедура может не сработать во время “високосной секунды” (leap second). Когда наши часы не совсем точно синхронизированы с дневным вращением Земли, специальная високосная секунда — официально это секунды 23:59 и 60 — вставляется прямо перед полуночью. В отличие от високосного года, у високосных секунд нет легко предсказуемого паттерна, что затрудняет автоматизацию их учета в программах и API. Вместо того, чтобы ввести специальную, 61-ю, секунду, операционные системы обычно реализуют високосную секунду переводя часы на секунду назад аккурат перед полуночью, так что при этом 23:59 происходит дважды. Такой сдвиг часов выглядит, как поворот времени вспять, и наш замер 10-миллисекундного события теперь может оказаться отрицательным значением в 990 миллисекунд.
start := time.Now() // 11:59:59.995
event()
end := time.Now() // 11:59:59.005 (really 11:59:60.005)
elapsed := end.Sub(start) // –990 ms
Поскольку обычные часы оказываются неточными для измерений длительности событий во время подобных сдвигов времени, операционные системы предоставляют второй тип часов — монотонные часы, которые просто считают секунды и никогда не изменяются и не сдвигаются.
Только при этом нестандартном сдвиге часов, монотонные часы не особо лучше обычных часов, которые, в отличие от монотонных, умеют показывать текущее время. Поэтому, ради простоты API пакета time
в Go 1 доступ есть только к обычным часам компьютера.
В октябре 2015 появился баг-репорт о том, что Go программы некорректно возвращают длительность событий во время подобных сдвигов часов, особенно в случае с високосной секундой. Предложенное решение было также и заголовком репорта: “Добавить новый API для доступа к монотонным часам”. Тогда я утверждал, что проблема не была достаточно значима, чтобы ради неё создавать новый API. Несколько месяцев перед этим, для високосной секунды в середине 2015 года, Akamai, Amazon и Google научились замедлять свои часы таким образом, что эта дополнительная секунда “размазывалась” по всему дню и не нужно было переводить часы назад. Всё шло к тому, что повсеместное использовать этого подхода “размазывания секунды” позволило бы избавиться от перевода часов вообще и проблема исчезнет сама собой. Для контраста, добавление нового API в Go добавило бы две новые проблемы: мы должны были бы объяснять про эти два типа часов, обучать пользователей когда использовать какой из них и конвертировать массу существующего кода, и всё лишь для ситуации, которая очень редка и, скорее всего, вообще исчезнет сама.
Мы поступили так, как делаем всегда, когда решение проблемы не очевидно — мы стали ждать. Ожидание даёт нам больше времени, чтобы накопить больше опыта и углубить понимание проблемы, плюс больше времени на поиски хорошего решения. В этом случае, ожидание добавило понимание серьёзности проблемы, в виде сбоя в работе Cloudflare, к счастью незначительного. Их Go код замеряющий длительность DNS запросов во время високосной секунды в конце 2016 года возвращал негативное значение, подобное примеру с -990 миллисекундами выше, и это приводило к панике на их серверах, поломав около 0.2% всех запросов в самом пике проблемы.
Cloudflare это именно тот тип облачных систем, для которых Go и создавался, и у них случился сбой в продакшене из-за того, что Go не мог замерять время правильно. Дальше, и это ключевой момент тут, Cloudflare написали про свой опыт — Джон Грэхем-Камминг опубликовал блог-пост “Как и почему високосная секунда повлияла на DNS Cloudflare”. Рассказав конкретные детали и подробности инцидента и их опыт работы с Go, Джон и Cloudflare помогли нам понять, что проблема неточного замера во время високосной секунды была слишком важной, чтобы оставлять её не решенной. Через два месяца после публикации статьи, мы разработали и реализовали решение, которое появится в Go 1.9 (и, кстати, мы сделали это без добавления нового API).
Пример: алиасы
Мой второй пример о поддержке алиасов в Go.
За последние несколько лет, Google собрал команду, сфокусированную на крупномасштабных изменениях в коде, вроде миграций API и исправлений багов по всей кодовой в базе, состоящей из миллионов файлов исходных кодов и миллиардов строк кода, написанных на C++, Go, Java, Python и других языках. Одна из вещей, которую я усвоил из их трудов, было то, что при замене в API старого имени на новое, важно иметь возможность делать изменения шаг за шагом, а не всё за один раз. Чтобы это сделать, должна быть возможность задекларировать, что под старым именем, подразумевается новое. В C++ есть #define, typedef и использование деклараций позволяют это сделать, но в Go такого механизма не было. И поскольку одной из главных задача перед Go стоит умение масштабироваться в больших кодовых базах, было очевидно, что нам нужен какой-то механизм перехода от старых имён к новым во время рефакторинга, и что другие компании также упрутся в эту проблему по мере роста их кодовых баз на Go.
В марте 2016 я начал обсуждать с Робертом Грисмайером и Робом Пайком то, как Go мог бы справляться с многошаговым рефакторингом кодовых баз, и мы пришли к идее алиасов (alias declarations), которые были именно тем механизмом, что нужно. В тот момент я был очень доволен тем, как Go развивался. Мы обсуждали идею алиасов ещё с ранних дней Go — на самом деле первый черновик спецификации Go содержит пример, использующий алиасы — но, каждый раз при обсуждении алиасов, и, чуть позже, алиасов типов, мы не сильно понимали для чего они могут быть важны, поэтому мы отложили идею. Теперь же мы предлагали добавить алиасы в язык не потому что они были прямо элегентным концептом, а потому что они решали очень серьезную практическую проблему, к тому же помогали Go лучше решать поставленную перед ним задачу масштабируемости разработки. Я искренне надеюсь, что это послужит хорошей моделью для будущих изменений в Go.
Чуть позднее той же весной Роберт и Роб написали предложение, и Роберт предоставил его на коротком докладе (lightning talk) на GopherCon 2016. Следующие несколько месяцев были достаточно смутными, и точно не могут служить примером того, как делать изменения в Go. Один из многих уроков, который мы тогда вынесли была важность описания значимости проблемы.
Минуту назад я объяснил вам суть проблемы, дав некоторую минимальную информацию о том, как и почему эта проблема может возникнуть, но не дав конкретных примеров о том, как вам вообще решить, коснётся ли эта проблема вас когда-нибудь или нет. То предложение и доклад оперировали абстрактными примерами, включающими пакеты C, L, L1 и C1..Cn, но ничего конкретного, с чем программисты могли ассоциировать проблему. В результате, большая часть ответной реакции от сообщества была основана на идее того, что алиасы решают проблему Google, и которая не актуальна для остальных.
Аналогично тому, как мы в Google поначалу не понимали важности корректной обработки високосной секунды, также мы и не донесли эффективно Go сообществу важность и необходимость уметь справляться с постепенной миграцией и исправлением кодовых баз во время крупномасштабного рефакторинга.
Осенью мы начали заново. Я выступил с докладом и написал статью, подробно объясняющую проблему, используя множество конкретных примеров из реальных open-source проектов, показывающих, что эта проблема актуальна для всех, а не только для Google. Теперь, после того как больше людей поняли проблему и могли оценить её важность, мы смогли начать продуктивное обсуждение о том, какое решение подойдёт лучше всего. Результатом этого стало то, что алиасы типов будут включены в Go 1.9 и помогут Go лучше масштабироваться во всё более крупных кодовых базах.
Рассказы об опыте использования
Один из уроков тут в том, что это сложно, но критически важно описывать важность проблемы понятным способом, чтобы другие люди, работающие в другой среде и условиях, могли её понять. Для обсуждения крупных изменений в Go в сообществе, мы должны будем уделять особое внимание этому процессу подробного описания важности каждой проблемы, которую мы будем пытаться решить. Самый хороший способ сделать это — показать как проблема влияет на реальные программы или реальные системы, как в блог посте Cloudflare или моей статье про рефакторинг.
Такие рассказы об опыте использования превращают проблемы из абстрактной в конкретную и позволяют нам понять её значимость. Они также выступают в роли тестовых примеров: любое предложенное решение можно проверить на этих примерах и оценить эффект.
К примеру, недавно я изучал проблему дженериков (generics), и пока что я не вижу в голове чёткой картины подробного и детального примера проблемы, для решения которой пользователям Go нужны дженерики. Как результат, я не могу чётко ответить на вопрос о возможном дизайне дженериков — например, стоит ли поддерживать generic-методы, тоесть методы, которые параметризованы отдельно от получателя (receiver). Если бы у нас был большой набор реальных практических проблем, мы бы смогли отвечать на подобные вопросы, отталкиваясь от них.
Или, другой пример, я видел предложения расширить error
интерфейс несколькими различными способами, но я не видел ещё ни разу рассказа об опыте больших проектов на Go с попыткой понять обработку ошибок, и уж тем более не видел статей о том, как текущее решение Go затрудняет эту попытку. Эти рассказы и статьи могли бы помочь нам понять детали и важность проблемы, без чего мы не можем даже начать её решать.
Я могу продолжать долго. Каждое потенциально крупное изменение в Go должно быть мотивировано одним или несколькими рассказами о практическом опыте использования, документирущими то, как люди используют Go сегодня и как что-то не работает для них достаточно хорошо. Для очевидных тем, которые мы можем рассматривать для Go, я пока что не вижу этих рассказов, особенно подробных статей, проиллюстрированными реальными примерами использования.
Эти статьи и рассказы будут служить сырым материалом для процесса подачи предложений для Go 2, и мы нуждаемся в вас, чтобы помочь понять нам ваш опыт с Go. Вас около полумиллиона, работающих в разных окружениях, и совсем немного нас. Напишите блог пост в своем блоге или на Medium, или Github Gist (добавив расширение .md для Markdown), или в Google doc, или любым другим удобным вам способом. Написав пост, пожалуйста, добавьте его в эту новую страницу Wiki: https://golang.org/wiki/ExperienceReports
Решения
Теперь, после того, как мы познакомились с тем, как мы будем находить и объяснять проблемы, которые должны быть решены, я хочу кратко отметить, что не все проблемы решаются лучше всего изменением языка, и это нормально.
Одна из проблем, которую мы, возможно, будем решать, это то, что компьютеры часто при базовых арифметических вычислениях выдают дополнительные результаты, но в Go нет прямого доступа к этим результатам. В 2013 Роберт предложил, что мы можем расширить идею двойных выражений (“comma-ok”) на арифметические операции. Например, если x
и y
, скажем, uint32 значения, lo, hi = x * y
вернет не только обычные нижние 32 бита, но и верхние 32 бита умножения. Эта проблема не выглядела особо важной, поэтому мы записали потенциальное решение, но не реализовывали его. Мы ждали.
Совсем недавно, мы разработали для Go 1.9 новый пакет math/bits, в котором находятся различные функции для манипулирования битами:
package bits // import "math/bits"
func LeadingZeros32(x uint32) int
func Len32(x uint32) int
func OnesCount32(x uint32) int
func Reverse32(x uint32) uint32
func ReverseBytes32(x uint32) uint32
func RotateLeft32(x uint32, k int) uint32
func TrailingZeros32(x uint32) int
...
Пакет содержит качественные реализации каждой функции, и компилятор задействует специальные инструкции процессоров, там где это возможно. Основываясь на опыте с math/bits
мы оба, Роберт и я, пришли к выводу, что делать дополнительные результаты арифметических операций в виде изменения языка это не лучший путь, и, взамен, мы должны оформить их в виде функций в пакете вроде math/bits
. Лучшим решением тут будет изменение в библиотеке, а не в языке.
Другая проблема, которую мы могли бы хотеть решить после выхода Go 1 был факт того, что горутины и разделённая (shared) память позволяли слишком легко создать ситуацию гонки (races) в Go программах, приводящих к падениям и прочим проблемам в работе. Решение, основанное на изменении языка, могло бы заключаться в том, чтобы найти какой-то способ гарантировать отсутствие ситуаций гонки, сделать так, чтобы такие программы не компилировались, например. Как это сделать для такого языка, как Go пока что остается открытым вопросом в мире языков программирования. Вместо этого мы добавили базовый инструмент, который очень просто использовать — этот инструмент, детектор гонок (race detector) стал неотъемлемой частью опыта работы с Go. В этом случае наилучшим решением оказалось изменение в runtime и в инструментарии, а не изменение языка.
Конечно, изменения языка также будут иметь место, но не все проблемы лучше всего решаются именно этим.
Выпуск Go 2
В заключение, как же мы будем выпускать Go 2?
Я думаю, наилучший план будет выпускать обратно-совместимые части Go 2 постепенно, шаг за шагом, по ходу обычного плана релизов Go 1. У такого подхода есть несколько важных свойств. Во-первых, это сохраняет привычный график релизов Go 1, позволяя своевременно планировать исправления ошибок и улучшения, от которых зависят пользователи. Во-вторых, это позволяет избежать разделения усилий на Go 1 и Go 2. В-третьих, это спасает от расхождения между Go 1 и Go 2, облегчая в итоге всем жизнь при миграции. В-четвертых, это позволяет нам сконцентрироваться на работе над одним изменением за раз, что позволит сохранять качество. В-пятых, это будет заставлять нас выбирать решения, которые обратно-совместимы.
Нам потребуется время и планирование перед тем, как какие-либо изменения вообще начнут попадать в релизы Go 1, но, вполне вероятно, что мы можем увидеть мелкие изменения уже где-то через год, в Go 1.12 или около того. Это также даст нам время завершить сначала проект с управлением зависимостями.
Когда все обратно-совместимые изменения будут внедрены, допустим в Go 1.20, тогда мы сможем приступить к обратно-несовместимым изменениям в Go 2. Если окажется так, что не будет обратно-несовместимых изменений, то мы просто объявим, что Go 1.20 это и есть Go 2. В любом случае, на том этапе мы плавно перейдем от работы над Go 1.X релизами к Go 2.X, возможно с более продлённым окном поддержки для финальных Go 1.X релизов.
Это всё пока немного спекулятивно, и только что упомянутые номера релизов это всего лишь заглушки для грубой оценки, но я хочу явно донести, что мы не оставляем Go 1 в стороне, и, на самом деле, мы будем способствовать продлению периода разработки Go 1 настолько долго, насколько это максимально возможно.
Вы можете нам помочь
Нам нужна ваша помощь
Обсуждение Go 2 начинается сегодня, и оно будет вестись публично, на открытых площадках вроде почтовой рассылки или трекера проблем. Пожалуйста, помогайте нам на каждом шагу на этом пути.
Сегодня мы больше всего нуждаемся в вашем опыте использования. Пожалуйста, расскажите и напишите о том, как Go работает для вас, и, что более важно, где и как он не работает. Напишите блог пост, покажите реальные примеры, конкретные детали и поделитесь своим опытом. И не забудьте добавить в вики-страничку. Это то, как мы начнём говорить о том, что мы, Go сообщество, можем захотеть изменить в Go.
Спасибо.
Russ Cox, 13 июля 2017
Комментарии (99)
majorius
15.07.2017 14:36+2Здравый подход, особенно если вспомнить что было после появления 3его питона ...
flatscode
15.07.2017 17:43-4А я бы предпочел для определения области видимости с помощью регистра первой буквы, способ с подчеркиванием.
Т.е. что бы идентификатор _<...> — был приватным, а любой другой — публичным. Ну и тогда название функций можно было бы писать со строчной буквы.
Иными словами, вместо
type mytype struct { size string hash uint32 Data []byte }
Хотелось бы видеть:
type _mytype struct { _size string _hash uint32 data []byte }
Кто-то есть с таким же мнением?umputun
15.07.2017 22:01+7это мне кажется просто классическим примером пожелания, нарушающего все идеи и все методы/подходы принятия решений об изменениях в этом языке. То, что такое предложения вообще возникло, скорее всего означает нарушение первого пункта («используй язык, набирай опыт»). Я не могу себе представить как кто-то, кто разрабатывает на Go предложит настолько радикально и калечаще поменять нечто, что совершенно не важно для тех, кто этот код на самом деле пишет. И тут подходим к нарушению второго пункта — а какую проблему это должно решить? Проблемы тут нет никакой, но есть «мне так приятней/привычней, и вообще — хочу как в питоне».
uvelichitel
15.07.2017 22:19Да за австорством Russ Cox кажется(могу шибаться) есть в сети документ — будем CamelCase а не underscore одобренный сообществом.
flatscode
16.07.2017 02:22Я не могу себе представить как кто-то, кто разрабатывает на Go предложит настолько радикально и калечаще поменять нечто, что совершенно не важно для тех, кто этот код на самом деле пишет.
Хочу заметить, что переход стиля от одного к другому может быть полностью автоматизирован (руками ничего переименовывать не надо). Можно даже сделать форк на эту тему.
хочу как в питоне
Про питон особо не знаю, скорее как в JavaScript.
Естественно, вероятность принятия такого решения стремится к нулю. Я скорее хотел узнать, тяготит ли такое соглашение еще кого-то.
JekaMas
16.07.2017 11:43+2Прекрасно сказано!!! Говорю как человек, вынужденный жить с интерфейсами, которые обязательно начинаются с 'I', ресиверами только this и прочим, что нам в рабочем code style напридумывали люди с полугодом опыта в golang, но зато с десятком лет в java, c или perl.
uvelichitel
15.07.2017 20:12+3Складывается впечатление что языку некуда расти. Вроде достигнут предел эффективности runtime и компилятора. Хиндли-Милнер не реализуем, гарантии shared memory access не рассматриваются, о distributed goroutines забыли в существующем дизайне. Но вот же прорыв — мы получим type alias. Бледно как то. При этом язык называется «Пошел», а инженер называется «суслик» — это было забавно лет пять назад.
uvelichitel
15.07.2017 23:27Сразу минусовать. У меня есть production и opensource Go code base. Все же я считаю поскольку компилятор 6c/8с не догонит gcc по эффективности кода на существующем железе и системных вызовах by design, постольку пусть даст легкость/скорость разработки.
JekaMas
16.07.2017 11:35А может не нужны «прорывы»? Радикальные изменения в каждой версии?
Язык развивается не только изменениями самого языка, но и новыми пакетами и инструментами. Вернее, в первую очередь надо развивать язык именно созданием и доработкой сторонних решений и уже потом изменениями в языке.
Пусть лучше будет продуманные нововведения через 2-3 года, нежели постоянно растущий набор новых фич, которые убивают фичи легкого изучения языка и чтания кода.
divan0
16.07.2017 11:55-2- Язык и не планировался "расти".
- Не думаю, что предел достигнут — скорее наоборот, изменения вроде SSA-бекенда направлены на то, чтобы эти пределы расширять.
- Ни разу не слышал, чтобы про алиасы говорилось как о "прорыве".
- Gopher это не суслик, это другое животное, не сильно известное за пределами Северной Америки. Оба из отряда грызунов, но суслик это семейство беличьих, а гоферы — семейство гоферовых. :)
flatscode
16.07.2017 02:45Из опыта/пожеланий могу сказать еще несколько пунктов.
1. Все же отсутствие возможности (необходимости) задекларировать реализуемый интерфейс ухудшает естественную документируемость кода. Это важно при чтении кода.
2. Возможность произвольно назвать receiver в методе (это, например, «v» в func (v Vertex) Abs() float64 ..) по факту приводит к разнобою в именовании этих receiver-переменных в разных местах. По-моему, в этом месте дана ненужная свобода. Лучше бы чего-то зарезервировали или советовали бы использовать одинаковое название. Сейчас имеется разнобой и необходимость задумываться, а как назвать эту переменную — лишняя трата времени.
3. Думаю, было бы здорово иметь возможность писать комментарии в Markdown (как в Rust, например).JekaMas
16.07.2017 11:39-11. Oracle, https://github.com/fzipp/pythia, например
2. Не наблюдал с этим сложностей. Можете подробнее рассказать, когда именно возникают эти трудности? Большие ли файлы у вас? Много ли типов в рамках одного файла?
3. Думаю, это не запрещено. Можно писать в markdown и найти или сделать пакет для работы с такими комментариями.flatscode
16.07.2017 14:11-1По п. 2.
Вы выше написали: «что нам в рабочем code style напридумывали люди с полугодом опыта в golang, но зато с десятком лет в java, c или perl».
В моем понимании, это следствие того, что в java, с++, js этой свободы нет (и не надо) и программисту не нужно задумываться, как назвать receiver. Обычно при написании кода есть, над чем подумать более полезном. А задумываться, как называть receiver — это трата времени и снижение эффективности.
В Go ведь сделали штатный форматер кода? Да, это отлично, вероятность различия формата минимизирована.
Так же есть рекомендации по названию интерфейсов, например. Тоже хорошо.
Но свобода в именовании ресивера по факту — это заусенка в языке, которая тяготит и может стать источником конфликтов в команде и становится, как у вас.JekaMas
16.07.2017 14:25+1Не смог понять, какие сложности все же возникают, при каком объеме кода, сколько кода на один файл и нет ли множества типов с реализацией их методов в рамках одного файла.
Насчет вопроса наименования. Какая в обзем-то разница — надо дать имена n переменным или n+1 при реализации методов типа? Мне лично имена ресиверов помогают — они сохраняют контекст внутри кода. То есть я могу открыть любой файл в любом месте и читать код, без необходимости отматывать код к тому месту, где есть сигнатура метода и названия типа, реализующего логику. This, self и прочее этого не дают.flatscode
16.07.2017 14:36+1То есть я могу открыть любой файл в любом месте и читать код, без необходимости отматывать код к тому месту, где есть сигнатура метода и названия типа
А не отмотав к сигнатуре как вы узнаете название ресивера?JekaMas
16.07.2017 14:45+1А зачем мне знать, что именно вот эта переменная в коде ресивер? Ее поведение ничем не отличается от поведения любых других объектов
flatscode
16.07.2017 15:00У этого объекта, в отличие от других доступы приватные поля, например. По отношению к этому объекту нарушается (может не использоваться или не подходит) инкапсуляция.
JekaMas
16.07.2017 15:44+1Странно… И то и то решается тем, что начинаешь читать код, а после вносить правки, на этом этапе получаешь список доступных полей и методов.
flatscode
16.07.2017 16:16-1Мне лично имена ресиверов помогают — они сохраняют контекст внутри кода. То есть я могу открыть любой файл в любом месте и читать код
Вот тут вы что имеете в виду?
И это касается только вашего кода или вообще любого кода на Go?JekaMas
17.07.2017 07:43+2Кода на golang, в нем есть возможность давать имена ресиверам.
На мой взгляд
func Sum(accumulator *Summer, value int)
Более читаемо внутри тела метода, чем
func Sum(this *Summer, value int)
flatscode
16.07.2017 14:30По п.1.
Ну есть разные варианты, в т.ч. явно в комментариях каким-либо образом описать свои намерения. Но в том же Java, например эта проблема решена на уровне языка.
Или это может быть решено с помощью тестов на интерфейс, в котором так же будут описаны и намерения, и проведена проверка на корректность имплементации интерфеса.
Может быть частично проблему решает и упомянутый Oracle.
Понятно, что авторы Go хотели «как лучше», но это «как лучше» тоже без последствий не обошлось.JekaMas
16.07.2017 14:43Не очень понятно, зачем такая проверка нужна.
Я лично пробовал того же oracle, но очень скоро удалил за ненадобностью.
Вы говорите о «последствиях», можете уточнить, о чем именно речь?flatscode
16.07.2017 14:49+1Типичный случай: есть тип и нужно выяснить, реализует от какой-то интерфейс или нет.
Или, нужно выяснить, имелись ли намерения у автора кода реализовывать какой-то интерфейс или это «случайно получилось».khim
16.07.2017 22:03-2Вообще не вижу тут проблемы. Если класс реализует нужный вам интерфейс правильно — то неважно: хотели ли его реализовать или «так случайно получилось». Если нет — вы можете заставить его реализовать этот интерфейс добавив к нему функций (через alias или «завернув» тип в свою структуру).
Проблемы возникают когда тип реализует-таки интерфейс «случайно» и… неправильно. И вот тут-таки интересно узнать статистику: как часто это происходит? Как сложно это заметить?flatscode
17.07.2017 06:14+2Если класс реализует нужный вам интерфейс правильно — то неважно: хотели ли его реализовать или «так случайно получилось».
На самом деле это важно. Реализация интерфейса предполагает выполнение заявленного контракта по этому интерфейсу.
Если просто удачно совпадают сигнатуры методов, то это абсолютно не означает, что методы будут работать так, как может быть задумано каким-либо интерфейсом.JekaMas
17.07.2017 07:45Вы пытаетесь обойти duck typing. Если оно неудобно, то лучше пользоваться языком без него. С явным указанием реализуемых интерфейсов.
flatscode
17.07.2017 07:58+1Вы пытаетесь обойти duck typing
Причем здесь Duck typing?
Речь идет о Structural type system против Nominal type system.
См., например, Nominative And Structural Typing.
Which is better? There are advantages to both approaches. Structural typing is arguably more flexible — one common complaint in JavaLanguage… On the other hand, it is very common that two objects that are structurally equivalent are semantically different...
Вот о чем я говорю.JekaMas
17.07.2017 08:48Эээ… Duck typing собственно и может относиться к языкам с structural typing, а может и нет.
В go два типа не равны друг другу даже если полностью совпадают всем кроме имени. Но в runtime работает ducktyping, про который собственно и речь. Не будь его, было бы необходимо явно указывать список реализованных интерфейсов. Правда, как здоровый минус, мы бы попутно убили гошную возможность отделения интерфейса от реализации, больше никаких интерфейсов не стороне пользователя.flatscode
17.07.2017 08:58Go использует структурную типизацию по методам для определения совместимости типа и интерфейса.
flatscode
17.07.2017 09:58Но в runtime работает ducktyping
Я, кстати, про runtime изначально ничего не говорил.
В Go главным образом используются проверки на этапе компиляции. Посмотрел, в Wikipedia даже есть пояснение на этот счет:
Duck typing is similar to, but distinct from structural typing. Structural typing is a static typing system that determines type compatibility and equivalence by a type's structure, whereas duck typing is dynamic and determines type compatibility by only that part of a type's structure that is accessed during run time.
The OCaml, Scala, Go, Elm, and Gosu languages support structural typing to varying degrees.
flatscode
17.07.2017 13:49Правда, как здоровый минус, мы бы попутно убили гошную возможность отделения интерфейса от реализации, больше никаких интерфейсов не стороне пользователя.
Кстати, а здесь вы что имеете ввиду?JekaMas
17.07.2017 13:58То и имею. На уровне реализиции работать с конкретной структурой, а пользователю предоставлять возможность определять нужные интерфейсы у себя.
Если у типа надо явно указывать реализуемые интерфейсы, то это становится невозможным.flatscode
17.07.2017 14:29На уровне реализиции работать с конкретной структурой, а пользователю предоставлять возможность определять нужные интерфейсы у себя.
А можете показать, как это выглядит на практике?JekaMas
17.07.2017 15:22flatscode
17.07.2017 17:19То, что интерфейс должен лежать отдельно — это понятно, т.к. у одного интерфейса может быть множество реализаций, и сам по себе интерфейс является общей спецификацией для них (подлежащей выделению в отдельный пакет).
Но размещение интерфейса у пользователя (потребителя) — это какое-то извращение, когда реализация не в курсе (явно или неявно, не важно) спецификации, которую она реализует.
В этом случае смысл интерфейса стремится к нулю.Edison
17.07.2017 18:18Но размещение интерфейса у пользователя позволяет уменьшить зависимости пакета, ничего не нужно импортить. Тоже самое про другую сторону — когда реализация не знает про интерфейс.
Смысл интерфейса стремится к нулю, когда у вас каждый про каждого знает.
flatscode
17.07.2017 18:44Но размещение интерфейса у пользователя позволяет уменьшить зависимости пакета, ничего не нужно импортить.
Размещение интерфейса у пользователя лишено смысла.
Во первых, потому что никто, кроме пользователя, им воспользоваться не может.
Во вторых, потому что нарушается отношение общее-частное и общее (интерфейс) начинает зависеть от частного (реализация), что полная ерунда.Edison
17.07.2017 19:10Интерфейс это не общее, интерфейс описывает реализацию.
Каким образом интерфейс будет зависеть от реализации в случае когда интерфейс объявлен в месте, где он используется?flatscode
17.07.2017 20:00Интерфейс это не общее, интерфейс описывает реализацию.
Интерфейс — это спецификация, контракт. Он определяет требования в максимально приемлемом общем виде.
Реализация интерфейса — это написание кода, который удовлетворяет его спецификации.
Каким образом интерфейс будет зависеть от реализации в случае когда интерфейс объявлен в месте, где он используется?
Хотя бы просто потому, что он у вас будет написан после реализации.Edison
17.07.2017 20:18С чего это он будет написан после реализации?
package service import "encoding/json" type Publisher interface { Publish([]byte) error } type Service struct { pub Publisher } func NewService(p Publisher) *Service { return &Service{ pub: p, } } func (s *Service) Send(msg Message) error { data, err := json.Marshal(msg) if err != nil [ return err } return s.pub.Publish(data) }
Где тут зависимость от реализации
Publisher
?flatscode
17.07.2017 20:45Где тут зависимость от реализации Publisher
Если здесь не описана спецификация Publish(), значит вы надеетесь на какую-то реализацию.
Отсутствие «технической» зависимости не означает, что логическая связь отсутствует. Как узнать, подходит какая-то структура семантически для использования в Send() или нет?flatscode
17.07.2017 20:56Если здесь не описана спецификация Publish(), значит вы надеетесь на какую-то реализацию.
Сразу напомню, как выглядит спецификация. Посмотрите, какой объем описания дан всего для одного метода.
// Reader is the interface that wraps the basic Read method. // // Read reads up to len(p) bytes into p. It returns the number of bytes // read (0 <= n <= len(p)) and any error encountered. Even if Read // returns n < len(p), it may use all of p as scratch space during the call. // If some data is available but not len(p) bytes, Read conventionally // returns what is available instead of waiting for more. // // When Read encounters an error or end-of-file condition after // successfully reading n > 0 bytes, it returns the number of // bytes read. It may return the (non-nil) error from the same call // or return the error (and n == 0) from a subsequent call. // An instance of this general case is that a Reader returning // a non-zero number of bytes at the end of the input stream may // return either err == EOF or err == nil. The next Read should // return 0, EOF. // // Callers should always process the n > 0 bytes returned before // considering the error err. Doing so correctly handles I/O errors // that happen after reading some bytes and also both of the // allowed EOF behaviors. // // Implementations of Read are discouraged from returning a // zero byte count with a nil error, except when len(p) == 0. // Callers should treat a return of 0 and nil as indicating that // nothing happened; in particular it does not indicate EOF. // // Implementations must not retain p. type Reader interface { Read(p []byte) (n int, err error) }
Edison
17.07.2017 23:02вы так и не ответили на вопрос.
С чего это интерфейс будет написан после реализации?
Как узнать, подходит какая-то структура семантически для использования в Send() или нет?
Где тут зависимость от реализации Publisher?
flatscode
18.07.2017 09:02Где тут зависимость от реализации Publisher?
Повторяю, если у вас здесь не описан контракт, то он описан где-то в другом месте. А это означает, что интерфейс зависит от этого места.
В случае Go это неявная (логическая, семантическая) зависимость. Из-за более свободной системы типов приходится больше внимания уделять логическим/семантическая ограничениям и связям.Edison
18.07.2017 09:52То есть весь упрек в том, что контракт (документация?). Так когда реальный код я буду писать, я напишу доку и интерфейсу, и структуре Сервис и методу Сенд.
Я очень часто писал либы, сервисы, где у меня был интерфейс
Logger
. И никакой зависимости от реализации не было.
Логическая зависимость — это демагогия. Я создаю интерфейс, я выдвигаю требования, тот кто использует мою библиотеку должен выполнить мое требование.
Так вот, с чего это интерфейс будет написан после реализации? Вы так и не ответили на вопрос, с чего этот спор начался. Конечно он может быть написан после, это совершенно не обязательно. Что я вам и показал выше на примере Логгера.
flatscode
18.07.2017 10:43+2Я создаю интерфейс, я выдвигаю требования, тот кто использует мою библиотеку должен выполнить мое требование.
Вот, отлично. Если реализация должна учитывать спецификации, то она зависит от интерфейса (явно или неявно).
Теперь возвращаемся к началу и видим:
… Правда, как здоровый минус, мы бы попутно убили гошную возможность отделения интерфейса от реализации, больше никаких интерфейсов не стороне пользователя.
Ну и каким образом явная зависимость убивает отделение интерфейса от реализации, если «кто использует мою библиотеку должен выполнить мое требование»?
Чем мешает явная зависимость размещению интерфейса у пользователя?Edison
18.07.2017 10:55+1вы сейчас смешали все в кучу. Есть пользователь реализации (пакет, структура) а есть пользователь моей библиотеки (как бы уже реальный пользователь).
Так вот я имел ввиду, что пакет, который нуждается в каком-то функционале (хочет использовать сторонний сервис, как тот же логгер), что бы не вызывать прямой зависимости от других пакетов, объявляет у себя интерфейс, убирая прямую зависимость от других пакетов.
Если вы скажете про логическую зависимость, покажите пример в джаве (или другом языке с implements интерфейсов), где такой зависимости не будет.
Чем мешает явная зависимость размещению интерфейса у пользователя?
У какого пользователя? Приведите пример кода, а то уж слишком много путаницы.
flatscode
18.07.2017 11:19Так вот я имел ввиду, что пакет, который нуждается в каком-то функционале (хочет использовать сторонний сервис, как тот же логгер), что бы не вызывать прямой зависимости от других пакетов, объявляет у себя интерфейс, убирая прямую зависимость от других пакетов.
По «классике», зависимости уменьшаются путем выделения общих сущностей (например, интерфейсов) в отдельную библиотеку. Тогда реализация зависит только от этой «библиотеки спецификаций», а не напрямую от потребителя.
То, что вам понравилось в Go, полагаю, является возможностью использования тривиального адаптера, см. Design Patterns in Golang: Adapter в конце:
If the Target and Adaptee has similarities then the adapter has just to delegate the requests from the Target to the Adaptee.Edison
18.07.2017 11:39По «классике», зависимости уменьшаются путем выделения общих сущностей (например, интерфейсов) в отдельную библиотеку.
Никто не мешает вам так сделать, если вы так привыкли. Пример с
io.Reader
сами привели. Иногда так даже лучше делать. Я стараюсь такого избегать.flatscode
18.07.2017 12:59-1Никто не мешает вам так сделать, если вы так привыкли. Пример с io.Reader сами привели. Иногда так даже лучше делать. Я стараюсь такого избегать.
Так это правильно и просто, это следствие хорошего системного анализа. Это не зависит от используемого языка.
Более того, если у вас в интерфейсе присутствует хотя бы с один метод с параметром или возвратом проблемно-ориентированного интерфейса/структуры, то иного пути достичь желаемого (уменьшения зависимости) нет — нужно выделять общие сущности отдельно.
Или вы и свои типы тоже стараетесь не использовать?Edison
18.07.2017 13:14Более того, если у вас в интерфейсе присутствует хотя бы с один метод с параметром или возвратом проблемно-ориентированного интерфейса/структуры, то иного пути достичь желаемого (уменьшения зависимости) нет — нужно выделять общие сущности отдельно.
Или вы и свои типы тоже стараетесь не использовать?Ну как это, использую. Опять же, в данном случае реализация интерфейса (интерфейс в пакете
foo
) будет зависеть от пакета foo (от интерфейса), но не наоборот (что вы пытаетесь тут доказать), хоть и пакет foo ее будет использовать.flatscode
18.07.2017 14:00Опять же, в данном случае реализация интерфейса (интерфейс в пакете foo) будет зависеть от пакета foo (от интерфейса)
Ну так как вы от этого избавитесь, кроме как выносом общих типов в отдельный пакет?
но не наоборот
Вот если вы сделаете реализацию, которая не в курсе о потребителях (как в примере https://github.com/golang/go/wiki/CodeReviewComments#interfaces), и у которого Thing() будет чуть сложнее (с использованием типа в producer), то при попытке вынести интерфейс в consumer у вас будет прямая явная зависимость consumer от producer.Edison
18.07.2017 15:42-1Ну так как вы от этого избавитесь, кроме как выносом общих типов в отдельный пакет?
Определением интерфейса в пакете, в котором он используется.
с использованием типа в producer
Вы только что это сами придумали и сами придумали эту зависимость.
Я бы определил этот сложный тип в консюмере и как уже сказал — при этом пакет с реализацией зависел бы от пакета с интерфейсом.
Еще раз, вы мне пытаетесь доказать про логическую зависимость при определении интерфейса в пакете, где он используется — можете привести код с интерфейсами где есть логическая зависимость и где ее нету?
flatscode
18.07.2017 17:01Определением интерфейса в пакете, в котором он используется.
Ну разместили, получили зависимость consumer от producer.
Я бы определил этот сложный тип в консюмере и как уже сказал — при этом пакет с реализацией зависел бы от пакета с интерфейсом.
Ну перенесли туда Data. И что видим? Зачем-то у нас реализация зависит от Consumer. А если у нас есть Consumer2, то он зачем-то зависит от Consumer.
А теперь, как правильно.
Edison
18.07.2017 17:08Вы берет частный случай, добавляете туда кучу всего и теперь говорите, что было неправильно. Но и как я уже писал, иногда лучше выносить в отдельный пакет.
Что если еще одного консюмера не будет, а будут только продюсеры? Что если не будет этой сложной структуры данных? Что если определить
queue
пакет и там иметь интерфейсы консюмеру и продюсеру и общую структуру данных?flatscode
18.07.2017 17:13Вы берет частный случай
Блин, да это не частный, это как раз общий случай!
Это у вас был простой частный случай.
Что если еще одного консюмера не будет, а будут только продюсеры?
Продюсеры ничего не должны знать о консьмерах, поэтому их количество не важно. От слова совсем.
Что если не будет этой сложной структуры данных?
Это общий случай, с данными.
Что если определить queue пакет и там иметь интерфейсы консюмеру и продюсеру и общую структуру данных?
Да пожалуйста.Edison
18.07.2017 17:28+1частный, потому что вы вцепились в название consumer, переименуйте его в queue, определите там интерфейс и используйте точно так же.
JekaMas
17.07.2017 19:18Смысл интерфейса в отделении данных и поведения. И вполне есть смысл в том, чтобы пользователь определял нужный ему интерфейс для работы(он в общем-то, можем быть сильно уже, чем набор методов, предоставляемый в стороннем пакете).
Если поставлять код в виде структур, а не интерфейсов, то пользоваться ими может любой пользователь, вне зависимости от того, какой набор поставляемых методов нужен именно ему.
Поищите, тут на хабре был перевод хорошей статьи именно про этот ключевой момент.
Еще лучше после попробовать на практике, попробовать все же код, идеоматичный для go. вполне может оказаться, что до этого была какая-нибудь java-go.flatscode
17.07.2017 20:21Смысл интерфейса в отделении данных и поведения.
Если не сложно, приведите ссылку на определение интерфейса, которое используете. Откуда оно?
khim
17.07.2017 19:32В этом случае смысл интерфейса стремится к нулю.
С какого перепугу? В случае размещения интерфейса у клиента вы можете сделать есть таким узким, каким только возможно — а в будущем его можно расширить без нарушения совместимости. Грубо говоря если у вас сегодня, сейчас, компонент не умеет модицифировать конфигурацию программы, то ему в интерфейсе методwrite
не нужен, а если завтра этот компонент научится конфигурацию править, то в его интерфейсеwrite
появится и это сразу станет заметно.
Как раз интерфейс, указываемый в классе — это неправильно, так как он описывает зараниее конечное и фиксированное количество сценариев для которых этот класс может быть использован.flatscode
17.07.2017 20:15С какого перепугу? В случае размещения интерфейса у клиента вы можете сделать есть таким узким, каким только возможно
Узким относительно чего? Релизации? Ну так это переворот с ног на голову отношение интерфейс-реализация (общее-частное).
Как раз интерфейс, указываемый в классе — это неправильно, так как он описывает зараниее конечное и фиксированное количество сценариев для которых этот класс может быть использован.
Интерфейс не конечное и фиксированное количество сценариев задает, а стандартизирует поведение согласно разработанной спецификации.
khim
17.07.2017 20:41+1Узким относительно чего?
Что значит «относительно чего»? Узкий — это интерфейс, в котором мало методов, широкий — в котором много…
Интерфейс не конечное и фиксированное количество сценариев задает, а стандартизирует поведение согласно разработанной спецификации.
Совершенно верно. Вот только кто у нас задаёт спецификацию? В Go и в современном C++, внезапно — потребитель.
Тот факт, что в язык где классические интерфейсы реализуются «как нефиг делать» люди упорно, годами, добавляют интерфейсы «в стиле Go» должен бы заставить вас остановиться и подумать… но, похоже, вы этому просто в принципе не обучены.flatscode
17.07.2017 20:51Совершенно верно. Вот только кто у нас задаёт спецификацию? В Go и в современном C++, внезапно — потребитель.
Это невозможно, т.к. потребитель не может изменить реализацию. Поэтому, он может потребить только то, что есть.
Как реализация может узнать, что нужно потребителю? Реализация ни про потребителей, ни про их количество абсолютно не в курсе.khim
17.07.2017 21:04Это невозможно, т.к. потребитель не может изменить реализацию.
Ещё как может! Вы никогда с заказчиками не общались, которые просили «вот тут кнопочку добавить»? Вот это — типиченый вариант, когда потребитель меняет реализацию.
Реализация ни про потребителей, ни про их количество абсолютно не в курсе.
Реализация — может быть и нет, а программист, пишущий реализацию — таки да. И в его интересах расширить класс так, чтобы им смогли пользоваться как можно большее число потребителей, так что если кому-то какого-то метода не хватить — он отреагирует на запись в багтрекере и его добавит. А то что не все потребители будут все возможности реализации использовать — так это нормально. В конце-концов когда транзисторы используются как резистры или конденсаоторы в микросхемах — тоже не все их возможности используются…flatscode
17.07.2017 21:20Ещё как может! Вы никогда с заказчиками не общались,
Это здесь причем? Нужна доработка — сделаем, согласно букве O в принципе SOLID.
Реализация — может быть и нет, а программист, пишущий реализацию — таки да.
Ну так отсутствие «технической» связи (например, явного упоминания интерфейса в реализации) не означает, что нет логической связи.khim
17.07.2017 22:48Ну так отсутствие «технической» связи (например, явного упоминания интерфейса в реализации) не означает, что нет логической связи.
А это уже другая история. Речь идёт про технические аспекты.
В Go (и, как я уже говорил выше, в современном C++) интерфейс описывает потребитель — и при наличии 100 потребителей реализация может одновременно и 100 интерфейсов реализовывать.
В этом нет ничего странного и, более того, на практике это часто удобнее, чем требовать, чтобы реализация жёстко перечисляла все интерфейсы, которые она поддерживает…flatscode
18.07.2017 07:29А это уже другая история. Речь идёт про технические аспекты.
Вы вспомните теорию, для чего вообще была введена система типов.
Основная роль системы типов заключается в уменьшении ошибок в программах посредством определения интерфейсов между различными частями программы и последующей проверки согласованности взаимодействия этих частей.
Чем строже у вас система типов, тем меньше необходимо накладывать семантические ограничения на код.
И обратно, чем свободнее система типов, тем больше необходимо накладывать семантические ограничения на код.
divan0
18.07.2017 00:13flatscode непонятно, с чем вы спорите. В Go интерфейсы используются несколько иначе, чем в Джаве например. В Go интерфейс как-правило создается только при надобности и после того, как как написаны "реализации". Тоесть, сначала был
*os.File
и*http.Request
, а потом был io.Reader. И да, именно поэтому интерфейс часто создается пользователем. Пример: вы пишете код для работы с git — создаете конкретный тип с методамиGit
, потом добавляетеHg
, потом, по мере усложнения проекта, выноса кода в библиотеку, например, абстрагируете их поведение в интерфейсVCS
, и это все делано вами, пользователем. Я как-то пытался свести эти мысли воедино вот в этой статье: https://habrahabr.ru/post/276981/ Не уверен, что получилось, но вдруг.flatscode
18.07.2017 05:30flatscode непонятно, с чем вы спорите. В Go интерфейсы используются несколько иначе, чем в Джаве например. В Go интерфейс как-правило создается только при надобности и после того, как как написаны «реализации». Тоесть, сначала был *os.File и *http.Request, а потом был io.Reader
Я не спорю, а как раз описывают эту ситуацию: интерфейс после реализации. Из этого следует, что интерфейс зависит от реализации, что является переворотом с ног на голову отношение общее-частное.
Выше я привел пример io.Reader — это классический интерфейс со спецификацией и благодаря ей есть возможность:
1. Создавать реализации, удовлетворяющие спецификации.
2. Проверять, удовлетворяет ли какая-либо реализация спецификации, т.к. подходит ли она для использования интерфейса или нет (потому что сигнатурное соответствие является необходимым, но недостаточным требованием, должно быть еще семантическое соответствие).
Реализация может или явно (как в Java, например с помощью implements), или неявно (в комментариях упомянуты совместимые интерфейсы или просто описание) ссылаться на спецификацию.
Неявная ссылка на интерфейс (спецификацию) — это не отсутствие логической связи между реализацией и интерфейсом (спецификации). Логические/семантические ограничения никуда не деваются!
Если в JavaScript в сигнатуре функции не указаны типы по технической причине (язык этого не предусматривает), это ведь не означает, что в качестве параметров туда можно передавать абсолютно что угодно!
divan0
16.07.2017 11:47-2Разнобой в именовании ресивера ловится
go lint
-ом.
Думаю, эти пункты не проходят ни шаг 1 (использовать Go, накопить опыт), ни шаг 2 (описать и объяснить проблему)..
flatscode
16.07.2017 14:17Это здорово, но выглядит, как «мы создадим проблему, а потом будем с ней героически бороться».
divan0
16.07.2017 14:35-2Из моего опыта, именование ресивера никогда не вызывает конфликтов или споров, а разнобой обычно появляется только в результате рефакторинга (переименовали тип, к примеру, и в методе, над которым работали, изменили ресивер). Но многие ставят даже
go lint
иgo vet
на сохранение файла, у других это git post hooks, ну или при код-ревью, кому-то в глаза бросится — исправят. Лично я не видел ещё споров или конфликтов на эту тему.
Если это и можно классифицировать как проблему, то решается она слишком легко и просто.
flatscode
16.07.2017 14:42Из моего опыта, именование ресивера никогда не вызывает конфликтов или споров
Ну выше JekaMas упомянул конфликт/спор.
Не скажу, что от свободного именования ресивера только минусы. Естественно, есть и плюсы.
Но по моему мнению и опыту «кодирования в соответствии с рекомендациями от golang» я сделал вывод, что минусов больше, чем плюсов.JekaMas
16.07.2017 14:44+1Возможно, или недостаточно разобрались, или инструмент для вас неподходящий.
flatscode
16.07.2017 14:55+1Я не говорю, что это проблеме нерешаемая. Я хочу сказать, что этой проблемы в принципе могло не быть.
Сделали, ведь, gofmt — решили проблему на канонический формат кода. Могли бы и не делать gofmt, тоже решали бы эту проблему (правда, каждый по-своему).
Edison
16.07.2017 23:32- Тут идея интерфейсов другая, не "я хочу быть вот таким то", а "мне нужно вот такое то"
- Есть принятые стандарты — ресивер как первая буква типа. Например
(p *Producer)
,(c *Client)
, etc. - https://github.com/golang/go/issues/2016
flatscode
17.07.2017 05:59По п. 2.
На примере стандартной библиотеки можно увидеть, что не всегда это соблюдается.
Где-то пишут так:
func (s SpReg) String() string { return fmt.Sprintf("SpReg(%d)", int(s)) }
а где-то так:
func (r PCRel) String() string { return fmt.Sprintf("PC%+#x", int32(r)) }
Но очевидно, что такое правило приводит еще к паре неприятных случаев.
Первый — это использование «i» в качестве ресивера, переменной, которая обычно применяется в циклах.
func (i Imm) String() string { return fmt.Sprintf("%d", int32(i)) }
Второе — это использование «l» в качестве идентификатора. За такое нужно бить сильно и больно по пальцам (за сходство с цифрой 1):
func (l Label) String() string { return fmt.Sprintf("%#x", uint32(l)) }
Кстати, в Питоне похожая ситуация с возможностью именования. Однако, так на этот счет есть рекомендация:
...Often, the first argument of a method is called self. This is nothing more than a convention: the name self has absolutely no special meaning to Python. Note, however, that by not following the convention your code may be less readable to other Python programmers, and it is also conceivable that a class browser program might be written that relies upon such a convention...
См. The Python Tutorial: 9.4. Random RemarksJekaMas
17.07.2017 07:47Стандартная библиотека — это плохой пример. Ее писали тогда, когда гошного опыта ни у кого не было.
Код там, очень часто, не идеоматичный для golang.
Edison
17.07.2017 10:25Я же сказал принято, не обязательно. Если ресиверы будут именоваться по-разному — то golint ворнинг выдаст.
Иногда и я пишу не первую букву, например вот в таких случая(p AsyncProducer)
, хотя опять же — первая важной части названия.flatscode
17.07.2017 10:29А если вы делаете рефакторинг, например, «AsyncProducer» переименовываете в «AsyncFactory», имена ресиверов тоже автоматически изменяются или это надо делать вручную?
Edison
17.07.2017 11:14вручную надо это делать, gorename'ом переименовывать каждый ресивер.
flatscode
17.07.2017 12:39gorename'ом переименовывать каждый ресивер
А если новое имя конфликтует с переменными в функции, то еще и их нужно переименовать.Edison
17.07.2017 12:45можешь совсем ничего не делать.
Но ты не сможешь переименовать, если код после этого не будет компилироваться.flatscode
17.07.2017 12:58На фоне этих проблем на ровном месте случай с «Говорю как человек, вынужденный жить с… ресиверами только this ...» выглядит не так уж и плохо.
Edison
17.07.2017 15:35ну это все субъективно — я называю одной, иногда двумя буквами и мне ок.
На счет рефакторинга — какой другой язык из коробки имеет такойбольшой набор тулзов? Я вот прям и не могу вспомнить.rraderio
17.07.2017 16:40Java + IDEA
Edison
17.07.2017 16:54+1только вот IDEA это не тулза из коробки которая идет с Java. Это IDE которая разрабатывается отдельной компанией, а не комьюнити.
В Go же gorename, guru, govet, golint, gofmt, etc и все это разрабатывается Go сообществом.
rraderio
17.07.2017 17:44Ну да, потому что для Go нет такой мощной IDE как для Java. Осталось goide
написать.Edison
17.07.2017 18:02во-первых есть gogland (даже ссылку вам уже дали). Во вторых, я говорю про сам язык и комьюнити.
В стандартной библиотеке естьgo/ast
,go/parser
,go/printer
, etc которые позволяют очень просто создавать свои тулзы для работы с Go кодом (то ли генерация, то ли рефакторинг), которые потом очень просто интегрировать с vim/emacs/sublime/atom.
Потому опять же скажу — не могу вспомнить я других языков с таким большим набором тулзов для работы с кодом которые поставляются с языком, как есть у Go.
JekaMas
17.07.2017 19:25Плохо, поверьте.
Очень чувствуется контраст между чтением корпоративного кода и хороших open source проектов. Вторые значительно легче для восприятия, если есть внятные имена ресиверов, отсутствуют интерфейсы ради интерфейсов, код соответствует golang review guide.
JekaMas
Изящное решение со сбором пользовательского опыта! Все же мне глубоко симпотично стремление подумать хорошо и понять, какую задачу решаем, для чего и только после вносить изменения в код!
Ну и всех поздравляю с довольно формальным событием, но все же важным: golang в top 10 tiobe!