2615
Здравый взгляд на язык программирования Go. Злой админ @nalgeon. Добрый админ @mikeberezin. Рекламы нет.
Опрос: Go-разработка в 2025 году
Ребята из DevCrowd каждый год проводят опрос русскоязычных Go-разработчиков — получается интересный срез профессии, рынка и технологий (вот результаты за 2024 год).
Мы традиционно предлагаем поучаствовать в новом опросе:
https://survey.alchemer.eu/s3/90907937/Go-2025
Заполнение займёт 10–12 минут. Результаты будут в начале ноября, ссылку опубликуем.
Объявление некоммерческое.
Циклические импорты
Йота, конечно, странная штука. Роб Пайк притащил ее из APL (язык программирования из 1960). Но поговорить я хотел не о ней.
Самая полезная из «странностей» Go (на мой взгляд) — это запрет циклических импортов.
Циклический импорт — это, например, когда пакет A зависит от B, B зависит от C, а C зависит от A (ну и любые более сложные комбинации).
A ↴
↑ B
C ↲
Проверить тип generic-параметра
Помните наш числовой тип?
type Number interface {
int | float64
}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
func sumn[T Number](nums ...T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
Кирпичики для конвейеров
Сделал небольшой пакет 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
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()
Получаем ошибку из нескольких горутин
Допустим, мы запускаем несколько горутин, причем в каждой может произойти ошибка. Если случилась хотя бы одна ошибка, мы хотим ее отловить:
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
}
Промисы
Шутки шутками, но я реализовал промисы на Go. Взял вот прямо оригинальную спецификацию из джаваскрипта, портировал их тест-сьют на Go, и сделал.
Что могу сказать.
Я конечно и раньше подозревал, что промисы — очень плохая модель многозадачности. Теперь же могу сказать об этом со всей уверенностью. Жуткая просто.
Код при этом получился на удивление простым и читаемым. Спасибо отлично продуманным примитивам многозадачности в Go.
В общем, если тема многозадачности вам интересна, почитать исходники может быть любопытно. Главное, не используйте это :)
https://github.com/nalgeon/azor
Выразительные тесты без 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)
}assert.ErrorIs(t, err, ErrNotFound)
assert.Nil(t, age)
got, want := "olleh", "hello"
be.Equal(t, got, want)
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)s := "go is awesome"
be.True(t, len(s) > 0)
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)
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.
Go 1.25: Новый сборщик мусора
Некоторые гоферы любят пошутить про Java и ее многочисленные сборщики мусора. Теперь не до шуток — новый GC появится и в Go.
Алгоритм сборки мусора под кодовым названием Green Tea хорошо подходит для программ, которые создают много маленьких объектов и работают на современных машинах с большим количеством ядер.
Старый сборщик мусора сканирует память не по порядку, а скачет туда-сюда. Из-за этого все работает неоптимально, потому что много времени уходит на доступ к памяти. Проблема усугубляется на многоядерных системах с неоднородным доступом к памяти (так называемая NUMA-архитектура, когда у каждого процессора или группы процессоров есть своя «локальная» память).
Green Tea работает иначе. Вместо того чтобы сканировать отдельные маленькие объекты, он сканирует память большими, непрерывными блоками — спанами (spans). Каждый спан содержит много маленьких объектов одного размера. Благодаря работе с большими блоками GC может сканировать память быстрее и лучше использовать кэш процессора.
Результаты бенчмарков разнятся, но команда Go ожидает, что в реальных программах с большим количеством GC затраты на сборку мусора снизятся на 10–40%.
подробности
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 раз.
Как-то так 😅
подробности
Не функция, а 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() в цикле, если нужно.
Читать полностью…
Синтаксического сахара в обработке ошибок не будет
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 навсегда надолго останется уродливой и многословной. Ровно такой, какая она и должна быть 😁
Почему RoundTripper?
Почему же DefaultTransport объявлен как интерфейс RoundTripper, а не как конкретный тип *Transport, хотя по факту он содержит именно *Transport?
var DefaultTransport RoundTripper = &Transport{...}DefaultTransport соответствует ожиданиям Client — тот ведь хочет видеть именно RoundTripper. Но DefaultTransport и так соответствует RoundTripper — явное указание интерфейса в Go не требуется.*Transport перестанет соответствовать интерфейсу RoundTripper, код не скомпилируется. Строго говоря, и это не обязательно — клиент уже использует DefaultTransport как RoundTripper во внутреннем методе transport.DefaultTransport интерфейсом, разработчики стандартной библиотеки оставили себе (теоретическую) возможность заменить в будущем реализацию (условно, сделать вместо Transport новый MagicTransport), не сломав обратную совместимость.
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
Переключатель типа в дженериках
Все знают, что канал в 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
}type Iterable[V any] interface {
chan V | iter.Seq[V]
}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
}
Интерфейс как объединение типов
Один из студентов обнаружил в курсе такой вот код:
type Number interface {
int | float64
}// 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 {
// ...
}
Итерирование по каналу с отменой
Вы, конечно, и так в курсе, но на всякий случай напомню.
Эта замечательная, красивая конструкция:
for v := range in {
select {
case <-ctx.Done():
return
default:
work(ctx, v)
}
}for {
select {
case <-ctx.Done():
return
case v, ok := <-in:
if !ok {
return
}
work(ctx, v)
}
}
Собираем все ошибки из горутин
Допустим, мы запускаем несколько горутин, причем в каждой может произойти ошибка. Если случились ошибки, мы хотим отловить их все.
Можно использовать канал с буфером на 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...)
errs := make([]error, n)
// ...
go func() {
err := work(i)
errs[i] = err
}()
// ...
err := errors.Join(errs...)
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)
}
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
}
Тур по Go 1.25
Мы рассмотрели все основные изменения в грядущем релизе.
Есть еще несколько интересных штук, но они довольно нишевые. Так что я не буду публиковать их отдельными постами, а дам ссылку на большую статью с интерактивными примерами — загляните, если интересно.
Итак, вот что ждет нас в Go 1.25 уже в августе:
— Фейковое время в тестах
— JSON v2 beta
— GOMAXPROCS для контейнеров
— Новый сборщик мусора beta
— Защита от CSRF
— Go в группе ожидания
— Flight recorder
— Больше Root-методов
— Рефлексивное приведение типа
— Атрибуты и вывод тестов
— Группировка атрибутов в логах
— Клон хеша
все вместе
Неплохой релиз, как по мне!
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-------
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(...))
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
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
docker run --cpus=4 golang:1.25rc1-alpine go run /app/nproc.go
NumCPU: 8
GOMAXPROCS: 4
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
}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)
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()
Протекший транспорт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)
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 implementedDefaultTransport объявлен как RoundTripper? Можно было спокойно объявить его как *Transport и использовать везде, где нужен RoundTripper.*Transport соответствует интерфейсу RoundTripper, а для Go этого достаточно.