Thank Go!
1.78K subscribers
3 photos
73 links
Неожиданный взгляд на язык программирования Go. Конструктив от @mikeberezin с нотками сарказма от @nalgeon
Download Telegram
✍️ Беспощадные строки в Go. Часть 4

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

Возьмём строку в разных представлениях (в байтах и рунах) и попробуем её изменить.
s := "hello"
b := []byte(s)
r := []rune(s)

b[0] = 98 // 'b'
r[0] = 'r'

fmt.Printf("%s %s %s\n", s, string(b), string(r))

>> hello bello rello


А если изменить что-то в строке?
s[0] = "s"

>> cannot assign to s[0]
>> Go build failed.

Компилятор ругается на присваивание элемента в строке. Т.е. не такой уж это и слайс?
На этот вопрос на поможет ответить понимание самой внутренней структуры слайса и строки, посмотрим на неё при помощи рефлексии:
sHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))
bHeader := (*reflect.SliceHeader)(unsafe.Pointer(&b))
rHeader := (*reflect.SliceHeader)(unsafe.Pointer(&r))

>> &reflect.StringHeader{Data:0x4c1fd1, Len:5}
>> &reflect.SliceHeader{Data:0xc00002c008, Len:5, Cap:8}
>> &reflect.SliceHeader{Data:0xc000014020, Len:5, Cap:8}


Видно, что у настоящих слайсов есть ёмкость (Cap), а у строки — нет.
На самом деле строки являются неизменяемыми слайсами. А при конвертации в слайсы байт и рун происходит копирование данных под капотом. Это видно из поля Data, в котором собственно и хранится ссылка на первый элемент массива данных, на котором строится слайс.
🌶 Для тех кто любит острее

Вышло как-то скучно, поэтому добавим перчика.

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

В Го такая возможность есть. И если вам нужно всё-таки сделать изменяемую строку, то вот она:
b := []byte{'h', 'e', 'l', 'l', 'o'}
s := *(*string)(unsafe.Pointer(&b))
fmt.Printf("%T %s\n", s)

>> string hello


Всё честно, это строка. А теперь следите за руками:
b[0] = 's'
fmt.Println(s)

>> sello


Строка и слайс байт лежат в памяти в одном месте, никакого копирования.
fmt.Printf("%#v\n", (*reflect.SliceHeader)(unsafe.Pointer(&b)).Data)
fmt.Printf("%#v\n", (*reflect.StringHeader)(unsafe.Pointer(&s)).Data)

>> 0xc00002c008
>> 0xc00002c008


Внимание! Так делать очень опасно и не рекомендуется. Пакет unsafe отбрасывает систему типов Го и лезет напрямую в память. Это может приводить к очень страшным рантайм ошибкам.

Например, если подобный трюк попытаться провернуть в обратную сторону, то ничего не выйдет:
s = "hello"
b = *(*[]byte)(unsafe.Pointer(&s))
b[0] = 's'

>> unexpected fault address 0x4c1db5
>> fatal error: fault


Без понимания того, как работает коробка передач, лучше ездить на автомате ;)
✍️ Беспощадные строки. Финал

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

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

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

Как же так?
Больше года думали, а на первый взгляд кажется, что придумали что-то странное.

Дизайн основных примитивов языка — это фича. Причём у этой «фичи» сразу несколько важных особенностей:
* это самые часто используемые элементы языка,
* сложно сделать что-то слишком выходящее за рамки привычного опыта пользователя,
* дизайн напрямую влияет на перфоманс языка.

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

Язык программирования — это уже не просто синтаксис + компилятор. Для повседневной разработки важны инструменты и библиотеки. Обычно эта обвязка вырастает из сформировавшегося вокруг языка комьюнити. Но что делать, когда ты только выходишь на рынок? Это типичная проблема платформенной бизнес-модели (можно считать её проблемой курицы и яйца). Платформе нужны пользователи и поставщики, но поставщики не придут, пока не будет пользователей и наоборот. Тоже самое и с языком — комьюнити растёт вокруг удобных инструментов и библиотек.

И тут очень важна «стандартная комплектация» — набор инструментов и библиотек, которые уже идут в комплекте. Вот и посмотрим на них.

Начнем с gofmt, которая уже упоминалась в канале. Кажется, что это очень простая идея — сделать форматер языка, но она оказалась довольно сильной. В Го полностью отсутствуют холивары о том, какая длина строки верная, где логичнее ставить переносы и отсупы, т.к. сразу был один верный сттандарт, идущий из коробки, который всем ОК.

Это привело к тому, что весь код на Го, даже который ты видишь в первый раз переключившись на внутренности используемой библиотеки, тебе знаком и понятен. Ты сразу визуально понимаешь что и где происходит. Даже горячо обсуждаемые конструкции
if err != nil {
return err
}


оказываются полезными — они делят код на функциональные блоки,
→ что-то сделали
→ возможно получили ошибку
→ что-то сделали с ошибкой


которые автоматом вычленяются мозгом при первом взгляде на код.

Т.е. простая идея встроенного форматера сделала весь код Го скучным и одноликим, но быстро поняным и знакомым.
О, если бы понятность кода достигалась единообразным форматированием 🙂
📜 Главный инструмент

Самое время для пятничного поста.
Результаты последнего опроса 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/a/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. Даже не сами по себе, а то, что из-за них получилось два разных инструмента работы с ошибками.

Конечно, нравятся горутины и каналы. Но тут вряд ли может быть два мнения ツ