thank_go | Unsorted

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

2615

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

Subscribe to a channel

Thank Go!

Опрос: Go-разработка в 2025 году

Ребята из DevCrowd каждый год проводят опрос русскоязычных Go-разработчиков — получается интересный срез профессии, рынка и технологий (вот результаты за 2024 год).

Мы традиционно предлагаем поучаствовать в новом опросе:
https://survey.alchemer.eu/s3/90907937/Go-2025

Заполнение займёт 10–12 минут. Результаты будут в начале ноября, ссылку опубликуем.

Объявление некоммерческое.

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

Thank Go!

Циклические импорты

Йота, конечно, странная штука. Роб Пайк притащил ее из APL (язык программирования из 1960). Но поговорить я хотел не о ней.

Самая полезная из «странностей» Go (на мой взгляд) — это запрет циклических импортов.

Циклический импорт — это, например, когда пакет A зависит от B, B зависит от C, а C зависит от A (ну и любые более сложные комбинации).

A ↴ 
↑ B
C ↲


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

Циклические импорты часто запрещены на уровне менеджера пакетов (например, в Rust так), но внутри проекта все основные языки (кроме Go) их разрешают.

А в Go вот решили запретить: если есть цикл — код не скомпилируется.

И правильно сделали!

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

Thank Go!

Проверить тип generic-параметра

Помните наш числовой тип?

type Number interface {
int | float64
}


В комментариях к прошлой заметке пожаловались, что проверка конкретного типа внутри generic-функции возможна только через приведение к any:

func whoami[T Number](n T) {
switch num := any(n).(type) {
case int:
fmt.Printf("var n int = %d\n", num)
case float64:
fmt.Printf("var n float64 = %f\n", num)
}
}


whoami(3.14)
// var n float64 = 3.140000


Отчего же так?

Классический ответ на этот вопрос: проверять конкретный тип вообще не надо.

Смысл дженериков в том, чтобы единообразно работать с разными типами. Поэтому код в generic-функции не должен менять поведение в зависимости от типа.

Например:

func sumn[T Number](nums ...T) T {
var total T
for _, n := range nums {
total += n
}
return total
}


Сумма — она и есть сумма. И неважно, целое число или действительное.

Однако, как часто бывает с «архитектурно правильными» ответами, этот подход иногда грубо разбивается о суровую реальность. А как именно — посмотрим в следующий раз :)

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

Thank Go!

Кирпичики для конвейеров

Сделал небольшой пакет chans с типовыми операциями над каналами вроде Filter, Map, Split и Merge.

Вот игрушечный пример для демонстрации.

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

docs := make(chan []string, 10)
docs <- []string{"go", "is", "awesome"}
docs <- []string{"cats", "are", "cute"}
close(docs)


Извлекаем слова из документов:

words := make(chan string, 10)
chans.Flatten(ctx, words, docs)
close(words)


И считаем общее количество байт в словах:

step := func(acc int, word string) int { return acc + len(word) }
count := chans.Reduce(ctx, words, 0, step)

fmt.Println("byte count =", count)
// byte count = 22


Подробности тут: https://antonz.org/chans

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

Thank Go!

errgroup.Group

Чтобы закончить тему с ошибками из горутин, рассмотрим еще один инструмент — тип Group из пакета расширенной стдлибы golang.org/x/sync/errgroup.

Это улучшенная и дополненная версия стандартной sync.WaitGroup:

— Запускает функции в отдельных горутинах через Group.Go.
— Каждая функция может вернуть ошибку.
— Если хотя бы одна функция вернула ошибку, ее же вернет и Group.Wait.

wg := new(errgroup.Group)
for i := range n {
wg.Go(func() error {
err := work(i)
return err
})
}
err := wg.Wait()


Поскольку метод Wait дожидается только первой ошибки, Group совершенно не подходит, если нужно собрать все ошибки.

Но если нужна только первая — идеальный вариант.

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

Thank Go!

Получаем ошибку из нескольких горутин

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

var wg sync.WaitGroup
wg.Add(n)

for i := range n {
go func() {
defer wg.Done()
err := work(i)
// ???
}()
}

wg.Wait()
err := // ???


Как это сделать? Есть несколько вариантов.

Если нас интересует любая из ошибок, можно использовать атомик:

var firstErr atomic.Pointer[error]
firstErr.Store(nil)

// ...

go func() {
err := work(i)
if err != nil {
firstErr.CompareAndSwap(nil, &err)
}
}()

// ...

err := firstErr.Load()


Или канал с одиночным буфером:

firstErr := make(chan error, 1)

// ...

go func() {
err := work(i)
if err != nil {
select {
case firstErr <- err:
default:
}
}
}()

// ...

select {
case err := <-firstErr:
// ...
default:
// no errors
}


Если же нас интересуют все ошибки, то тоже есть пара вариантов. О них — в следующей заметке.

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

Thank Go!

Промисы

Шутки шутками, но я реализовал промисы на Go. Взял вот прямо оригинальную спецификацию из джаваскрипта, портировал их тест-сьют на Go, и сделал.

Что могу сказать.

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

Код при этом получился на удивление простым и читаемым. Спасибо отлично продуманным примитивам многозадачности в Go.

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

https://github.com/nalgeon/azor

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

Thank Go!

Выразительные тесты без testify/assert

Если писать тесты, используя только стдлибу, они будут выглядеть как бесконечная череда if + t.Errorf:

db.Str().Set("name", "alice")
age, err := db.Str().Get("age")

if !errors.Is(err, ErrNotFound) {
t.Errorf("want ErrNotFound, got %v", err)
}

if age != nil {
t.Errorf("want nil, got %v", age)
}


Многим разработчикам это не нравится, поэтому они используют ассерты (проверки в тестах) из пакета testify/assert (или testify/require):

assert.ErrorIs(t, err, ErrNotFound)
assert.Nil(t, age)


Проблема с этим пакетом (на мой взгляд) в том, что он огромный. Около 40 ассертов, включая вариации одного и того же вроде Error, EqualError, NoError, ErrorAs, ErrorIs, и совсем уж странные вроде Greater и GreaterOrEqual.

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

Equal проверяет на равенство (с учетом нюансов вроде сравнения time.Time):

got, want := "olleh", "hello"
be.Equal(t, got, want)


Err проверяет любые ситуации с ошибками (есть ошибка, нет ошибки, текст ошибки, значение ошибки, тип ошибки):

err := &fs.PathError{"open", "file.txt", fs.ErrNotExist}

// наличие ошибки
be.Err(t, err)

// текст ошибки
be.Err(t, err, "does not exist")

// значение ошибки
be.Err(t, err, fs.ErrNotExist)


И True для всего остального:

s := "go is awesome"
be.True(t, len(s) > 0)


Можно даже и без True обойтись, но с ней как-то приятнее.

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

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

Thank Go!

Go 1.25: Атрибуты и вывод тестов

Тесты — сильная сторона Go. В 1.25 они станут еще чуточку лучше.

Новый метод T.Attr добавляет дополнительную информацию к тесту. Например, ссылку на задачу, описание тест-кейса или что-то еще, что поможет анализировать результаты тестов:

func TestAttrs(t *testing.T) {
t.Attr("issue", "demo-1234")
t.Attr("description", "Testing for the impossible")

if 21*2 != 42 {
t.Fatal("What in the world happened to math?")
}
}


=== RUN   TestAttrs
=== ATTR TestAttrs issue demo-1234
=== ATTR TestAttrs description Testing for the impossible
--- PASS: TestAttrs (0.00s)


А метод T.Output предоставляет доступ к потоку вывода (io.Writer), который использует тест. Удобно, если вы хотите отправлять логи приложения прямо в лог теста — так их проще читать или автоматически анализировать.

подробности

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

Thank Go!

Go 1.25: Flight recorder

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

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

Новый тип trace.FlightRecorder реализует этот подход в Go.

Сначала настраиваем скользящее окно:

// Сохранять как минимум 5 последних секунд
// трассировки, с размером буфера не более 3 МБ.
cfg := trace.FlightRecorderConfig{
MinAge: 5 * time.Second,
MaxBytes: 3 << 20, // 3MB
}


Затем создаем трассировщик и запускаем его:

rec := trace.NewFlightRecorder(cfg)
rec.Start()
defer rec.Stop()


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

file, _ := os.Create("/tmp/trace.out")
defer file.Close()
n, _ := rec.WriteTo(file)


И смотрим трассировку в браузере командой go tool.

подробности

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

Thank Go!

Go 1.25: Новый сборщик мусора

Некоторые гоферы любят пошутить про Java и ее многочисленные сборщики мусора. Теперь не до шуток — новый GC появится и в Go.

Алгоритм сборки мусора под кодовым названием Green Tea хорошо подходит для программ, которые создают много маленьких объектов и работают на современных машинах с большим количеством ядер.

Старый сборщик мусора сканирует память не по порядку, а скачет туда-сюда. Из-за этого все работает неоптимально, потому что много времени уходит на доступ к памяти. Проблема усугубляется на многоядерных системах с неоднородным доступом к памяти (так называемая NUMA-архитектура, когда у каждого процессора или группы процессоров есть своя «локальная» память).

Green Tea работает иначе. Вместо того чтобы сканировать отдельные маленькие объекты, он сканирует память большими, непрерывными блоками — спанами (spans). Каждый спан содержит много маленьких объектов одного размера. Благодаря работе с большими блоками GC может сканировать память быстрее и лучше использовать кэш процессора.

Результаты бенчмарков разнятся, но команда Go ожидает, что в реальных программах с большим количеством GC затраты на сборку мусора снизятся на 10–40%.

подробности

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

Thank Go!

Go 1.25: JSON v2

Да-да, подвезли вторую версию пакета json. Правда, пока экспериментальную (включается отдельным флагом).

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

Итак:

→ Базовые функции Marshal/Unmarshal остались, но теперь в них можно передавать опции, которые управляют логикой кодирования значений в JSON. Например, пустой срез — это null или []. Опций много, несколько десятков. Их можно комбинировать.

→ Добавились функции MarshalWrite/UnmarshalRead, они работают с io.Writer вместо среза байт.

→ Добавились функции MarshalEncode/UnmarshalDecode, они работают с Encoder/Decoder вместо среза байт. Итого, у нас теперь три пары функций маршалинга-анмаршалинга.

→ Типы Encoder и Decoder переехали в отдельный пакет jsontext, их интерфейсы сильно изменились.

→ Добавились новые теги. Например, format:template задает формат выходного значения, а inline делает «плоский» объект вместо иерархичного.

→ Появилась возможность создавать маршалеры и анмаршалеры «на лету» и передавать их в функции маршалинга/анмаршалинга.

→ Заметно изменились умолчательные правила маршалинга и анмаршалинга. Старые правила можно вернуть через опции.

→ Производительность маршалинга не изменилась, а анмаршалинг ускорился в 3-10 раз.

Как-то так 😅

подробности

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

Thank Go!

Не функция, а builtin

Допустим, у нас есть код, который заполняет срез случайными числами:

s := make([]int, 1024)
for i := range len(s) {
s[i] = rand.IntN(len(s))
}


Думаю, вы в курсе, что len(s) не пересчитывает каждый раз элементы в срезе (медленно), а возвращает свойство среза под названием len (быстро).

Но все же — разумно ли постоянно вызывать функцию len() в цикле? Не лучше ли сохранить длину среза в отдельную переменную? Вот так:

s := make([]int, 1024)
n := len(s)
for i := range n {
s[i] = rand.IntN(n)
}


Нет, не лучше.

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

Не пытайтесь микро-оптимизировать такие вещи. Спокойно используйте len() и cap() в цикле, если нужно.

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

Thank Go!

Синтаксического сахара в обработке ошибок не будет

For the foreseeable future, the Go team will stop pursuing syntactic language changes for error handling.

We will also close all open and incoming proposals that concern themselves primarily with the syntax of error handling, without further investigation.

из блога

Обработка ошибок в Go навсегда надолго останется уродливой и многословной. Ровно такой, какая она и должна быть 😁

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

Thank Go!

Почему RoundTripper?

Почему же DefaultTransport объявлен как интерфейс RoundTripper, а не как конкретный тип *Transport, хотя по факту он содержит именно *Transport?

var DefaultTransport RoundTripper = &Transport{...}


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

➊ Такое объявление явно декларирует, что DefaultTransport соответствует ожиданиям Client — тот ведь хочет видеть именно RoundTripper. Но DefaultTransport и так соответствует RoundTripper — явное указание интерфейса в Go не требуется.

➋ Страховка на будущее. Если *Transport перестанет соответствовать интерфейсу RoundTripper, код не скомпилируется. Строго говоря, и это не обязательно — клиент уже использует DefaultTransport как RoundTripper во внутреннем методе transport.

➌ Объявив DefaultTransport интерфейсом, разработчики стандартной библиотеки оставили себе (теоретическую) возможность заменить в будущем реализацию (условно, сделать вместо Transport новый MagicTransport), не сломав обратную совместимость.

Вот только на практике это не так.

А как — в следующем посте (длинная серия получилась, вы уж простите 🤷‍♀️)

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

Thank Go!

debug.SetMaxThreads

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

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

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

Если таких потоков в итоге получится 10000, программа упадет с фатальной ошибкой thread exhaustion (уверен, вы никогда с ней не сталкивались).

Отрегулировать максимально допустимое количество потоков можно функцией debug.SetMaxThreads:

const nThreads = 5
defaultVal := debug.SetMaxThreads(nThreads)
fmt.Println("Default max threads:", defaultVal)
fmt.Println("Current max threads:", nThreads)


Default max threads: 10000
Current max threads: 5


Делать это, как вы понимаете, почти никогда не надо.

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

Thank Go!

Переключатель типа в дженериках

Все знают, что канал в Go можно обойти через range:

var total int
for v := range ch {
total += v
}


Итератор обходится точно так же:

s := []int{1, 2, 3, 4, 5}
it := slices.Values(s)
var total int
for v := range it {
total += v
}


Если вы великий любитель обобщать, можете попытаться сделать generic-функцию, которая будет единообразно работать с каналами и итераторами.

Определяем общий интерфейс Iterable:

type Iterable[V any] interface {
chan V | iter.Seq[V]
}


И делаем функцию, которая суммирует значения (для простоты только int):

func sumit[It Iterable[int]](it It) int {
var total int
for v := range it {
total += v
}
return total
}


Работать, это, конечно же, не будет 😁

cannot range over it (variable of type It constrained by Iterable[int]): chan int and iter.Seq[int] have different underlying types


Тут-то вам и пригодится переключатель типа:

func sumit[It Iterable[int]](it It) int {
var total int
switch it := any(it).(type) {
case chan int:
for v := range it {
total += v
}
case iter.Seq[int]:
for v := range it {
total += v
}
}
return total
}


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

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

Thank Go!

Интерфейс как объединение типов

Один из студентов обнаружил в курсе такой вот код:

type Number interface {
int | float64
}


И задался закономерным вопросом: ЭТО ЕЩЕ ЧТО ТАКОЕ???

Действительно, обычно в интерфейсе мы указываем список методов. А тут вместо методов — объединение (union) типов (в данном случае — int и float64).

Это легальная конструкция: интерфейс может быть объявлен как объединение типов. Но использовать такое чудо получится только в качестве ограничения для generic-параметра:

// Pow возводит число n в степень exp
// и возвращает результат того же типа, что и n.
func Pow[T Number](n T, exp int) T {
return T(math.Pow(float64(n), float64(exp)))
}


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

func Add(n1, n2 Number) Number {
// ...
}


будет ошибка компиляции: cannot use type Number outside a type constraint: interface contains type constraints.

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

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

Thank Go!

Итерирование по каналу с отменой

Вы, конечно, и так в курсе, но на всякий случай напомню.

Эта замечательная, красивая конструкция:

for v := range in {
select {
case <-ctx.Done():
return
default:
work(ctx, v)
}
}


Не гарантирует выход из цикла при отмене. Если поставщик данных в канал in не закроет его при отмене контекста, есть все шансы зависнуть на очередном чтении из in в v.

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

for {
select {
case <-ctx.Done():
return
case v, ok := <-in:
if !ok {
return
}
work(ctx, v)
}
}


Железобетонно гарантирует, что на чтении из in мы не зависнем.

Лишнее напоминание о том, что об отмене в Go задумались сильно после того, как спроектировали многозадачность.

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

Thank Go!

Собираем все ошибки из горутин

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

Можно использовать канал с буфером на N элементов (по количеству горутин):

errs := make(chan error, n)

// ...

go func() {
err := work(i)
errs <- err
}()

// ...
close(errs)

var errsSlice []error
for err := range errs {
errsSlice = append(errsSlice, err)
}
err := errors.Join(errsSlice...)


errors.Join игнорирует nil, так что в горутине можно спокойно отправлять err в канал, не проверяя на nil.

Или можно сразу использовать срез размером N:

errs := make([]error, n)

// ...

go func() {
err := work(i)
errs[i] = err
}()

// ...

err := errors.Join(errs...)


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

Если каждая горутина работает с собственным элементом среза, и не пытается изменить сам срез (например, через append или [:]), то рантайм гарантирует, что это безопасно.

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

Thank Go!

ErrUnsupported

Сколько работаю с пакетом errors, и только недавно заметил, что в нем определена переменная ErrUnsupported (Go 1.21+).

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

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

func Ping(url string) error {
if !strings.HasPrefix(url, "https://") {
return fmt.Errorf(
"only https scheme is allowed: %w",
errors.ErrUnsupported,
)
}
return doPing(url)
}


Причем в ходе обсуждения пропозала выяснилось, что все понимают смысл "unsupported" по-разному.

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

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

Thank Go!

async/await (несерьезное)

Есть джаваскриптеры в чате? Тут ваши привычные инструменты подвезли:

fn := async(func() any {
return "Hello, JavaScript!"
})
res := await(fn)
fmt.Println(res)
// Hello, JavaScript!


А вот и реализация 😁

func async(f func() any) <-chan any {
out := make(chan any, 1)
go func() {
defer close(out)
out <- f()
}()
return out
}


func await(in <-chan any) any {
return <-in
}


Приходите на курс, мы там еще и промисы делаем!

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

Thank Go!

Тур по Go 1.25

Мы рассмотрели все основные изменения в грядущем релизе.

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

Итак, вот что ждет нас в Go 1.25 уже в августе:

Фейковое время в тестах
JSON v2 beta
GOMAXPROCS для контейнеров
Новый сборщик мусора beta
Защита от CSRF
Go в группе ожидания
Flight recorder
Больше Root-методов
Рефлексивное приведение типа
Атрибуты и вывод тестов
Группировка атрибутов в логах
Клон хеша

все вместе

Неплохой релиз, как по мне!

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

Thank Go!

Go 1.25: Больше Root-методов

Тип os.Root ограничивает работу с файловой системой конкретным каталогом. Теперь он поддерживает несколько новых методов, аналогичных функциям пакета os:

— Chmod меняет права доступа к файлу.
— Chown меняет идентификатор пользователя (uid) и группы (gid) файла.
— Chtimes меняет время последнего доступа и изменения файла.
— Link создает жесткую ссылку на файл.
— MkdirAll создает новый каталог и все родительские каталоги.
— ReadFile читает файл и возвращает его содержимое.
— Readlink возвращает путь, на который указывает символическая ссылка.
— RemoveAll удаляет файл или каталог со всем содержимым.
— Rename переименовывает (перемещает) файл или каталог.
— Symlink создает символическую ссылку на файл.
— WriteFile записывает данные в файл.

Пример:

root, _ := os.OpenRoot("data")
root.Chmod("01.txt", 0600)

finfo, _ := root.Stat("01.txt")
fmt.Println(finfo.Mode().Perm())


-rw-------


Теперь, когда os.Root содержит все основные операции, вам вряд ли понадобятся файловые функции пакета os. Это делает работу с файлами намного безопаснее.

подробности

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

Thank Go!

Go 1.25: Анти-CSRF

Новый тип http.CrossOriginProtection защищает от CSRF-атак, отклоняя небезопасные кросс-доменные запросы из браузера.

Кросс-доменные запросы определяются так:

➀ Проверкой по заголовку Sec-Fetch-Site.

➁ Сравнением домена в заголовке Origin с доменом в заголовке Host.

Вот пример, где мы включаем CrossOriginProtection и явно разрешаем несколько дополнительных источников.

Регистрируем пару обработчиков:

mux := http.NewServeMux()
mux.HandleFunc("GET /get", func(...))
mux.HandleFunc("POST /post", func(...))


Настраиваем защиту от CSRF-атак:

antiCSRF := http.NewCrossOriginProtection()
antiCSRF.AddTrustedOrigin("https://example.com")
antiCSRF.AddTrustedOrigin("https://*.example.com")


Подключаем защиту ко всем обработчикам:

srv := http.Server{
Addr: ":8080",
Handler: antiCSRF.Handler(mux),
}
log.Fatal(srv.ListenAndServe())


Теперь сервер будет автоматически отклонять кросс-доменные запросы:

curl --data "ok" -H "sec-fetch-site:cross-site" localhost:8080/post


cross-origin request detected from Sec-Fetch-Site header


подробности

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

Thank Go!

Go 1.25: GOMAXPROCS для контейнеров

Параметр рантайма GOMAXPROCS определяет максимальное количество потоков операционной системы, которые планировщик Go может использовать для одновременного выполнения горутин.

Начиная с Go 1.5, по умолчанию он равен значению runtime.NumCPU, то есть количеству логических CPU на машине.

Например, на моем ноутбуке с 8 ядрами значение GOMAXPROCS по умолчанию тоже равно 8:

maxProcs := runtime.GOMAXPROCS(0)
fmt.Println("NumCPU:", runtime.NumCPU())
fmt.Println("GOMAXPROCS:", maxProcs)


NumCPU: 8
GOMAXPROCS: 8


Программы на Go часто запускаются в контейнерах под управлением Docker или Kubernetes. В этих системах можно ограничить использование процессора для контейнера с помощью функции Linux, которая называется cgroups.

До версии 1.25 рантайм Go не учитывал ограничение по CPU (CPU-квоту) при установке значения GOMAXPROCS. Как бы вы ни ограничивали ресурсы процессора, GOMAXPROCS всегда устанавливался равным количеству CPU на хосте.

А теперь начал учитывать:

docker run --cpus=4 golang:1.25rc1-alpine go run /app/nproc.go


NumCPU: 8
GOMAXPROCS: 4


Если лимит CPU изменяется, рантайм автоматически обновляет значение GOMAXPROCS. Сейчас это происходит не чаще одного раза в секунду.

подробности

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

Thank Go!

Go 1.25: Синтетическое время в тестах

В августе выходит Go 1.25, так что пришло время небольшого марафона по новым фичам!

Начнем с нашего старого знакомого — пакета synctest. В 1.24 он еще был экспериментальным, а теперь можно полноценно использовать.

Пакет пригодится в тестах, где используются time.Sleep, time.After, time.Timer и другие «ждуны». Он предоставляет искусственные часы, которые автоматически идут вперед всякий раз, когда тестируемая горутина заблокирована.

Например, чтобы протестировать такую функцию:

func Slowpoke() int {
start := time.Now()
time.Sleep(30 * time.Second)
fmt.Println("Finished after", time.Since(start))
return 42
}


Не нужно ждать 30 секунд. Заворачиваем в synctest.Test():

func TestSlowpoke(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
res := Slowpoke()
if res != 42 {
t.Fatalf("expected 42, got %d", res)
}
})
}


И тест проходит моментально:

=== RUN   TestSlowpoke
Finished after 30s
--- PASS: TestSlowpoke (0.00s)


подробности

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

Thank Go!

Go 1.25: WaitGroup.Go

Путь длиной в 13 лет от

var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("Hello, World!")
}()
wg.Wait()


до

var wg sync.WaitGroup
wg.Go(func() {
fmt.Println("Hello, World!")
})
wg.Wait()


Премьера уже в августе!

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

Thank Go!

Протекший транспорт

DefaultTransport — классический пример протекшей абстракции.

На практике во многих проектах DefaultTransport приводят к *Transport, чтобы настроить транспорт. Причем делают это без проверки типа (такой пример есть даже в документации стдлибы):

t := http.DefaultTransport.(*http.Transport).Clone()
// теперь можно менять свойства транспорта
t.TLSHandshakeTimeout = time.Second
t.DisableKeepAlives = true


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

На мой взгляд, объявление DefaultTransport как RoundTripper — неудачное решение. Лучше бы он был *http.Transport — не пришлось бы замусоривать код приведением типа.

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

var _ RoundTripper = (*Transport)(nil)


Такие дела.

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

Thank Go!

http.RoundTripper

Итак, переменная http.DefaultTransport объявлена как интерфейс RoundTripper, хотя по факту содержит значение *Transport:

var DefaultTransport RoundTripper = &Transport{...}


RoundTripper — это интерфейс с единственным методом RoundTrip (выполнить запрос и вернуть ответ):

type RoundTripper interface {
RoundTrip(*Request) (*Response, error)
}


Тип http.Client делегирует фактическое выполнение запроса своему свойству Transport, которое как раз имеет тип RoundTripper:

type Client struct {
Transport RoundTripper
// ...
}


Это логично и удобно. Чтобы заменить механизм выполнения запросов, достаточно реализовать в собственном типе единственный метод RoundTrip:

type DummyTransport struct{}

func (t *DummyTransport) RoundTrip(*http.Request) (*http.Response, error) {
return nil, errors.New("not implemented")
}


И клиент будет его использовать:

c := &http.Client{}
c.Transport = &DummyTransport{}
resp, err := c.Get("http://example.com")
fmt.Println(err)
// Get "http://example.com": not implemented


Но все же, почему DefaultTransport объявлен как RoundTripper? Можно было спокойно объявить его как *Transport и использовать везде, где нужен RoundTripper.

Ведь *Transport соответствует интерфейсу RoundTripper, а для Go этого достаточно.

Разберем эту загадку в следующем посте (я это уже говорил, но... 😅)

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