thank_go | Unsorted

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

2615

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

Subscribe to a channel

Thank Go!

Или

Допустим, ваше приложение по умолчанию слушает на порту 8080. Порт можно изменить, задав переменную окружения PORT. Как бы реализовать это в коде?

Можно так:

port := os.Getenv("PORT")
if port == "" {
port = "8080"
}


А с функцией cmp.Or можно компактнее:

port := cmp.Or(os.Getenv("PORT"), "8080")


Or принимает любое количество аргументов и возвращает первый ненулевой. Работает в 1.22+

Мелочь, но приятная.

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

Thank Go!

README для начинающих разработчиков

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

The Missing README

Это по сути такой очень подробный чеклист на 250 страниц по всяким хорошим практикам в разных областях разработки:

— Работа с кодом
— Проектирование и архитектура
— Управление зависимостями
— Тестирование
— Ревью кода
— Сборка, интеграция и деплой
— Решение инцидентов с прода
— Планирование и оценка
— Общение с менеджером
— Дальнейшая карьера

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

Есть перевод на русский, но он местами с потерей исходного смысла.

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

Thank Go!

Проверки в коде

Разберем функцию, которая проверяет разрешения пользователя:

// CanEdit returns true if the user can edit objects.
func CanEdit(user *User) bool {
if user.IsActive() {
perms := user.Permissions()
for _, perm := range perms {
if perm == PermWrite {
return true
}
}
return false
} else {
return false
}
}


Оставим за скобками вопросики к типу User — например, почему CanEdit не сделан методом. Для простоты будем считать, что тип User менять нельзя.

Первая проблема — нет проверки на nil. Вторая — отсутствие early return (он же guard clause). С nil, думаю, и так понятно, а вот на раннем возврате остановимся подробнее.

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

func Do() {
if cond_1 {
// do stuff
if cond_2 {
// do more stuff
if cond_3 {
// do even more stuff
}
}
}
}


Читать такое больно из-за лесенки ифов. Лучше переделать на ранний возврат, а «основной сценарий» сделать без отступов:

func Do() {
if !cond_1 {
return
}
// do stuff

if !cond_2 {
return
}
// do more stuff

if !cond_3 {
return
}
// do even more stuff
}


Теперь явно видно, что делает функция, и что в ней может пойти не так.

Применим к CanEdit:

func CanEdit(user *User) bool {
if user == nil || !user.IsActive() {
return false
}
for _, perm := range user.Permissions() {
if perm == PermRead {
return true
}
}
return false
}


Можно еще заменить цикл на slices.Contains (хотя это уже частности):

func CanEdit(user *User) bool {
if user == nil || !user.IsActive() {
return false
}
perms := user.Permissions()
return slices.Contains(perms, PermRead)
}


Главное — не забывайте проверять на nil и использовать ранний возврат.

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

Thank Go!

Кто канал создал, тот и закрывает

В Go есть одна эвристика, которую лучше не нарушать без веских причин: кто канал создал, тот и закрывает.

Поэтому мне больше по душе такая реализация канала завершения:

func work() <-chan struct{} {
done := make(chan struct{})
go func() {
// do work
time.Sleep(10 * time.Millisecond)
close(done)
}()
return done
}


done := work()
<-done


Если нужно не просто сигнализировать о завершении, а возвращать значение — заменяем chan struct{} на нужный тип вроде chan int, и готово.

А если хотим возвращать еще и ошибку, то так:

type Result[T any] struct {
Value T
Err error
}

func work() <-chan Result[int] {
out := make(chan Result[int], 1)
go func() {
// do work
time.Sleep(10 * time.Millisecond)
out <- Result[int]{Value: 42}
close(out)
}()
return out
}


out := work()
result := <-out
fmt.Println(result)
// {42 <nil>}


песочница

Удобно!

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

Thank Go!

Про печеньки

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

За годы развития веба куки обзавелись дополнительной атрибутикой. Вот как выглядит структура Cookie в Go 1.23:

Name и Value — ключ и значение.
Quoted — true, если значение передано в кавычках.
Path — разрешает куку на страницах, которые начинаются с указанного пути.
Domain — разрешает куку на указанном домене и всех его поддоменах.
Expires — дата окончания годности куки.
MaxAge — срок жизни куки в секундах.
Secure — разрешает куку только по HTTPS.
HttpOnly — закрывает доступ к куке из JavaScript.
SameSite — разрешает или запрещает куку при кросс-доменных запросах.
Partitioned — ограничивает доступ к third-party кукам.

Неслабо, да?

Начиная с версии Go 1.23, серверную куку можно распарсить из строки с помощью http.ParseSetCookie:

line := "session_id=abc123; SameSite=None; Secure; Partitioned; Path=/; Domain=example.com"
cookie, err := http.ParseSetCookie(line)


Браузерные куки тоже можно распарсить из строки, с помощью http.ParseCookie:

line := "session_id=abc123; dnt=1; lang=en; lang=de"
cookies, err := http.ParseCookie(line)


песочница

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

Thank Go!

Уникальные значения

Продолжаем разбирать нововведения в Go 1.23.

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

Рассмотрим пример. У нас есть генератор слов:

const nDistinct = 100
const wordLen = 40
generate := wordGen(nDistinct, wordLen)

fmt.Println(generate())
// nlfgseuif...
fmt.Println(generate())
// anixapidn...
fmt.Println(generate())
// czedtcbxa...


Размер словаря ограничен (100 слов), так что генерируемые значения будут часто повторяться.

Сгенерим 10000 слов и запишем их в срез строк:

words = make([]string, nWords)
for i := range nWords {
words[i] = generate()
}


Memory used: 622 KB


10К слов заняли 600 Кб в куче.

Попробуем другой подход. Используем unique.Handle, чтобы назначить дескриптор каждому уникальному слову, и будем хранить эти дескрипторы вместо самих слов:

words = make([]unique.Handle[string], nWords)
for i := range nWords {
words[i] = unique.Make(generate())
}


Memory used: 95 KB


100 Кб вместо 600 Кб — в 6 раз меньше памяти.

Функция Make создает уникальный дескриптор для значения любого comparable-типа. Она возвращает ссылку на «каноническую» копию значения в виде объекта Handle.

Два Handle равны только в том случае, если равны исходные значения. Сравнение двух Handle эффективно, потому что сводится к сравнению указателей.

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

песочница

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

Thank Go!

Ча-ча-ча

(несерьезное) В чате спросили, разбирается ли на курсе тема канала каналов (chan chan). Пользуясь случаем, хочу вам его представить.

Встречайте: ча, чача, и ча-ча-ча:

cha := make(chan int, 1)
chacha := make(chan chan int, 1)
chachacha := make(chan chan chan int, 1)

cha <- 1
chacha <- cha
chachacha <- chacha

fmt.Printf("%#v\n", chachacha)
// (chan chan chan int)(0x14000102180)


Если серьезно, то канал — это всего лишь указатель на структуру. Ничего особенного в нем нет.

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

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

Thank Go!

Таймеры в Go 1.23

Тут прям детективная история приключилась. В Go есть таймер (тип Timer), а в нем — поле с каналом (Timer.C), в который таймер тикает спустя указанное время.

В коде стдлибы таймер создается так:

func NewTimer(d Duration) *Timer {
c := make(chan Time, 1)
t := &Timer{
C: c,
// ...
}
startTimer(&t.r)
return t
}


Такая реализация привела к проблемам с time.After и Reset, от которых многие страдали.

И вот в Go 1.23 решили это исправить, для чего сделали канал в таймере небуферизованным:

// As of Go 1.23, the channel is synchronous (unbuffered, capacity 0),
// eliminating the possibility of those stale values.
func NewTimer(d Duration) *Timer {
c := make(chan Time, 1)
t := (*Timer)(newTimer(when(d), 0, sendTime, c, syncTimer(c)))
t.C = c
return t
}


Вот только если вы посмотрите на фактический код, то канал-то остался буферизованным 😁

c := make(chan Time, 1)


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

Specifically, the timer channel has a 1-element buffer like it always has, but len(t.C) and cap(t.C) are special-cased to return 0 anyway, so user code cannot see what's in the buffer except with a receive.

Эту логику вкорячили прямо в реализацию канала (тип chan).

Что тут скажешь. Ну и дичь.

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

Thank Go!

Безопасные ворота

Вот три способа безопасно закрыть ворота, один другого краше.

➊ sync.Mutex

Дубовый, но надежный способ. Защищаем изменение логического поля closed мьютексом:

type Gates struct {
// признак закрытия ворот
closed bool
// мьютекс для защиты closed
mu sync.Mutex
}

func (g *Gates) Close() {
g.mu.Lock()
defer g.mu.Unlock()
if g.closed {
// игнорируем повторное закрытие
return
}
// закрыть ворота
g.closed = true
// освободить ресурсы
}


➋ atomic.Bool

Compare-and-set на атомарном bool гарантирует, что только одна горутина сможет поменять значение с false на true:

type Gates struct {
// признак закрытия ворот
closed atomic.Bool
}

func (g *Gates) Close() {
if !g.closed.CompareAndSwap(false, true) {
// игнорируем повторное закрытие
return
}
// закрыли ворота,
// можно освободить ресурсы
}


➌ sync.Once

Once.Do гарантирует однократное выполнение в конкурентной среде, поэтому не приходится даже явно хранить состояние:

type Gates struct {
// гарантирует однократное выполнение
once sync.Once
}

func (g *Gates) Close() {
g.once.Do(func() {
// освободить ресурсы
})
}


Правда, такие ворота уже не получится открыть обратно, в отличие от предыдущих вариантов.

Кто ваш любимчик? Поделитесь в комментариях.

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

Thank Go!

Помоги Маше закрыть ворота

Друзья, тут такое дело. Маша закрывает ворота вот так:

type Gates struct {
// признак закрытия ворот
closed chan struct{}
}

func (g *Gates) Close() {
select {
case <-g.closed:
// игнорируем повторное закрытие
return
default:
// закрыть ворота
close(g.closed)
}
}


Безопасно ли одновременно вызывать Close() из нескольких горутин? Опрос следует.

P.S. Чур в комментариях ответ не спойлерить!

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

Thank Go!

Обновил курс до Go 1.22

Добавил в базовый курс по Go новые блоки и темы:

— range по целым числам;
— встроенные функции min, max и clear;
— комбинация ошибок через errors.Join;
— причина отмены контекста и context.AfterFunc (+ забористая задачка);
— дженерики, пакеты slices и maps (про них уже писал выше);
— тип-обертка Null в database/sql.

Заодно добавил маленький урок 1.9 со всякой всячиной (пока там блоки, группы и йота).

P.S. А еще на этом уроке вам предстоит победить Сквернолапа. Ну или Когтевика. В крайнем случае — Огнежора 😅

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

Thank Go!

Количество одновременных горутин

Вот конструкция, к которой мы пришли:

func work() {
// работает работу на работе
}

nConc = runtime.NumCPU()*2
sema := make(chan struct{}, nConc)

const n = 100_000
for range n {
sema <- struct{}{}
go func() {
work()
<-sema
}()
}


Количество одновременных горутин равно количеству ядер × 2. Это хороший подход, если работа внутри work в основном использует CPU (CPU-bound).

Но что делать, если work делает HTTP-запросы? Тогда, вероятно, большую часть времени она ожидает ответа от удаленного сервера, и CPU при этом не используется.

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

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

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

Итого:

➊ Ограничивайте количество одновременных горутин (семафором или другими средствами).

➋ Для CPU-bound задач степень параллелизма определяется количеством ядер.

➌ Для I/O-bound задач степень параллелизма определяется спецификой работы и доступными ресурсами.

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

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

Thank Go!

Запускаем 100К горутин

Итак, сколько же одновременно живущих горутин можно создать в 1Гб памяти без проблем для работы сервера? Давайте разбираться.

Горутина — это структура минимального размера около 2Кб, так что формально в 1Гб можно уместить 1024*1024/2 ≈ 500K горутин. Понятно, что часть памяти уже занята, но 100 тысяч уж всяко уместятся, так что это ответ 100000+

Но есть нюанс (и даже не один).

Допустим, мы взяли и запустили все горутины, вот так:

func work() {
// пока забудем, что именно
// тут происходит
}

const n = 100_000
for range n {
go work()
}


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

Что с этим делать — в следующей заметке.

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

Thank Go!

Приемчики форматирования

Несколько приемов форматирования, о которых вы, возможно, не слышали.

➊ Закавыченная строка

Используйте %q, чтобы вывести строковое значение в кавычках.

s := "Hello, World!"
fmt.Printf("%q\n", s)
// "Hello, World!"


➋ Названия полей структуры

Используйте %+v, чтобы вывести названия полей структуры, а не только значения.

alice := person{"Alice", 25}
fmt.Printf("%+v\n", alice)
// {name:Alice age:25}


➌ Тип значения

Используйте %T, чтобы вывести тип значения.

var val any
val = 42
fmt.Printf("%T: %v\n", val, val)
// int: 42

val = "Hello, World!"
fmt.Printf("%T: %v\n", val, val)
// string: Hello, World!

val = person{"Alice", 25}
fmt.Printf("%T: %v\n", val, val)
// main.person: {Alice 25}


➍ Индекс аргумента

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

num := 42
fmt.Printf("%[1]T: %[1]v\n", num)
// int: 42


Нумерация с 1.

песочница

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

Thank Go!

range по функциям — что это и зачем

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

Предположим, вы написали собственный тип OrderedMap, который (в отличие от обычной карты) сохраняет порядок элементов.

Сделали ему конструктор и метод Set:

m := NewOrderedMap[string, int]()
m.Set("one", 1)
m.Set("two", 2)
m.Set("thr", 3)


Хорошо, а как теперь итерироваться по карте? Традиционно это делали примерно так:

m.Range(func(k string, v int) {
fmt.Println(k, v)
})
// one 1
// two 2
// thr 3


песочница

А с новой фичей range-over-func можно сделать так:

for k, v := range m.Range {
fmt.Println(k, v)
}


песочница

То есть это такой синтаксический сахарок.

Стоило ли оно того? Об этом следующая заметка.

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

Thank Go!

Курсы на Степике

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

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

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

Thank Go!

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

Второй год подряд ребята из DevCrowd проводят большое исследование Go-разработчиков:

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

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

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

👉 Пройти опрос

👀 Посмотреть результаты прошлого года

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

Thank Go!

Проверки в коде

Есть вот такая функция, которая проверяет разрешения пользователя.

// CanEdit returns true if the user can edit objects.
func CanEdit(user *User) bool {
if user.IsActive() {
perms := user.Permissions()
for _, perm := range perms {
if perm == PermWrite {
return true
}
}
return false
} else {
return false
}
}


Что думаете о ней? (опрос следует)

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

Thank Go!

Канал завершения

Как вы знаете, с помощью done-канала горутина сигнализирует вызывающему, что закончила работать.

Есть пара вариантов реализации:

➊ Принимаем done-канал на входе

func worker1(done chan struct{}) {
// do work
time.Sleep(10 * time.Millisecond)
close(done)
}


done := make(chan struct{})
go worker1(done)
<-done


➋ Возвращаем done-канал из функции

func worker2() chan struct{} {
done := make(chan struct{})
go func() {
// do work
time.Sleep(10 * time.Millisecond)
close(done)
}()
return done
}


done := worker2()
<-done


песочница

Какой вариант вам больше по душе и почему?

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

Thank Go!

Скопировать каталог

Раньше, чтобы рекурсивно скопировать каталог со всем содержимым, вам пришлось бы написать 50 строк кода.

Теперь, благодаря os.CopyFS в Go 1.23, будет достаточно одной:

src := os.DirFS("/home/src")
dst := "/home/dst"
err := os.CopyFS(dst, src)


Вроде и мелочь, но весьма уместная.

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

Thank Go!

Пустышки

Вряд ли вы об этом задумывались, но все эти конструкции в Go разрешены:

// ничего не делает
{}

// ничего не делает
switch {}

// бесконечный цикл
for {}

// блокирует горутину
select {}


Полезным может быть разве что select{} для быстрых демок — это самый короткий способ заблокировать горутину.

Например:

func main() {
// тикающая горутина
go func() {
ticker := time.NewTicker(1 * time.Second)
for range ticker.C {
fmt.Print(".")
}
}()

// блокирует горутину main
select {}
}

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

Thank Go!

Курс «Многозадачность в Go»

Закончил курс по многозадачности! Вот какие темы в нем разобраны:

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

Если вы совсем не знакомы с многозадачностью, курс поможет освоить ее с нуля. А если уже прошли модуль «Многозадачность» на курсе «Go на практике» — детально разберетесь в гонках, синхронизации и пакетах sync и atomic.

Как обычно, все концепции разобраны на практических примерах и закреплены задачками с автоматической проверкой.

Курс непростой. Подойдет практикующим разработчикам с уверенным знанием основ Go.

https://stepik.org/a/133280

Цена скоро вырастет.

Всем go 💪

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

Thank Go!

Git в примерах

Зашел я на Степик в новые курсы, а там «Основы Git». В связи этим вспомнил, что у меня тоже есть интерактивная книга / сборник рецептов по гиту, называется Git by example.

Удобный краткий формат с конкретными примерами, я сам туда постоянно подглядываю (особенно в раздел undo).

Загляните и вы, если не обзавелись еще черным поясом по гиту.

https://antonz.org/git-by-example

P.S. А вы же знаете, что по Go тоже такая есть?

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

Thank Go!

Опасные ворота

Вот наши ворота:

type Gates struct {
// признак закрытия ворот
closed chan struct{}
}

func (g *Gates) Close() {
select {
case <-g.closed:
// игнорируем повторное закрытие
return
default:
// закрыть ворота
close(g.closed)
// освободить ресурсы
}
}


Метод Close — небезопасный. Если две горутины одновременно вызовут Close, обе могут провалиться в default-ветку селекта, обе попытаются закрыть канал, и второе закрытие приведет к панике.

Другими словами, здесь гонки на закрытии канала. Селект сам по себе не защищает от гонок. Sad but true.

Что с этим делать — традиционно в следующей заметке.

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

Thank Go!

Баян [::]

Гошный баян (вообще он «полносрезное выражение» или full slice expression, но «баян» мне ближе) имеет такой синтаксис:

s[low : high : max]


Баян создает срез длиной high-low и емкостью max-low. Используется крайне редко.

Чтобы понять разницу между обычным срезом и баяном, рассмотрим пример.

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

arr := [5]int{1, 2, 3}
// [1 2 3 0 0]

s := arr[0:3]
// [1 2 3]

len(s) // 3
cap(s) // 5


Срез s указывает на массив arr. Его длина (length) равна 3, а емкость (capacity, размер массива под срезом) равна 5.

Добавление элемента в срез добавляет его в массив, поскольку емкость это позволяет:

s = append(s, 4)

fmt.Println(arr)
// [1 2 3 4 0]

fmt.Println(s)
// [1 2 3 4]


А вот что будет, если создать срез с помощью баяна:

arr := [5]int{1, 2, 3}
// [1 2 3 0 0]

s := arr[0:3:3]
// [1 2 3]

len(s) // 3
cap(s) // 3


Все как раньше, только емкость среза равна 3. Поэтому добавление элемента в срез приведет к созданию нового массива под срезом. Исходный массив arr не изменится:

s = append(s, 4)

fmt.Println(arr)
// [1 2 3 0 0]

fmt.Println(s)
// [1 2 3 4]


Такие дела. За разновидностями баянов приглашаю в комментарии.

🪗

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

Thank Go!

Пустой срез vs. nil-срез

Как вы знаете, объявленная без инициализации переменная в Go автоматически получает нулевое значение соответствующего типа:

var num int // 0
var str string // ""
var flag bool // false


Для среза нулевое значение — nil:

var snil []int
// []int(nil)


С другой стороны, бывает инициализированный, но пустой срез:

sempty := []int{}
// or
// sempty = make([]int, 0)


Это разные значения, которые не равны между собой:

reflect.DeepEqual(snil, sempty)
// false


И в то же время, пустой срез и nil-срез почти всегда взаимозаменямы:

len(snil) // 0
cap(snil) // 0
snil = append(snil, 1) // []int{1}

len(sempty) // 0
cap(sempty) // 0
sempty = append(sempty, 1) // []int{1}

reflect.DeepEqual(snil, sempty)
// true


Почти всегда ваш код не должен делать разницы между пустым и nil-срезом.

Есть и исключения, конечно. Приглашаю поделиться ими в комментариях.

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

Thank Go!

Создаем 100К горутин, выполняем N

Итак, мы создаем 100 тысяч горутин.

Разумно при этом ограничить количество одновременно выполняющихся горутин, чтобы не пытаться впихнуть их на CPU все разом:

nConc = runtime.NumCPU()*2
sema := make(chan struct{}, nConc)

const n = 100_000
for range n {
go func() {
sema <- struct{}{}
work()
<-sema
}()
}


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

Мы по-прежнему создаем 100К горутин, но теперь каждая из них пытается занять семафор перед выполнением работы. Количество мест в семафоре ограничено (16 мест на 8-ядерном сервере), так что в каждый момент времени только 16 горутин будут выполняться, а остальные будут ждать на семафоре.

Общее время выполнения не увеличится (скорее даже уменьшится), а ресурсы сервера будут использоваться более разумно. Если же мы не хотим занимать CPU на 100% и готовы пожертвовать общим временем, то можно уменьшить nConc.

А если вынести sema <- struct{}{} из горутины в общий цикл, то и создаваться горутины будут не все разом, а постепенно.

Всегда ли оптимален такой подход? Нет. Но об этом в следующий раз.

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

Thank Go!

Горутинки

Допустим, у вас есть сервер с 1Гб памяти. Вы выполняете на нем какие-то задачки, и для каждой задачки создаете горутину. Внутри горутина очень простая, без рекурсий и жирных объектов в памяти:

func work() int {
res := httpGet(url)
return res
}


Сколько одновременно живущих горутин можно создать без проблем для работы сервера? Опрос следует.

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

Thank Go!

Куда идет Go

Стоило ли добавлять в язык range-over-func? На мой взгляд — нет.

Сила Go — в дубовости. У нас уже есть много фичастых языков с большой внутренней сложностью (Java, C#, C++), и Go был единственной простой мейнстрим-альтернативой для них.

Да, Go не модный, и код на нем простынявый. Но он простой до примитивности, и это хорошо.

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

Почему так происходит?

Есть подозрение, что в команде разработки Go заработал стандартный для гугла процесс — promotion-driven development. Развитием языка управляют люди, которым надо демонстрировать выполнение KPI — а значит, будут поливать Go фичами из шланга, превращая в еще одну джаву.

Ждут нас и корутины, и стримы, и паттерн-матчинг.

Штош.

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

Thank Go!

range по функциям

В Go 1.23 (запланирован в августе) появится цикл range по функциям. Проще всего показать на примере.

Допустим, мы написали функцию, которая принимает срез, и возвращает его инвертированную версию, не меняя при этом оригинал:

func Backward[E any](s []E) []E {
r := make([]E, 0, len(s))
for i := len(s) - 1; i >= 0; i-- {
r = append(r, s[i])
}
return r
}


Пример использования:

s := []string{"a", "b", "c"}
for _, el := range Backward(s) {
fmt.Print(el, " ")
}
// c b a


И все бы хорошо, но Backward создает копию исходного среза. Для abc это не проблема, но в целом не слишком-то эффективно с точки зрения использования памяти.

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

С range по функциям это можно сделать. Следите за руками:

func Backward[E any](s []E) func(func(int, E) bool) {
return func(yield func(int, E) bool) {
for i := len(s) - 1; i >= 0; i-- {
if !yield(i, s[i]) {
return
}
}
}
}


Пример использования:

s := []string{"a", "b", "c"}
for _, el := range Backward(s) {
fmt.Print(el, " ")
}
// c b a


Вы можете поинтересоваться, какого черта мы вообще этим занимаемся. Ведь можно сделать обычный цикл, обходящий s в обратном порядке, да и все дела.

Напишу об этом отдельно.

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