thank_go | Unsorted

Telegram-канал thank_go - Thank Go!

2615

Здравый взгляд на язык программирования Go. Злой админ @nalgeon. Добрый админ @mikeberezin. Рекламы нет.

Subscribe to a channel

Thank Go!

Generics

Рано или поздно это должно было случиться: в моем курсе по Go появился урок по дженерикам.

Совсем уж в дебри не погружает, но рассматривает базовые возможности, которых хватит в большинстве случаев:

— Обобщенные функции и типы.
— Параметры и ограничения типа.
— Пакеты slices и maps.
— Задачки! (как без них).

Урок доступен бесплатно, а если решите купить весь курс — сегодня действует скидка 25% по промо-коду GENERICS.

https://stepik.org/lesson/1352836

Читать полностью…

Thank Go!

Интерфейсы и nil, часть 2

Как мы обсудили в прошлый раз, внутри Go переменная типа interface представлена как пара (type, value). Когда мы вызываем метод на интерфейсной переменной, Go вызывает соответствующий метод value.

Пока интерфейсной переменной не присвоено значение, у нее и type, и value равны nil, поэтому сама переменная считается равной nil:

var ivar any
// type == nil, value == nil
// поэтому ivar == nil
fmt.Println(ivar == nil)
// true


Но как только интерфейсной переменной присвоили значение, type перестает быть nil. Поэтому переменная больше не равна nil, даже если value равно nil:

var e *english
fmt.Println(e == nil)
// true

ivar = e
// type == *english, value == nil
// поскольку type != nil, то ivar != nil
fmt.Println(ivar == nil)
// false


Это часто ставит в тупик начинающих (да и не только) разработчиков на Go, так что имейте в виду.

Читать полностью…

Thank Go!

Go 1.22

Вчера вышел Go 1.22 с интересными фичами:

— Починили багу с замыканием счетчика цикла (большое дело!)

— Цикл for range заработал по числам (прямо как в питоне)

— Новый пакет для генерации случайных чисел math/rand/v2

— Роутинг с поддержкой переменных в http (наконец-то)

Вместо сухих официальных релиз нотсов можно читать мои интерактивные с примерами.

P.S. Сделал и на русском тоже.

Читать полностью…

Thank Go!

Декораторы

Мой знакомый Редован поднял вопрос организации повторных попыток (retries) в Go.

Это интересная тема, потому что ретраи — частный случай декораторов, которые на практике встречаются постоянно (мидлвари, транзакции или аудит тоже удобно делать через декораторы).

В заметке он предлагает на выбор два подхода — рефлексию или дженерики.

Мне не нравятся оба. Предпочитаю использовать замыкания, потому что с ними код проще и понятней.

А вы что думаете? (опрос следует)

Читать полностью…

Thank Go!

Многозадачность: примитивы синхронизации

На курсе «Многозадачность в Go» вышли три новых урока!

В них вы:

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

P.S. Помню, что обещал закончить курс еще в прошлом году, а он все никак. Если вы передумали проходить и хотите вернуть деньги — пишите в личку.

https://stepik.org/a/133280

Читать полностью…

Thank Go!

Структура проекта в Go

Любимое развлечение го-разработчиков — спорить о правильной структуре проекта. Вот и авторы Go наконец решили высказаться (очень вовремя, всего-то 6 лет прошло с момента появления модулей).

Вкратце:

— Для мелких проектов — плоская структура без каталогов.
— Для более крупных — каталог internal с вложенными пакетами, публичные пакеты только при необходимости.
— Каталог cmd — когда в проекте несколько команд.

Что делать с прочими артефактами помимо кода (вроде документации и инфраструктуры) — скромно умолчали.

https://go.dev/doc/modules/layout

Читать полностью…

Thank Go!

1. Копирование

Можно было бы вернуть просто s[:]. Но это была бы поверхностная (shallow) копия — так скопируется срез, а массив под ним окажется тем же, что был. Поэтому изменяя копию среза, изменим и оригинальный массив.

Конструкция s[:0] создает срез нулевой длины (zero-length), который все еще ссылается на оригинальный массив. А s[:0:0] создает срез нулевой вместимости (zero-capacity), который ссылается на новый (пустой) массив.

Поэтому, когда мы добавляем к клону элементы оригинального s — они попадают в новый массив. Это и обеспечивает глубокое копирование.

Читать полностью…

Thank Go!

Довольно простая функция

Ян Ланс Тейлор (один из авторов Go) считает, что это довольно простая функция:

func Clone[S ~[]E, E any](s S) S {
return append(s[:0:0], s...)
}


А вы как считаете? (опрос следует)

Читать полностью…

Thank Go!

Пишем менеджер пакетов на Go

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

Так что если вам интересно посмотреть, как темы курса Thank Go! применяются на практике — рекомендую.

Вот какие темы нашли применение в проекте:

1) Все из модуля «Основы», от базовых типов до интерфейсов и ошибок.
2) Пакеты, модули и тесты.
3) Работа с текстом.
4) Работа с файлами.
5) JSON.
6) HTTP.

Исходники тоже доступны, разумеется.

А еще там комментарии и тесты на каждый файл, благодать. Когда еще такое встретишь!

https://antonz.ru/writing-package-manager/

Читать полностью…

Thank Go!

Наш отдел дата-аналитики трудился не покладая рук, и подготовил ИНФОГРАФИКУ.

Читать полностью…

Thank Go!

min, max и clear в Go 1.21

Вроде только недавно вышел Go 1.20, а тут уже релиз-кандидат 1.21 подвезли. Что поделать, будем разбирать.

В язык добавили аж три встроенных функции. Встроенные (builtin) — это такие, которые не требуют импорта пакета, вроде len или make.

Теперь появились еще min, max и clear.

min и max

Делают ровно то, что вы от них ожидаете. Наконец-то можно не писать цикл с ручным перебором среза:

m := max(10, 3, 22)
fmt.Println(m)
// 22


m := min(10, 3, 22)
fmt.Println(m)
// 3


песочница

clear

Тут интереснее. Функция работает на срезах и картах (а еще составленных из них дженериках), причем по-разному.

Из карты она удаляет все элементы:

m := map[string]int{"one": 1, "two": 2, "three": 3}
clear(m)
fmt.Printf("%#v\n", m)
// map[string]int{}


А вот у среза только зануляет элементы, не меняя длину:

s := []string{"one", "two", "three"}
clear(s)
fmt.Printf("%#v\n", s)
// []string{"", "", ""}


песочница

Знатоки Go легко ответят, почему со срезом так получилось. А мы сохраним интригу ツ

P.S. А вот еще неочевидный вопрос. Зачем делать встроенные min и max вместо дженерик-функций в пакете math? Ответ тоже существует.

Читать полностью…

Thank Go!

Кольцевой список

Кольцевой список — это разновидность связанного списка, у которого последний элемент ссылается на первый.

Выглядит примерно так (список из трех элементов 123, 456 и 789):

// голова
// ↓
// ┌ → 123 → 456 → 789 → ┐
// └─────────────────────┘


(надеюсь, схема уместилась у вас на экране без переноса строк)

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

В Go кольцевой список реализован типом ring.Ring.

Размер списка фиксирован, указываем его в конструкторе:

r := ring.New(3)


Элементы списка по умолчанию равны nil, а голова на первом элементе:

//      ↓
// ┌ → <n> → <n> → <n> → ┐
// └─────────────────────┘


Устанавливаем значение первого элемента и перемещаем голову на второй:

r.Value = 123
r = r.Next()

// ↓
// ┌ → 123 → <n> → <n> → ┐
// └─────────────────────┘


Устанавливаем второй, перемещаем голову на третий:

r.Value = 456
r = r.Next()

// ↓
// ┌ → 123 → 456 → <n> → ┐
// └─────────────────────┘


Устанавливаем третий, перемещаем голову на первый (список же закольцован):

r.Value = 789
r = r.Next()

// ↓
// ┌ → 123 → 456 → 789 → ┐
// └─────────────────────┘


И так далее хоть до бесконечности.

Список двусвязный, так что можно перемещаться и в обратном направлении через метод .Prev()

песочница

Читать полностью…

Thank Go!

Джавафикация Go (несерьезное)

Несмотря на то, что я очень уважаю авторов Go, хочу заметить, что я вот на такое не подписывался:

func min[P interface{ ~int64 | ~float64 }](x, y P) P


Сила Go всегда была в дубовой простоте. К сожалению, язык с дженериками физически не может оставаться простым.

Если вы в этом сомневаетесь — прочитайте свежую статью в блоге All your comparable types.

Go, ты должен был бороться со злом, а не примкнуть к нему!

Читать полностью…

Thank Go!

Другие изменения в stdlib 1.20

Вот еще несколько новшеств стандартной библиотеки Go 1.20, которые меня заинтересовали.

bytes.Clone() клонирует срез байт:

b := []byte("abc")
clone := bytes.Clone(b)


math/rand теперь самостоятельно инициализирует генератор случайным стартовым значением. Больше не нужно вызывать rand.Seed().

strings.CutPrefix() и strings.CutSuffix() обрезают префикс/суффикс аналогично TrimPrefix/TrimSuffix, но дополнительно сообщают, был ли этот префикс в строке:

s := "> go!"
s, found := strings.CutPrefix(s, "> ")
fmt.Println(s, found)
// go! true


sync.Map обзавелся атомарными методами Swap, CompareAndSwap и CompareAndDelete:

var m sync.Map
m.Store("name", "Alice")
prev, ok := m.Swap("name", "Bob")
fmt.Println(prev, ok)
// Alice true


time.Compare() сравнивает два времени и возвращает -1/0/1 по результатам сравнения:

t1 := time.Now()
t2 := t1.Add(10 * time.Minute)
cmp := t2.Compare(t1)
fmt.Println(cmp)
// 1


все изменения

Читать полностью…

Thank Go!

Причина ошибки контекста в Go 1.20

При отмене контекста происходит ошибка context.Canceled. Это не новость:

ctx, cancel := context.WithCancel(context.Background())
cancel()

fmt.Println(ctx.Err())
// context canceled


Начиная с 1.20, контекст можно создать с помощью context.WithCancelCause().

Тогда cancel() будет принимать один параметр — корневую причину ошибки (cause):

ctx, cancel := context.WithCancelCause(context.Background())
cancel(errors.New("the night is dark"))


Вытащить из контекста причину ошибки поможет context.Cause():

fmt.Println(ctx.Err())
// context canceled

fmt.Println(context.Cause(ctx))
// the night is dark


Возможно, вы спросите — почему context.Cause()? Логичнее ведь было бы добавить метод Cause в сам контекст, аналогично методу Err?

Да. Но Context — это интерфейс. А любое изменение интерфейса ломает обратную совместимость. Поэтому сделали так.

песочница

Читать полностью…

Thank Go!

nil-получатель метода

Раз уж мы заговорили о пустых значениях — вы же в курсе, что метод можно вызвать даже на пустом указателе?

type english struct {
name string
}

// e может быть nil!
func (e *english) greet() {
if e == nil {
fmt.Println("I'm nil")
return
}
fmt.Println("Hello", e.name)
}


Прям вот так взять и вызвать:

var e *english
e.greet()
// I'm nil


И если убрать проверку на nil из метода — будет паника на обращении к e.name 🤷‍♀️

Читать полностью…

Thank Go!

Интерфейсы и nil, часть 1

Внутри Go переменная типа interface представлена как пара (type, value), где value — конкретное значение, а type — тип этого значения (на самом деле все чуть сложнее, но совсем уж в дебри не будем погружаться).

Например:

// переменная интерфейсного типа
var ivar interface{}

ivar = "hello"
// ivar представлена парой (string, "hello")

ivar = 3.14
// ival представлена парой (float64, 3.14)


Когда мы вызываем метод на интерфейсной переменной, Go вызывает соответствующий метод value:

type greeter interface {
greet()
}

type english struct {
name string
}
func (e *english) greet() {
fmt.Println("Hello", e.name)
}

var ivar greeter = &english{"world"}
// type == *english, value == &english{"world"}
ivar.greet()
// вызывает value.greet() и печатает "Hello world"


Пока все логично. Но с nil вас может ожидать неожиданный поворот.

Stay tuned :)

Читать полностью…

Thank Go!

Замыкание = гибкость

Замыкания в Go используются повсеместно. Они позволяют универсально использовать один и тот же инструмент для работы в разных ситуациях.

Вот, например, карта в пакете syncsync.Map:

var m sync.Map
m.Store("alice", 11)
m.Store("bob", 12)
m.Store("cindy", 13)


Чтобы проитерироваться по карте, предусмотрен метод Range с такой сигнарутой:

Range(f func(key, value any) bool)


Но что делать, если мы хотим просуммировать все значения карты? Кажется, Range не дает такой возможности. Придется переписать все на питон.

Ну или использовать замыкание:

total := 0
m.Range(func(key, value any) bool {
total += value.(int)
return true
})


Анонимная функция, переданная в Range, использует внешнюю переменную total как накопитель. Таким образом можно реализовать вообще любое поведение, не трогая сигнатуру Range.

Полезная штука.

Читать полностью…

Thank Go!

Значение ↔ указатель

Все знают, что когда метод определен на значении, его можно вызвать на указателе:


type Cat struct{}
func (c Cat) Meow() {}

val := Cat{}
val.Meow()

ptr := &Cat{}
ptr.Meow()


https://go.dev/play/p/MydY2bB1qro

И наоборот:


type Cat struct{}
func (c *Cat) Meow() {}

val := Cat{}
val.Meow()

ptr := &Cat{}
ptr.Meow()


https://go.dev/play/p/9l7GVRcystN

Дело усложняется, когда переменная имеет не конкретный тип, а интерфейсный.

Работает почти всегда, кроме одной ситуации: метод определен на указателе, а интерфейсная переменная содержит значение:


type Meower interface {
Meow()
}

type Cat struct{}
func (c *Cat) Meow() {}

var ival Meower
ival = Cat{}


Здесь будет ошибка: Cat does not implement Meower (method Meow has pointer receiver)

Подробности в go-вики

Читать полностью…

Thank Go!

Go 1.22: интерактивные заметки к релизу

Release notes в Go традиционно сухие как прошлогодние сухари, а я люблю разбирать все новое на примерах.

Поэтому решил оживить заметки к релизу 1.22 (выйдет в феврале) и добавил в них интерактивных примерчиков, чтобы вы могли попробовать новые фичи уже сейчас, прямо из браузера.

https://antonz.org/go-1-22

Читать полностью…

Thank Go!

2. Сигнатура

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

То есть функция должна принимать тип S, который либо сам по себе срез ([]E), либо образован от среза (~[]E), а тип элемента среза при этом не важен (E any). На птичьем языке дженериков в Go получается S ~[]E, E any.

Такие дела.

Читать полностью…

Thank Go!

Довольно простая функция: разбор

Вот функция, простотой которой наслаждается Ян:


func Clone[S ~[]E, E any](s S) S {
return append(s[:0:0], s...)
}


Она выполняет глубокое (deep) копирование среза s: клонирует не только сам срез, но и массив под ним (как вы помните, срез — это легковесная структура, которая сама по себе не содержит данных, а содержит ссылку на массив).

Легкую оторопь тут могут вызвать два момента: само копирование и сигнатура функции. Разберем оба.

Читать полностью…

Thank Go!

Go ♡ SQLite

Тут добрый человек Riyaz Ali сделал Go-модуль с набором полезных расширений SQLite:

— кодирование/декодирование
— динамический SQL
— работа с файлами
— текстовые функции
— IP адреса
— мат. статистика
— UUID
— CSV

Если вы, как и я, неравнодушны SQLite — рекомендую!

https://github.com/riyaz-ali/sqlean.go

P.S. Небольшой анонс, чтобы два раза не писать. В сентябре возвращаюсь к курсу «Многозадачность в Go», так что если вдруг переживали за его судьбу — не переживайте ツ

Читать полностью…

Thank Go!

🗒 Узнаем больше о Go разработке

Ребята из DevCrowd (а они же основатели классного подкаста Podlodka) проводят исследование Go-разработчиков:

- Какие навыки для go-разработчиков самые важные
- Какие инструменты используются в работе
- Как попадают в профессию и куда из нее уходят
- Полезные для развития каналы, курсы и книги

Результаты будут в открытом доступе, а значит это отличный шанс узнать больше об индустрии.

Опрос довольно большой, но интересный. Мне потребовалось 10 минут, чтобы вдумчиво его пройти.

Давайте поучаствуем!

Пройти опрос

Читать полностью…

Thank Go!

Пока в комментариях идет битва за clear, отвечу на вопрос из посткриптума:

Зачем делать встроенные min и max вместо дженерик-функций

Ответ существует, но вам он не понравится. Процитирую авторов языка (с сокращениями):

We have gone back and forth a few times on this proposal about min/max being builtins vs being in package cmp.

There are good arguments on both sides. On the one hand, min and max are fundamental arithmetic operations much like addition, which justifies making them builtins.

On the other hand, we have generics now and it would make sense to use generics to write library code rather than make them builtins.

Even among the active Go language designers, our own personal intuitions differ on this.

полный ответ

🤷‍♀️

Читать полностью…

Thank Go!

Творим историю в Go

Вино и женщины Обстоятельства непреодолимой силы отвлекли меня от кольцевых списков, но пришло время к ним вернуться.

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

// голова
// ↓
// ┌ → 123 → 456 → 789 → ┐
// └─────────────────────┘


Одно из применений кольцевого списка — паттерн история. Это когда мы храним последние N штук чего-нибудь:

— последние 100 выполненных команд;
— последние 500 обработанных элементов;
— последние 30 отправленных сообщений.

Размер истории фиксирован, так что новые элементы должны вытеснять старые. Ровно то, что умеет кольцевой список!

Например, будем хранить последние пять выполненных команд:

history := ring.New(5)


Функция addCommand добавляет команду в историю:

func addCommand(history *ring.Ring, cmd string) *ring.Ring {
history.Value = cmd
return history.Next()
}


Запишем первые три:

func echo(n int) string {
return fmt.Sprintf("echo %d", n)
}


for i := 0; i < 3; i++ {
cmd := echo(i + 1)
history = addCommand(history, cmd)
}
// 1 2 3 <nil> <nil>
// ↑


Ожидаемо, есть еще два свободных места. Запишем их:

for i := 3; i < 5; i++ {
cmd := echo(i + 1)
history = addCommand(history, cmd)
}
// 1 2 3 4 5
// ↑


Список заполнился. Что будет, когда придут новые команды?

for i := 5; i < 8; i++ {
cmd := echo(i + 1)
history = addCommand(history, cmd)
}

// 6 7 8 4 5
// ↑


Новые команды (6-7-8) затерли более старые (1-2-3). Это как раз то, чего мы ожидаем от истории.

Обход истории при помощи метода Do выдаст команды от самой старой к самой новой:

history.Do(func(cmd any) {
fmt.Println(cmd)
})

// echo 4
// echo 5
// echo 6
// echo 7
// echo 8


Достаточно удобно. Еще удобнее было бы обернуть все это в собственный тип History, но об этом в другой раз.

песочница: https://go.dev/play/p/lxxkHwqpbIW

Читать полностью…

Thank Go!

📆 Почему такой странный формат для даты и времени?

Поступил вопрос из зала, как же можно запомнить формат даты и времени в Golang и почему он вообще такой странный?

2006-01-02T15:04:05.000

Понять его логику крайне сложно, потому что мы не привыкли к тому формату даты, в котором он создавался. А вот авторы языка наоборот — для них родной формат даты, используемый в Америке. И тогда все становится на свои места, а формат запоминается по простой мнемонике.

Jan 2 15:04:05 2006 MST

1 2 3 4 5 6 7

Читать полностью…

Thank Go!

Оптимизация по профилю

Быстрота гошного кода — в значительной мере заслуга оптимизирующего компилятора (а вы думали, ваша? ха!).

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

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

Но начиная с Go 1.20, можно приделать к компилятору ChatGPT снять профиль нагрузки с продакшена и выдать его компилятору. Благодаря этому оптимизации станут эффективнее, а бинарник заработает на 2-4% быстрее без изменений в исходниках.

Фича называется Profile-Guided Optimization (PGO), в блоге Go есть хорошая вводная статья про нее:

https://go.dev/blog/pgo-preview

Читать полностью…

Thank Go!

Ручное управление памятью в Go 1.20

Занятую память в Go освобождает сборщик мусора. Это одно из архитектурных решений, на которых построен язык.

Несмотря на это, в версию 1.20 протащили экспериментальный пакет arena. Он выделяет кусок памяти, в котором сборщик мусора не работает.

Типа такого:

type T struct{
// ...
}

func process() {
mem := arena.NewArena()
defer mem.Free()

for i := 0; i < 10; i++ {
obj := arena.New[T](mem)
}
}


Получив полную панамку неприятных вопросов, разработчики Go потерли любые упоминания арен из release notes (хотя сама реализация, вероятно, останется).

It is unclear when, if ever, we will pick up the idea and try to push it forward into a public API, but it's not going to happen any time soon, and we don't want users to start depending on it.

Так что подробно рассказывать про арены не буду ツ

спека для любопытных

Читать полностью…

Thank Go!

Альтернативный подход к обработке паники (несерьезное)

Все, наверно, слышали про ChatGPT. Она неплохо умеет отвечать на всевозможные вопросы по Go, но пока не предоставляет нормальное API.

Поэтому народные умельцы «повторили» ChatGPT на предыдущем поколении нейросети — GPT-3. Я не смог пройти мимо и задал ей вопрос:

> How to recover from panic in Go?

Ответ:

Recovering from panic in Go is a process that can take some time. The best way to start is to take a step back and take some deep breaths. Focus on your breathing and try to relax your body. You can also try some relaxation techniques such as progressive muscle relaxation or guided imagery.

Additionally, you can try talking to someone you trust and confide in them about your worries. Finally, if the panic persists, it may be beneficial to reach out to a professional for counseling or treatment.

Так и отвечайте на собеседованиях!

Читать полностью…
Subscribe to a channel