thank_go | Unsorted

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

2615

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

Subscribe to a channel

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!

http.DefaultTransport

Выражение http.DefaultTransport.(*http.Transport) демонстрирует сразу несколько не самых часто применяемых вещей в Go. Давайте их разберем.

Думаю, по поводу http ни у кого не возникло сомнений — это пакет net/http. Чаще всего в пакетах встречаются:

— Функции (например, http.StatusText() возвращает расшифровку числового HTTP-статуса).
— Типы (например, http.Client умеет делать HTTP-запросы).
— Константы (например, http.StatusOK = 200, статус упешного ответа).

Реже пакеты экспортируют переменные. Как правило, это ошибки (например, http.ErrNotSupported). Но иногда это просто значение (или указатель) на структуру.

http.DefaultTransport как раз такая переменная. Грубо говоря, это реализация HTTP-протокола («транспорт») с набором настроек по умолчанию.

Поскольку транспорт в пакете http представлен типом Transport, логично было бы определить DefaultTransport так:

var DefaultTransport = &Transport{
// настройки по умолчанию
}


Но на самом деле он выглядит так:

var DefaultTransport RoundTripper = &Transport{
// настройки по умолчанию
}


Хотя де-факто DefaultTransport — это указатель на значение Transport, объявлен он как значение интерфейса RoundTripper. Разберем эту загадку в следующем посте.

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

Thank Go!

Как сделать бип

Поднимать серьезные темы сразу после праздников было бы странно, поэтому сегодня мы поговорим о том, как дзынькать из Go.

Вот так (готов поспорить, что проще программы вы не видели):

print("\a")


Делает дзынь во многих терминалах (вроде даже в cmd.exe в windows).

Живите теперь с этим.

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

Thank Go!

if-err

Есть три школы написания Go-кода.

Наглядная:

err := doSmth()
if err != nil {
return err
}


Краткая:

if err := doSmth(); err != nil {
return err
}


Беспечная:

doSmth()


К какой относитесь вы?

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

А беспечную люблю использовать в одноразовых скриптах 😈

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

Thank Go!

UUIDv7

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

Они бывают разных версий, самая популярная из которых — UUIDv4. Обычно, когда говорят о uuid, имеют в виду именно ее. Но не так давно в стандарт добавили новую версию — UUIDv7.

В отличие от v4, UUIDv7 объединяет таймштамп и случайную часть, поэтому айдишники упорядочены по времени с точностью до 1 миллисекунды. Отлично подходит для идентификаторов записей в базах данных, в том числе распределенных.

Вот как выглядит UUIDv7 в виде строки:

0190163d-8694-739b-aea5-966c26f8ad91
└─timestamp─┘ │└─┤ │└───rand_b─────┘
ver │var
rand_a


128-битное значение складывается из нескольких частей:

— timestamp (48 бит) — юниксовое время в мс.
— ver (4 бит) — версия UUID (7).
— rand_a (12 бит) — случайные биты.
— var (2 бита) — равно 10.
— rand_b (62 bits) — случайные биты.

Интересно, что весь алгоритм UUIDv7 реализуется в пару десятков строк кода на Go. Рассмотрим их следующим постом.

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

Thank Go!

Go 1.24: Случайный текст

Небольшое, но весьма приятное дополнение стандартной библиотеки.

Появилась функция crypto/rand.Text, которая возвращает криптографически случайную строку:

text := rand.Text()
fmt.Println(text)
// 4PJOOV7PVL3HTPQCD5Z3IYS5TC


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

Использует алфавит Base32.

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

Thank Go!

Странный пингер

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

type Server struct {
nPings int
}

func (s *Server) Ping() {
s.nPings++
}


Давайте попингуем его вот так:

s := Server{}
ping := s.Ping // хм

ping()
ping()
ping()


Как думаете, что произойдет?

Опрос следует (чур в комментах не спойлерить)

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

Thank Go!

Go 1.24: SHA-3 и его друзья

Новый пакет crypto/sha3 реализует хеш-функцию SHA-3 и другую криптографическую хурму, про которую вам вряд ли интересно знать (смотрите FIPS 202 если вдруг интересно):

s := []byte("go is awesome")
fmt.Printf("Source: %s\n", s)
fmt.Printf("SHA3-224: %x\n", sha3.Sum224(s))
fmt.Printf("SHA3-256: %x\n", sha3.Sum256(s))
fmt.Printf("SHA3-384: %x\n", sha3.Sum384(s))
fmt.Printf("SHA3-512: %x\n", sha3.Sum512(s))


Source: go is awesome
SHA3-224: 6df...94a
SHA3-256: ece...5c4
SHA3-384: c7b...47c
SHA3-512: 9d4...c5e


И еще несколько crypto-пакетиков:

crypto/hkdf по RFC 5869
crypto/pbkdf2 по RFC 8018
crypto/meme для генерации мем-коинов

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

Thank Go!

Go 1.24: Заглушить логи

Мы уже как-то обсуждали несложный способ создать бесшумный логгер (например, для тестов) — использовать slog.TextHandler в связке с io.Discard:

log := slog.New(
slog.NewTextHandler(io.Discard, nil),
)
log.Info("Prints nothing")


А в Go 1.24 (кстати, он уже вышел) появился еще более простой способ — через slog.DiscardHandler:

log := slog.New(slog.DiscardHandler)
log.Info("Prints nothing")


Вроде и так не особо сложно было, ну да ладно :)

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

Thank Go!

Go 1.24: Синтетическое время для тестов

Пусть у нас есть функция, которая таймаутит, если не получила значение из канала в течение минуты:

func Read(in chan int) (int, error) {
select {
case v := <-in:
return v, nil
case <-time.After(60 * time.Second):
return 0, errors.New("timeout")
}
}


Используем так:

ch := make(chan int)
go func() {
ch <- 42
}()
val, err := Read(ch)
fmt.Printf("val=%v, err=%v\n", val, err)
// val=42, err=<nil>


Как бы теперь протестировать ситуацию с таймаутом? Нежелательно, чтобы тест действительно ждал 60 секунд. Можно сделать таймаут параметром функции (и стоило бы, наверно). Но допустим, что это не вариант.

Новый пакет testing/synctest спешит на помощь! Функция synctest.Run выполняет изолированный "пузырь" в отдельной горутине. Внутри пузыря функции пакета time используют искусственные часы, что позволяет тесту пройти мгновенно:

func TestReadTimeout(t *testing.T) {
synctest.Run(func() {
ch := make(chan int)
_, err := Read(ch)
if err == nil {
t.Fatal("expected timeout error, got nil")
}
})
}


Горутины в пузыре используют синтетическую реализацию времени (точка отсчета - полночь UTC 2000-01-01). Время идет вперед, когда все горутины в пузыре заблокированы.

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

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

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

Thank Go!

Go 1.24: os.Root

Новый тип os.Root ограничивает операции с файловой системой определенной директорией.

Функция OpenRoot открывает директорию и возвращает Root:

dir, err := os.OpenRoot("data")
fmt.Println(dir.Name(), err)
// data <nil>


Методы Root работают внутри директории и не позволяют использовать пути за ее пределами:

file, err := dir.Open("01.txt")
fmt.Println(file.Name(), err)
// data/01.txt <nil>

file, err = dir.Open("../main.txt")
fmt.Println(err)
// openat ../main.txt: path escapes from parent


Методы Root поддерживают большинство операций с файловой системой, доступных в пакете os:

file, err := dir.Create("new.txt")
stat, err := dir.Stat("02.txt")
err = dir.Remove("03.txt")


Поработав с Root, не забудьте положить на место его закрыть:

dir, err := os.OpenRoot(path)
defer dir.Close()
// do stuff


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

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

Thank Go!

Go 1.24: Конкурентно-безопасная карта

Лирическое отступление. Обычная карта в Go никогда не скукоживается, только растет пока не поглотит вселенную. GC не освобождает память, занятую самой картой, даже если удалять из нее элементы. Не изменилось это и в новой «швейцарской» карте в Go 1.24.

Но есть в Go еще одна карта, конкурентно-безопасная (sync.Map). И по странному стечению обстоятельств, в Go 1.24 у нее тоже новая реализация! Теперь она основана на concurrent hash-trie (помесь хэш-таблицы и префиксного дерева) и работает быстрее, особенно при модификациях карты.

Кроме того, новая sync.Map лучше освобождает память, чем предыдущая. Та тоже умела это делать, но там использовалась «поколенческая» модель, и память собиралась с запаздыванем. В новой никаких поколений нет, и память освобождается по мере удаления элементов.

Исходно новую расчудесную карту сделали для пакета unique в Go 1.23 — там как раз нужен был конкурентно-безопасный кэш. А теперь заметили, что и для пакета sync новая реализация отлично подходит. В результате sync.Map теперь по сути фасад к HashTrieMap.

Если вы страшный ретроград, вернуться к старой sync.Map можно через переменную GOEXPERIMENT=nosynchashtriemap при сборке.

Карточный релиз какой-то получается!

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

Thank Go!

Go 1.24: Улучшенная очистка

Это последняя заметка с жестью, дальше пойдут более человеколюбивые фичи, чесслово :)

Помните наш блоб?

b := newBlob(1000)
fmt.Printf("b=%v, type=%T\n", b, b)


b=Blob(1000 KB), type=*main.Blob


Что если мы хотим запустить некоторую функцию очистки (cleanup function), когда объект будет собран сборщиком мусора?

Раньше для этого мы бы вызывали runtime.SetFinalizer, который сложно использовать. Теперь есть его улучшенная версия — runtime.AddCleanup:

  func main() {
b := newBlob(1000)
now := time.Now()
// Регистрируем функцию, которую рантайм
// вызовет после сборки памяти объекта b.
runtime.AddCleanup(b, cleanup, now)

time.Sleep(10 * time.Millisecond)
b = nil
runtime.GC()
time.Sleep(10 * time.Millisecond)
}


func cleanup(created time.Time) {
fmt.Printf(
"object is cleaned up! lifetime = %dms\n",
time.Since(created)/time.Millisecond,
)
}


object is cleaned up! lifetime = 10ms


AddCleanup прикрепляет функцию очистки к объекту. Она выполняется после того как объект становится недоступен (на него больше никто не ссылается).

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

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

Более полный пример, который демонстрирует совместную работу слабых указателей и AddCleanup — в песочнице.

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

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 этого достаточно.

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

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

Thank Go!

Проверяем интернет

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

http://google.com/generate_204


Доступность сервисов гугла на него не влияет, так что урл отвалится только если гугл ляжет целиком (ну или если его забанит самизнаетекто).

Пример проверки:

// isOnline проверяет, есть ли интернет-соединение.
func isOnline(ctx context.Context) bool {
const url = "http://google.com/generate_204"

req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return false
}

resp, err := http.DefaultClient.Do(req)
if err != nil {
return false
}

defer resp.Body.Close()
return resp.StatusCode == http.StatusNoContent
}


Урл даже доступен по http, чтобы не тратить время на установку шифрованного соединения. Но и по https тоже можно вызвать, конечно.

Хорошо придумали.

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

Thank Go!

Возведение в степень

В Go нет оператора для возведения в степень. Я не знаю, почему.

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

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

// 3 мегабайта в байтах (3*2²⁰)
three_mb := 3 << 20
fmt.Println(three_mb)
// 3145728


И все же непонятно, почему нельзя было добавить нормальный оператор x ** y. Йота им, значит, язык не усложнила, а вот оператор степени ну прям никак.

Поэтому живем с такими вот костыликами:

// pow рассчитывает x в степени y.
func pow[T int | float64](x, y T) T {
res := math.Pow(float64(x), float64(y))
return T(res)
}

fmt.Println(3 * pow(2, 20))
// 3145728


Мда.

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

Thank Go!

UUIDv7 на Go

Давайте реализуем алгоритм генерации значения UUIDv7 с нуля, средствами стандартной библиотеки.

Генерим 16 случайных байт (128 бит):

var val [16]byte
_, err := rand.Read(val[:])


Получаем текущее время в миллисекундах и записываем его в значение UUID:

ts := time.Now().UnixMilli()
val[0] = byte(ts >> 40)
val[1] = byte(ts >> 32)
val[2] = byte(ts >> 24)
val[3] = byte(ts >> 16)
val[4] = byte(ts >> 8)
val[5] = byte(ts)


Наконец, заполняем версию и вариант:

val[6] = (val[6] & 0x0F) | 0x70
val[8] = (val[8] & 0x3F) | 0x80


Вот и все, uuid готов!

песочница

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

Thank Go!

(воскресное несерьезное)

Ты: хм, какой бы перспективный язык выучить? Питон? Котлин? Может Сишарп?

Вселенная:

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

Thank Go!

Метод-значение

Это вы конечно молодцы, что так хорошо знаете возможности языка!

Действительно, метод у значения структуры (или указателя на значение) — это просто функция с конкретным получателем (тем самым значением структуры). Такой метод-значение (method value) можно использовать как обычную функцию — вызывать напрямую или передавать в качестве параметра, например.

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

Монитору совершенно не нужно знать о конкретной реализации сервиса. Достаточно получить в конструкторе функцию-пинг, и дальше вызывать ее:

type Monitor struct {
ping func() error
// ...
}

func NewMonitor(ping func() error) *Monitor {
return &Monitor{ping}
}


И если у нашего сервера есть подходящий по сигнатуре метод:

func (s *Server) Ping() error {
// ...
}


То можно прямо его и передать без всяких оберток:

s := Server{}
m := NewMonitor(s.Ping)


Такие вот элементы функционального программирования в глубоко процедурном языке :)

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

Thank Go!

Go 1.24: пропуск нулевых значений в JSON

Новая опция omitzero инструктирует JSON-маршалер пропускать нулевые значения.

Вообще у нас уже был для этого omitempty, но omitzero вроде как поудобнее будет. Например, он пропускает нулевые значения time.Time, чего omitempty делать не умеет.

Вот omitempty:

type Person struct {
Name string `json:"name"`
BirthDate time.Time `json:"birth_date,omitempty"`
}

alice := Person{Name: "Alice"}
b, err := json.Marshal(alice)
fmt.Println(string(b))


{"name":"Alice","birth_date":"0001-01-01T00:00:00Z"}


А вот omitzero:

type Person struct {
Name string `json:"name"`
BirthDate time.Time `json:"birth_date,omitzero"`
}

alice := Person{Name: "Alice"}
b, err := json.Marshal(alice)
fmt.Println(string(b))


{"name":"Alice"}


Если у типа есть метод IsZero() bool — именно он используется при маршалинге, чтобы определить, нулевое значение или нет.

Если метода нет, используется стандартное понятие нулевого значения (0 для целого, "" для строки, и так далее).

Наверно, стоило бы сразу задизайнить так omitempty, но кто же знал :)

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

Thank Go!

Go 1.24: Больше итераторов

Как известно, в 1.23 авторы Go воспылали необъяснимой стратью к итератором, и этот пожар с тех пор только разгорается жарче.

Вот притащили еще горстку в пакете strings.

Lines итерирует по строкам, разделенным \n:

s := "one\ntwo\nsix"
for line := range strings.Lines(s) {
fmt.Print(line)
}

// one
// two
// six


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

s := "one-two-six"
for part := range strings.SplitSeq(s, "-") {
fmt.Println(part)
}
// one
// two
// six


SplitAfterSeq как SplitSeq, но делит после разделителя:

s := "one-two-six"
for part := range strings.SplitAfterSeq(s, "-") {
fmt.Println(part)
}
// one-
// two-
// six


FieldsSeq итерирует по частям, разделенным пробельными символами (unicode.IsSpace) и их последовательностями:

s := "one two\nsix"
for part := range strings.FieldsSeq(s) {
fmt.Println(part)
}
// one
// two
// six


FieldsFuncSeq как FieldsSeq, но логику «пробельных» символов определяете вы сами:

f := func(c rune) bool {
return !unicode.IsLetter(c) && !unicode.IsNumber(c)
}

s := "one,two;six..."
for part := range strings.FieldsFuncSeq(s, f) {
fmt.Println(part)
}

// one
// two
// six


Ровно такие же итераторы добавили в пакет bytes.

Редакция Thank Go увлечение итераторами решительно осуждает 😑

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

Thank Go!

Go 1.24: Контекст для тестов

Допустим, мы хотим протестировать этот жутко полезный сервер:

// Дает ответы на все вопросы.
type Server struct{}

// Возвращает ответ сервера.
func (s *Server) Get(query string) int {
return 42
}

// Запускает сервер. Остановка через отмену контекста.
func startServer(ctx context.Context) *Server {
go func() {
select {
case <-ctx.Done():
// Освобождаем ресурсы.
}
}()
return &Server{}
}


Вот чудесный тест, который я написал:

func Test(t *testing.T) {
srv := startServer(context.Background())
if srv.Get("how much?") != 42 {
t.Fatal("unexpected value")
}
}


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

Исправить это несложно — достаточно использовать контекст с отменой:

func Test(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

srv := startServer(ctx)
if srv.Get("how much?") != 42 {
t.Fatal("unexpected value")
}
}


Но практика показывает, что люди часто забывают это делать 🤷‍♀️

Поэтому в Go 1.24 добавили метод T.Context. Он возвращает контекст, который автоматически отменяется перед тем, как тест завершится:

func Test(t *testing.T) {
srv := startServer(t.Context())
if srv.Get("how much?") != 42 {
t.Fatal("unexpected value")
}
}


Удобно!

P.S. С утечкой ресурсов в тестах есть еще одна неочевидная проблема, но здесь уже нет места ее разбирать. Если интересно, читайте в блоге.

P.P.S. А еще в тесты добавили метод Chdir, который меняет рабочую директорию на время теста. Пример тоже в блоге.

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

Thank Go!

Go 1.24: B.Loop

Вы наверняка знакомы с циклом в бенчмарках (for range b.N):

var sink int

func BenchmarkSlicesMax(b *testing.B) {
// Setup the benchmark.
s := randomSlice(10_000)
b.ResetTimer()

// Run the benchmark.
for range b.N {
sink = slices.Max(s)
}
}


Go сам управляет бенчмарком, определяет разумное значение b.N, и пишет результаты. Это удобно.

Но есть и нюансы:

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

— Чтобы сетап не повлиял на результат, приходится вызывать b.ResetTimer().

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

Go 1.24 предлагает кое-что получше — testing.B.Loop:

func BenchmarkSlicesMax(b *testing.B) {
// Setup the benchmark.
s := randomSlice(10_000)

// Run the benchmark.
for b.Loop() {
slices.Max(s)
}
}


b.Loop решает все проблемы b.N:

— Функция бенчмарка выполняется один раз, поэтому и сетап тоже выполняется только однажды.

— Все, что находится вне b.Loop, не влияет на результат, поэтому b.ResetTimer() не нужен.

— Компилятор никогда не оптимизирует вызовы функций внутри b.Loop.

В общем, с выходом Go 1.24 больше нет причин использовать for range b.N. Переходим на b.Loop!

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

Thank Go!

Скукоживание карт в Go

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

Характерный комментарий:

Если есть сервер на го с сессиями которые реализованы в виде мапы, то даже после отключения клиентов и удаления ключей из нее память не будет освобождаться?🤌

Штош. Давайте разбираться.

Вот наш клиент с идентификатором и телом в 40 байт:

type Client struct {
id uint64
body [40]byte
}


Создаем карту, добавляем 10К клиентов:

printAlloc("initial")

m := make(map[int]Client)
for i := range 10000 {
m[i] = Client{id: uint64(i)}
}

runtime.GC()
printAlloc("after create")


initial: heap size = 109 KB
after create: heap size = 1110 KB


Размер кучи вырос до 1100 KB. Удаляем все записи из карты:

for i := range 10000 {
delete(m, i)
}

runtime.GC()
printAlloc("after delete")


after delete: heap size = 1110 KB


Ни байтика не отдала, зараза!

Попробуем хранить указатели вместо значений:

m := make(map[int]*Client)
for i := range 10000 {
m[i] = &Client{id: uint64(i)}
}

for i := range 10000 {
delete(m, i)
}


after create: heap size = 898 KB
after delete: heap size = 429 KB


Почему часть памяти освободилась?

Здесь в памяти хранятся только указатели на клиентов, а сами значения (48B каждое) хранятся вне карты. Поэтому клиентов GC спокойно спокойно освобождает (ссылок-то на них больше нет), а вот внутренние структуры карты по-прежнему занимают память.

Напоследок предположим, что вместо легкого клиента у нас толстенький боди-позитивный с телом на 1024B:

type Client struct {
id uint64
body [1024]byte
}


m := make(map[int]Client)
for i := range 10000 {
m[i] = Client{id: uint64(i)}
}

for i := range 10000 {
delete(m, i)
}


after create: heap size = 11683 KB
after delete: heap size = 434 KB


Что за ерунда? Мы же используем значения, а не указатели, почему память освободилась?

Если значения в карте достаточно большие (больше 128B) Go автоматически хранит в карте не сами значения, а указатели на них. Поэтому после GC занятая клиентами память освободилась, и осталась только занятая самой картой память размером 400KB.

песочница (можете поменять версию на dev, и убедиться, что в Go 1.24 ничего не изменилось)

Такие дела.

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

Thank Go!

Go 1.24: Швейцарские таблицы 🇨🇭

Спустя много лет команда Go решила изменить реализацию map!

Теперь она основана на SwissTable и предлагает несколько оптимизаций (со слов разработчиков, лично не проверял):

— Чтение и запись в больших картах (>1024 записей) быстрее на ~30%.
— Запись в аллоцированных картах (с установленной емкостью) быстрее на ~35%.
— Итерация в целом быстрее на ~10%, для карт с низкой наполненностью (большая емкость, мало записей) на ~60%.

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

Если интересно, вот исходники.

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

Thank Go!

Слабые указатели

Если вы сильный программист, то не читайте дальше

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

Предположим, у нас есть тип blob (реализация скрыта для краткости):

// Blob is a large byte slice.
type Blob []byte


И указатель на блоб размером в 1000 КБ:

b := newBlob(1000)


Мы можем создать слабый указатель (weak.Pointer) из обычного с помощью weak.Make. А получить доступ к оригинальному указателю поможет Pointer.Value:

wb := weak.Make(newBlob(1000))
fmt.Println(wb.Value())
// Blob(1000 KB)


Обычный указатель не позволит сборщику мусора освободить занятую объектом память:

b := newBlob(1000)
fmt.Println("before GC =", b)
runtime.GC()
fmt.Println("after GC =", b)


before GC = Blob(1000 KB)
after GC = Blob(1000 KB)


Слабый указатель же разрешает сборщику мусора освободить память:

wb := weak.Make(newBlob(1000))
fmt.Println("before GC =", wb.Value())
runtime.GC()
fmt.Println("after GC =", wb.Value())


before GC = Blob(1000 KB)
after GC = <nil>


Как видите, Pointer.Value возвращает nil, если сборщик мусора уже освободил значение по указателю.

Пр этом нет гарантии, что nil вернется сразу после того, как объект перестал использоваться (или в любое другое время позже). Рантайм сам решает, когда освобождать память, и освобождать ли вообще.

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

Мы еще поговорим об этом в следующий раз.

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