Thank Go!
552 subscribers
1 photo
31 links
Неожиданный взгляд на язык программирования Golang. Конструктив от @mikeberezin с нотками сарказма от @nalgeon
Download Telegram
🌈 Дженерики в Го — как показать сообществу сложную фичу

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

Если посмотреть на опросы пользователей 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
😱 Про панику

В одном из последних проектов нужно было много и часто сортировать мапки (или как @nalgeon любит говорить «карты»). Сортировать по-разному: то по ключам, то по значением, то с приведением к разным типам или вообще по длине строк. Пока Go2 с дженериками не появился на свет, такие сортировки приходится делать руками.

Чтобы добро не пропадало, дженерализировал эти сортировки и вынес в библиотеку. У сортировщика есть два вида интерфейса на любой вкус, а также обработка ошибок через recover. На этом остановлюсь чуть подробнее.

В прошлом году слушал интересный доклад ребят из CockroachDB о минусах явной обработки ошибок и возможности его замены на recover. Они подсвечивают, что использование паники даёт даже выигрыш в перфомансе. Подход используется и в стандартной библиотеке (например, json), а также описан в Effective Go.

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

Также в пакете есть бенчмарки перфоманса сортировки библиотекой и нативным go кодом. Чудес не произошло — теряем почти в 5 раз по всем метрикам.
📐 The Busy Developers's Guide to Go Profiling, Tracing and Observability

Ребята из датадог начали писать новую версию гайда по профайлингу Го.

Пока есть только одна статься (с гифками!) о работе планироващика и сборщика мусора. Минималистично и понятно, рекомендую!

Будем ждать новых статей.

https://github.com/DataDog/go-profiler-notes/tree/main/guide
reflect и линейный поиск

Любопытная история. Искандер Шарипов заметил, что при сборке сайта в Hugo 20% времени занимает вызов reflect.Type.MethodByName()

Начал копать, и обнаружил, что MethodByName() ищет нужный метод линейным перебором по срезу. Понятно, что можно чинить это в Hugo, но Искандер предложил более радикальный вариант.

В итоге сделал патч для Go, который включает бинарный поиск вместо линейного при большом количестве методов. И даже получил ответ от Роба Пайка!

120 methods is just crazy. I wonder how common this problem actually is, and if a different fix, if any, is the right way forward. MethodByName works very hard, even ignoring the a linear scan.

Also, using the reflect package to call a method is sometimes necessary but often avoidable with a bit of work elsewhere. I am nervous about opening the door to optimizations of the reflect package.

https://habr.com/ru/post/645727/
https://github.com/golang/go/issues/50617
Дженерики, добро пожаловать!

Вышел релиз Го 1.18 с поддержкой дженериков!
Полные релиз ноутс.

В качестве бонуса лучшее на сегодня применение дженериков.

Делитесь в комментах, если знаете что-то еще.
Go на практике: чистый код

Доделал второй модуль курса «Go на практике»! В нем:

— Пакеты и модули
— Тесты
— Бенчмарки
— Профайлер

Думал еще фаззинг добавить, но решил пока без него.

Если интересно — проходите, пишите в комментариях вопросы и замечания.
Go на практике: многозадачность

Доделал третий модуль курса «Go на практике»! В нем:

— Горутины и каналы
— Конвейеры, композиция и селект
— Работа со временем
— Контекст
— Синхронизация

Модуль не сделает вас гуру многозадачности, но гарантирую намного более глубокое понимание темы, чем после Tour of Go и некоторых других курсов.

Если интересно — проходите, пишите в комментариях вопросы и замечания.
🎲 Чем полезен фаззинг

В Го 1.18 появилась возможность фаззинг тестирования. Вроде бы и интересно, но непонятно, где может быть полезно в реальной жизни. Давайте разбираться.

Представим, что вам достался проект на рефакторинг и вы натыкаетесь на следующую функцию.

func isCapital(src string) bool {
re := regexp.MustCompile(`[^A-Z]`)
return !re.MatchString(src)
}


Вы были прилежным учеником на курсе «Go на практике» и знаете, что регулярками надо пользоваться только при сильной необходимости, и кажется, что тут её нет.

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

{"01. Positive", "CAPITAL", true},
{"02. Negative", "notCAPITAL", false},

Для надежности проверим покрытие.

>> go test . -run=Test_isCapital -cover      
coverage: 100.0% of statements


Ну все отлично! Делаем быстренький рефакторинг, получаем выигрыш в скорости. Тесты проходят на отлично.

func isCapital2(src string) bool {
for _, r := range src {
if r < 'A' || r >= 'Z' {
return false
}
}
return true
}

Но теперь сделаем фаззинг тест.

func Fuzz_isCapitalRef(f *testing.F) {
f.Add("hello")
f.Fuzz(func(t *testing.T, src string) {
if isCapital(src) != isCapital2(src) {
t.Errorf("%s", src)
}
})
}


Логика следующая. Мы говорим тесту, что наша функция принимает на вход строку, например, hello. Дальше библиотека сама начинает мутации этого входного параметра — перебирает разные возможные варианты строк. А проверяем — совпадает ли результат старой и новой реализации. По умолчанию перебор вариантов фаззинг тестов будет происходить до тех пор, пока тест не упадет — хоть тысячи, хоть миллионы мутировавших строк (это время можно ограничить дополнительным параметром -fuzztime).

И вот результат

--- FAIL: Fuzz_isCapitalRef (0.00s)
    main_test.go:48: AAZ


Фаззинг тест нашел строку AAZ, на которой результат отличается.

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

Пользуйтесь!
Вышел Go 1.19!

— Сборщик мусора научился уважать лимит на использование памяти.

— Дженерики стали до 20% быстрее.

sync/atomic получил удобные типы вроде Bool, Int32, Int64 и Pointer.

— В fmt появились Append, Appendf и Appendln, которые дописывают к байтовому срезу.

— В net/url появилась JoinPath, которая собирает URL из кусочков.

— Сортировка в sort стала быстрее (хотя куда уж еще), плюс появилась функция Find, которая как Search, но для людей.

— В time добавился метод Duration.Abs, который возвращает абсолютное значение продолжительности времени.

https://go.dev/doc/go1.19