Thank Go!
367 subscribers
1 photo
24 links
Неожиданный взгляд на язык программирования Golang от двух продактов. Конструктив от @mikeberezin с нотками сарказма от @nalgeon
Download Telegram
to view and join the conversation
О, если бы понятность кода достигалась единообразным форматированием 🙂
📜 Главный инструмент

Самое время для пятничного поста.
Результаты последнего опроса Golang-разработчиков навели меня на идею небольшого сравнения.

Для меня было удивительно то, что в 2019 году 14% разработчиков на Го используют Vim в качестве предпочитаемого редактора.

Ок, а как там у других?

* Rust — 23,6%, ну вроде объяснимо
* Golang — 14%
* JS — 13%, кто эти люди?
* Python — 9%
* Scala — <7,4%
* и наконец C++ — 7%

Раз такое дело, вот прекрасная статья про Vim.
Цитата:

Сколько понадобится времени

Если следовать советам выше и постоянно правильно практиковать вим, то время от «любое действие занимает минуту» до «могу неспешно работать периодически подсматривая в документацию» займет около месяца. Дальше процесс пойдет легче, но останавливаться в развитии на этом этапе нельзя. Оттачивание всех необходимых навыков может занять и год. Но не стоит пугаться. В любом случае через месяц-два вы сможете вполне сносно работать.
💣 Письма в редакцию

Читатель Максим интересуется: как же разработчики Го могут жить без стримов? Stream — понятие из java-мира. Речь идёт о map/filter/reduce подходах из функционального программирования.

Поговаривают, что именно Sequences (аналог джава стримов) помогли Котлину захватить умы андроид-разработчиков, застрявших на 7й джаве в то время.

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

Согласно опросу разработчиков Го 2019 года — фичи функциональных языков (довольно обобщенно) находятся на третьем месте по желаниям разработчиков, сразу после наиболее обсуждаемых дженериков и обработки ошибок.

А что думаете вы? Пользуетесь чем-то похожим в ежедневной практике?
Документация!

С чем у Go действительно всё классно — так это с документацией.

1️⃣ A Tour of Go — интерактивное введение в язык.
2️⃣ Effective Go — книга о том, как писать идиоматичный код на Go.
3️⃣ Ссылки из документации на исходники каждой функции стандартной библиотеки (например, Max).

Для сравнения я взял топ-10 языков из StackOverflow Developer Survey и посмотрел, как у них дела с этими тремя пунктами:

1. JavaScript. У языка даже своего сайта нет ツ Есть миллион обучалок, но единого авторитетного источника — нет. Документация по языку есть только благодаря Мозилле. Ссылок на исходники нет, конечно. Зато по каждой функции есть интерактивный пример, это отлично.

2. SQL. Всё как у JS, только ещё хуже — в мире SQL не нашлось своей Мозиллы, которая бы сделала документацию с примерами. Где свой SQL получали, там и ищите.

3. Python. Первый язык в топе с собственным сайтом. Есть ссылка Get Started, но дальше разбегаются глаза — что выбрать? Вместо рекомендованной авторами базовой обучалки вываливают на новичка простыню ссылок. Официальный туториал найти можно только каким-то нетривиальным способом. Интерактива нет. Незачёт.

Исходников отдельных функций нет, исходники модулей — только если они написаны на Python (например, для heapq исходники есть, а для itertools — нет)

4. Java. Язык принадлежит компании Оракл, этим всё сказано. Заходите на официальный сайт посмеяться.

5. Bash. Не ожидал, но у него есть сайт и документация. Спасибо и на том.

6. С#. Первый язык родом из нового тысячелетия в списке. Есть интерактивный туториал, тур по языку и руководство разработчика. Неплохо! В документации есть интерактивные примеры, а вот ссылок на исходники нет.

7. PHP. Первое, что встречает на официальном сайте — «Please DO NOT use this version in production, it is an early test version». Спасибо, да. Я бы вообще сделал DO NOT use this in production официальным слоганом языка. Кхм, пардон, отвлёкся. Есть неинтерактивная обучалка, неплохая документация со статическими примерами и комментариями сообщества, и, неожиданно — аж целая книга PHP at the Core: A Hacker's Guide.

8. TypeScript. Новейший язык, 2012 года выпуска. Несколько введений, местами интерактивные. Подробный учебник, который начинается как интерактивный, но быстро превращается в статический (wtf?). Ссылок на исходники нет, но они особо и не нужны — у тайп-скрипта нет собственной стандартной библиотеки.

9. C++. Есть интерактивный тур по языку! Шучу, конечно. Тур выложен в PDF, что тут скажешь. Зато есть Core Guidelines о том, как писать правильный код.

10. C. Сайта нет, ничего нет, населена роботами.

Счёт 10-0 в пользу Go.
🌈 Дженерики в Го — история одной фичи

Три главные фичи неустанно обсуждаются в го-комьюнити с начала времён:
* нормальное управление зависимостями,
* дженерики,
* обработка ошибок.

Начиная с Go 1.11, первый вопрос был решён уже в бета-версии, а в релиз Go 1.13 вошла окончательная версия Go Modules — решение всех проблем, связанных с управлением зависимостями и публикацией модулей.

Но вот остальные два вопроса продолжают быть «самыми желанными фичами». Дженерики — лидер с большим отрывом, начиная с 2016 года. В опросе 2019 года за них голосует 79% пользователей! Это невероятный результат для честного голосования (хотя, кто его знает).

Так в чём же собственно проблема? Почему просто нельзя взять и сделать?
Чтобы лучше разобраться в ситуации, пробежимся по основным вехам:

Еще в 2009 году Russ Cox написал от дилемме дженериков:
do you want slow programmers, slow compilers and bloated binaries, or slow execution times

В 2011 году появляется первый пропосал на гитхабе от Ian Taylor — тут следует добавить, что он до сих пор продолжает участвовать в дизайне дженериков (почти 10 лет!).

В 2015 Russ Cox уже не так консервативен и говорит, что просто нужна ещё одна итерация — это чисто техническая проблема.

И вот в 2019 году, спустя почти 10 лет раздумий, появляется огромный черновик дизайна дженериков, который шокирует сообщество. Это невероятное усложнение почти на пустом месте. Твиттер наполняется шутками про то, что контракты — это новые интерфейсы.

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

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

Эти вопросы мы и разберём в следующих постах.
🌈 Дженерики в Го — как показать сообществу сложную фичу

Как видно из предыдущего поста, тональность обсуждения темы дженериков в Го со временем изменялась. Мы знаем из статьи Роба Пайка, что язык задумывался, как инженерное решение для больших проектов. Он должен быть простым и быстрым со строго поступательным развитием — у не должно быть резких и обратно несовместимых изменений в дизайне. Такие тезисы значительно усложняют добавление новых сложных фич.

Если посмотреть на опросы пользователей 2016, 2017 и 2018 года, то будет виден явный рост доли людей, которые не пользуются языком из-за недостатка нужных фич: 11%, 19%, 22%, соответственно. В топ-3 этих фич входили и дженерики. Их не хватает пользователям для построение более сложных абстракций.

И тут получается довольно сложное противопоставление. С одной стороны, у языка уже есть активное комьюнити, в котором ценятся основные его плюсы: простота и скорость. Для них любые усложнения — боль и страдания, а без более сложных абстракций можно и обойтись. Но в какой-то момент рост этих пользователей замедляется и становится видна часть новых ребят, которым не хватает важных фич для использования продукта — для них это блок-фактор. И получаем мы продуктовую задачу: как растить продукт так, чтобы всем было хорошо. Единственный правильный ответ — никак. Всё равно этот путь пойдет через компромиссы. Но его можно и нужно делать более мягким.

Что мы видим дальше?

В 2019 году вместе с драфтом-дизайна дженериков выходит еще и пост (а также и видео на GopherCon от Ian Taylor) с подробным разбором того, зачем же все-таки нужны дженерики, в чём их польза для языка. Это информационная поддержка продуктового развития, начинаем доносить будущую пользу пользователям.

Далее идёт бурное и открытое обсуждение первого дизайна в самой активной части комьюнити. Обсуждение значительно влияет на вторую версию дизайна, тем самым завлекая сообщество в решение этой проблемы.

Потом мы видим результаты опроса разработчиков 2019 года, в котором уже 79% разработчиков не могут жить без дженериков. Неплохой рост с 22%, правда? Теперь это уже явное желание комьюнити!

Вместе со вторым дизайном в 2020 году выходит в свет версия языка и плейграунд (!) с его поддержкой. Этот шаг вовлекает в обсуждение новых пользователей, которые раньше не находили сил погрузиться в сложное описание. А теперь то уже можно всё потрогать! Появляется большое количество статей с примерами на плейграунде от разных ребят, распространяя фичу еще шире.

Дженерики появятся в Го не раньше, чем через год. Но к тому времени информационная поддержка сработает уже так хорошо, что большинство пользователей будет знакомо с фичей и никакого негатива это не вызовет.
🦄 Фича х10

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

Есть ли что-то подобное в языке программирования Го как продукте?

Не буду тянуть, ответ есть и он очень простой. Как выглядит вызов синхронной функции, печатающей "Hello, world!":

func() {
fmt.Println("Hello, world!")
}()


А вызов такой же функции, которая не будет блокировать основной сценарий исполнения? Знакомьтесь, примадонна Горутина:

go func(){
fmt.Println("Hello, world!")
}()

Окей, а как две неблокирующие функциии могут взаимодействовать с общими данными? Будет же блокировка! Нет, если есть примауомо Канал:

shareData := make(chan data)

go func(in chan<- data){
doSomeWriteTo(in)
}(shareData)


go func(in <-chan data){
doSomeReadFrom(in)
}(shareData)


Это и есть фича х10. Давайте теперь разбираться что тут к чему.
👩‍💻 Примадонна Горутина

Что же такого примечательного в запуске неблокирующей функции в Го?

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

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

Знаете, в продукте должен быть шарм. Андрей Бреслав — дизайнер языка Котлин — как-то сказал, что функция в Котлине начинается с ключевого слова fun, потому что это fun! И в этом что-то есть.

Но вот fun'а от вызова функции в Котлине я ни разу не испытал (признаюсь, не часто и пытался), но я кайфую от вызова горутины. Вы только посмотрите:

go func(){
someWork()
}()


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

Нарассказывал я вам про фичу х10, но не всё так просто. Нельзя просто так взять и сделать понятную асинхронность. Да, в Го всё выглядит неплохо, по сравнению с другими языками (привет, @ohmypy), но всегда есть но. Дальше давайте разбираться уже на примерах, без них тут ничего не объяснить.

fmt.Println("Hello, Gopher!")
go func() {
time.Sleep(time.Second)
fmt.Println("I'm inside the Goroutine!")
}()
fmt.Println("Buy, Gopher!")
// >> Hello, Gopher!
// >> Buy, Gopher!


Воу! А куда делась то наша так просто запущенная горутина? А она просто не успела выполниться до завершения основной функции. И да, main функция тоже работает внутри горутины.

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

В нашем случае поможет WaitGroup из стандартного пакета sync — будем ждать, пока горутина все же отработает.

fmt.Println("Hello, Gopher!")
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
time.Sleep(time.Second)
fmt.Println("I'm inside the Goroutine!")
}()
wg.Wait()
fmt.Println("Buy, Gopher!")
// >> Hello, Gopher!
// >> I'm inside Goroutine!
// >> Buy, Gopher!


Вроде справились, но и это не всё. У Го здесь есть свой путь, к которому мы и переходим дальше — каналы.

Если вам интересно подробнее поразбираться с асинхронностью в Го, то в A Tour of Go есть прекраснейший раздел.
Проблемы первого мира! В питоне, если хочешь сделать функцию доступной для синхронного и асинхронного вызова — приходится писать две функции 😐
Go на практике

У этого канала два автора — Миша и Антон. Миша отлично знает Go и активно применяет его на работе, а Антон (это я) ограничивался почитыванием статей и глубокомысленными высказываниями вроде «что за язык, даже дженериков нет», «да вы посмотрите, они символы 'рунами' называют» и «неужели нельзя было сделать как в питоне».

Но бесконечно так продолжаться не могло. Я наконец решил нормально освоить Go, потому что все же что-то такое в нем есть, неуловимо притягательное. И сразу столкнулся с проблемами:

1) Хочется нормальный курс. А то они все сделаны в стиле «сейчас, дети, я два часа буду рассказывать вам, как объявлять переменные». «А вот что такое цикл». «Давайте подойдем поближе и внимательно рассмотрим удивительную конструкцию if-else». Эти ребята вообще в курсе, что некоторые из нас уже умеют программировать?

2) Хочется вменяемых задачек. А не «что вернет эта функция» (где функция представляет собой адовый треш, который никто в здравом уме никогда не напишет).

3) Одному скучно, хочется сообщников и сообщества. Обмениваться опытом и замечать то, что сам пропустил. Обсуждать подходы и практики. Да те же задачки решать.

Пока я думал, что с этим делать — наткнулся на концепцию «learn in public». В результате появился бесплатный курс «Go на практике». Если вы как я, в основном знакомы с Go по статьям в интернете, или знали, но подзабыли, или просто любите решать задачки — присоединяйтесь!

https://stepik.org/96832
Thank Go!
🥺 Независимость и контроль Нарассказывал я вам про фичу х10, но не всё так просто. Нельзя просто так взять и сделать понятную асинхронность. Да, в Го всё выглядит неплохо, по сравнению с другими языками (привет, @ohmypy), но всегда есть но. Дальше давайте…
🥺 Независимость и контроль – 2

@nalgeon взбудоражил канал своим learn in public. Придётся дописывать недописанные посты 😇

Напомню, мы разбирали ситуацию, когда горутина не успевает выполниться до завершения main горутины. В качестве варианта решения использовали WaitGroup из пакета sync.

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

1 fmt.Println("Hello, Gopher!")
2 done := make(chan struct{})
3 go func() {
4 time.Sleep(time.Second)
5 fmt.Println("I'm inside the Goroutine!")
6 done <- struct{}{}
7 }()
8 <-done
9 fmt.Println("Buy, Gopher!")
10 close(done)


>> Hello, Gopher!
>> I'm inside Goroutine!
>> Buy, Gopher!


Мы создаём канал и ждём из него сообщения в main горутине на 8 строчке. Пока этого не случится она будет заблокирована.

В дочерней горутине мы пишем в этот же канал на 6 строчке. Тем самым main горутина дожидается завершения работы дочерней. Таким образом мы получили полный аналог синхронизации из предыдущего примера. Код в плейграндуе.

Антон, как думаешь, какой из вариантов более Go Way?
Возможности языка

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

Нравится отсутствие явного public/private, скобочек в условиях и точек с запятой. Действительно, зачем синтаксис там, где без него можно обойтись.

Нравится, как сделаны функции. Все отлично — и несколько возвращаемых значений, и анонимные функции, и типы на основе функций, и методы.

Скорее нравится, что у всех файлов в пределах пакета общий scope. Скорее не нравится, что заранее не подумали о модулях и приделали их потом сбоку (да и назвали «модулями», хотя какие они к черту модули).

Нравятся указатели без арифметики. Хороший баланс между возможностями и хрупкостью.

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

Нравятся defined-типы, они добавляют семантичности коду.

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

Нравится embedding в структурах. Простая концепция, но необычайно мощная (сначала даже не понимаешь, насколько).

Нравится defer. Удобнее и мощнее, чем try-finally.

Не нравятся panic и recover. Даже не сами по себе, а то, что из-за них получилось два разных инструмента работы с ошибками.

Конечно, нравятся горутины и каналы. Но тут вряд ли может быть два мнения ツ
Thank Go!
🥺 Независимость и контроль – 2 @nalgeon взбудоражил канал своим learn in public. Придётся дописывать недописанные посты 😇 Напомню, мы разбирали ситуацию, когда горутина не успевает выполниться до завершения main горутины. В качестве варианта решения использовали…
🥺 Независимость и контроль – 3

Мы познакомились с горутинами (легкой абстракцией над тредами) и каналами — безопасным способом передачи данных между ними. Остался последний «кит», на котором держится асинхронность в Гошечке.

Рассмотрим вот такой пример:

1 ticker := time.NewTicker(500 * time.Millisecond)
2
3 go func() {
4 defer fmt.Println("Goroutine returned!")
5 for _ = range ticker.C {
6 fmt.Println("Tick")
7 }
8 }()
9
10 time.Sleep(1600 * time.Millisecond)
11 ticker.Stop()
12 fmt.Println("Ticker stopped")
13 time.Sleep(100 * time.Millisecond)

>> Tick
>> Tick
>> Tick
>> Ticker stopped

На первой строчке мы создаём тикер — счётчик, который представляет собой канал, в который периодически приходит сообщение с текущим временем. В нашем случае — раз в 500 миллисекунд. Дальше в горутине слушаем этот канал и печатаем тики (строка 6). В мейн горутине останавливаем тикер (строка 11) и выходим.

Но что же тут странного? То что defer в горутине (строка 4) так и не был исполнен. Это означает, что горутина не была завершена и продолжает ожидать сообщения в канале с тиками. Так произошло из-за того, что канал тикера не был закрыт — это связано с особенностью реализации функции ticker.Stop(), но сейчас не об этом. Такое поведение может возникнуть где угодно.

Горутина, ожидающая сообщения в открытом канале, в который никто уже не будет писать — яркий пример утечки памяти. Все выделенные на это дело ресурсы продолжат жить и не будут переиспользованы. А что если они будут копиться и копиться?

🐳 Третий кит

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

Вот на этот вопрос и отвечает последняя конструкция, необходимая для асинхронности здорового человека — select ...

Смотрим пример:

1 ticker := time.NewTicker(500 * time.Millisecond)
2
3 go func() {
4 defer fmt.Println("Goroutine returned!")
5 for {
6 select {
7 case <-done:
8 return
9 case <-ticker.C:
10 fmt.Println("Tick")
11 }
12 }
13 }()
14
15 time.Sleep(1600 * time.Millisecond)
16 ticker.Stop()
17 fmt.Println("Ticker stopped")
18 done <- struct{}{}
19 time.Sleep(100 * time.Millisecond)

>> Tick
>> Tick
>> Tick
>> Ticker stopped
>> Goroutine returned!


Внимание на конструкцию на строчках 5-12. В бесконечном цикле мы ждем сообщения из двух каналов: в каком раньше появляется, то и читаем. Когда мы отправляем сообщение в канал done в main горутине, то горутина с тикером завершается и наш defer срабатывает.

А теперь вопросы для обсуждения в чате:
1. А что будет с каналом ticker.C — ведь он так и не закрыт?
2. Можно ли не отправлять сообщение в канал done, но добиться такого же поведения?
Понятный код и Go

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

Коллега прочитал и спрашивает:

Как ты считаешь, у go это T искусственно ограничено? То есть, язык недостаточно выразителен, чтобы ввести абстракции высокого порядка и ты не сможешь написать «сложнее»? Подходит ли поэтому go для гетерогенных по опыту команд, будет ли это плохо для опытных?

Да Go прямо создан для уменьшения T! Судите сами:

— дубовый, минимум фич;
— принудительный код-стайл;
— запрет на неиспользуемые переменные и импорты;
— запрет на циклические зависимости.

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

Думаю, го намного лучше подходит для корпоративной разработки и больших команд, чем джава и ее производные. А маленькой команде прокачанных гуру будет в го тесновато, мягко говоря ツ
Go 1.17 Beta

Версия 1.17 выйдет в августе, а пока готова первая бета. В целом 1.17 выглядит спокойным релизом, вот что потенциально интересного:

— Conversions from slice to array pointer
— Lazy module loading
— Performance improvements of about 5%, and a typical reduction in binary size of about 2%
— Added a new testing flag -shuffle which controls the execution order of tests and benchmarks

Подробности: https://tip.golang.org/doc/go1.17