Антон ЖияновGo, SQL и разработка софтаhttps://antonz.ru/https://antonz.ru/assets/favicon/favicon.pngАнтон Жияновhttps://antonz.ru/Hugo -- gohugo.ioru-ruSat, 20 Dec 2025 16:00:00 +0000Go-фича: Обновленный go fixhttps://antonz.ru/accepted/modernized-go-fix/Sat, 20 Dec 2025 16:00:00 +0000https://antonz.ru/accepted/modernized-go-fix/С новыми анализаторами и движком от go vet.Часть серии Принято! В ней простыми словами объясняются новые фичи Go.

Обновлённая команда go fix использует новый набор анализаторов и ту же инфраструктуру, что и go vet.

Версия. 1.26 • Тулинг • Полезно

Что

Команда go fix теперь работает на основе analysis framework — это тот же движок, что использует go vet.

Хотя теперь go fix и go vet используют одну и ту же инфраструктуру, у них разные задачи и разные наборы анализаторов:

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

Полный список анализаторов go fix — в разделе Анализаторы.

Зачем

Главная цель — добавить инструменты модернизации кода из Go language server (gopls) в командную строку. Если в go fix появится набор фиксов modernize, разработчики смогут быстро и безопасно обновлять одной командой весь код после выхода новой версии Go.

Переосмысление go fix также упрощает саму экосистему Go. Теперь go fix и go vet используют одну и ту же базу и расширения. Это делает инструменты более понятными, удобными для поддержки и гибкими для тех, кто хочет подключать собственные анализаторы.

Как

Новая команда go fix:

usage: go fix [build flags] [-fixtool prog] [fix flags] [packages]

Fix runs the Go fix tool (cmd/fix) on the named packages
and applies suggested fixes.

It supports these flags:

  -diff
        instead of applying each fix, print the patch as a unified diff

The -fixtool=prog flag selects a different analysis tool with
alternative or additional fixers.

По умолчанию go fix запускает полный набор анализаторов (см. список ниже). Чтобы выбрать конкретные анализаторы, используйте флаг -NAME для каждого из них, или -NAME=false, чтобы запустить все анализаторы, кроме тех, которые вы отключили.

Например, здесь включен только анализатор forvar:

go fix -forvar .

А здесь включены все анализаторы кроме omitzero:

go fix -omitzero=false .

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

Флаг -fixtool=prog позволяет выбрать другой инструмент анализа вместо стандартного. Например, чтобы собрать и запустить инструмент "stringintconv", который исправляет преобразования string(int), используйте такие команды:

go install golang.org/x/tools/go/analysis/passes/stringintconv/cmd/stringintconv@latest
go fix -fixtool=$(which stringintconv)

Альтернативные инструменты должны строиться на основе unitchecker, который отвечает за взаимодействие с go fix.

Анализаторы

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

any • bloop • fmtappendf • forvar • hostport • inline • mapsloop • minmax • newexpr • omitzero • plusbuild • rangeint • reflecttypefor • slicescontains • slicessort • stditerators • stringsbuilder • stringscut • stringcutprefix • stringsseq • testingcontext • waitgroup

any

Заменяет interface{} на any:

// before
func main() {
    var val interface{}
    val = 42
    fmt.Println(val)
}
// after
func main() {
    var val any
    val = 42
    fmt.Println(val)
}

bloop

Заменяет в бенчмарках цикл for-range по b.N на b.Loop, и удаляет ручное управление таймером:

// before
func Benchmark(b *testing.B) {
    s := make([]int, 1000)
    for i := range s {
        s[i] = i
    }
    b.ResetTimer()

    for range b.N {
        Calc(s)
    }
}
// after
func Benchmark(b *testing.B) {
    s := make([]int, 1000)
    for i := range s {
        s[i] = i
    }

    for b.Loop() {
        Calc(s)
    }
}

fmtappendf

Заменяет []byte(fmt.Sprintf) на fmt.Appendf, чтобы избежать промежуточного выделения памяти:

// before
func format(id int, name string) []byte {
    return []byte(fmt.Sprintf("ID: %d, Name: %s", id, name))
}
// after
func format(id int, name string) []byte {
    return fmt.Appendf(nil, "ID: %d, Name: %s", id, name)
}

forvar

Убери лишнее переопределение переменных в цикле:

// before
func main() {
    for x := range 4 {
        x := x
        go func() {
            fmt.Println(x)
        }()
    }
}
// after
func main() {
    for x := range 4 {
        go func() {
            fmt.Println(x)
        }()
    }
}

hostport

Заменяет создание сетевых адресов с помощью fmt.Sprintf на net.JoinHostPort, потому что пары "хост-порт", собранные через форматирование строк типа %s:%d или %s:%s, не работают с IPv6:

// before
func main() {
    host := "::1"
    port := 8080
    addr := fmt.Sprintf("%s:%d", host, port)
    net.Dial("tcp", addr)
}
// after
func main() {
    host := "::1"
    port := 8080
    addr := net.JoinHostPort(host, fmt.Sprintf("%d", port))
    net.Dial("tcp", addr)
}

inline

Встраивает вызовы функций согласно директивам go:fix inline:

// before
//go:fix inline
func Square(x float64) float64 {
    return math.Pow(float64(x), 2)
}

func main() {
    fmt.Println(Square(5))
}
// after
//go:fix inline
func Square(x float64) float64 {
    return math.Pow(float64(x), 2)
}

func main() {
    fmt.Println(math.Pow(float64(5), 2))
}

mapsloop

Заменяет явные циклы по картам на вызовы функций пакета maps (Copy, Insert, Clone или Collect в зависимости от ситуации):

// before
func copyMap(src map[string]int) map[string]int {
    dest := make(map[string]int, len(src))
    for k, v := range src {
        dest[k] = v
    }
    return dest
}
// after
func copyMap(src map[string]int) map[string]int {
    dest := make(map[string]int, len(src))
    maps.Copy(dest, src)
    return dest
}

minmax

Заменяет if/else на вызовы встроенных функций min или max:

// before
func calc(a, b int) int {
    var m int
    if a > b {
        m = a
    } else {
        m = b
    }
    return m * (b - a)
}
// after
func calc(a, b int) int {
    var m int
    m = max(a, b)
    return m * (b - a)
}

newexpr

Заменяет собственные функции вида "указатель на T" на вызов new(expr):

// before
type Pet struct {
    Name  string
    Happy *bool
}

func ptrOf[T any](v T) *T {
    return &v
}

func main() {
    p := Pet{Name: "Fluffy", Happy: ptrOf(true)}
    fmt.Println(p)
}
// after
type Pet struct {
    Name  string
    Happy *bool
}

//go:fix inline
func ptrOf[T any](v T) *T {
    return new(v)
}

func main() {
    p := Pet{Name: "Fluffy", Happy: new(true)}
    fmt.Println(p)
}

omitzero

Убирает omitempty у полей со структурным типом, потому что этот тег на них не влияет:

// before
type Person struct {
    Name string `json:"name"`
    Pet  Pet    `json:"pet,omitempty"`
}

type Pet struct {
    Name string
}
// after
type Person struct {
    Name string `json:"name"`
    Pet  Pet    `json:"pet"`
}

type Pet struct {
    Name string
}

plusbuild

Удаляет устаревшие комментарии //+build:

//go:build linux && amd64
// +build linux,amd64

package main

func main() {
    var _ = 42
}
//go:build linux && amd64

package main

func main() {
    var _ = 42
}

rangeint

Заменяет 3-частные циклы for на for-range по числам:

// before
func main() {
    for i := 0; i < 5; i++ {
        fmt.Print(i)
    }
}
// after
func main() {
    for i := range 5 {
        fmt.Print(i)
    }
}

reflecttypefor

Заменяет reflect.TypeOf(x) на reflect.TypeFor[T]() если тип известен во время компиляции:

// before
func main() {
    n := uint64(0)
    typ := reflect.TypeOf(n)
    fmt.Println("size =", typ.Bits())
}
// after
func main() {
    typ := reflect.TypeFor[uint64]()
    fmt.Println("size =", typ.Bits())
}

slicescontains

Заменяет циклы на slices.Contains или slices.ContainsFunc:

// before
func find(s []int, x int) bool {
    for _, v := range s {
        if x == v {
            return true
        }
    }
    return false
}
// after
func find(s []int, x int) bool {
    return slices.Contains(s, x)
}

slicessort

Заменяет sort.Slice на slices.Sort для простых типов:

// before
func main() {
    s := []int{22, 11, 33, 55, 44}
    sort.Slice(s, func(i, j int) bool { return s[i] < s[j] })
    fmt.Println(s)
}
// after
func main() {
    s := []int{22, 11, 33, 55, 44}
    slices.Sort(s)
    fmt.Println(s)
}

stditerators

Использует итераторы вместо API в стиле Len/At для некоторых типов стандартной библиотеки:

// before
func main() {
    typ := reflect.TypeFor[Person]()
    for i := range typ.NumField() {
        field := typ.Field(i)
        fmt.Println(field.Name, field.Type.String())
    }
}
// after
func main() {
    typ := reflect.TypeFor[Person]()
    for field := range typ.Fields() {
        fmt.Println(field.Name, field.Type.String())
    }
}

stringsbuilder

Заменяет повторные вызовы += на strings.Builder:

// before
func abbr(s []string) string {
    res := ""
    for _, str := range s {
        if len(str) > 0 {
            res += string(str[0])
        }
    }
    return res
}
// after
func abbr(s []string) string {
    var res strings.Builder
    for _, str := range s {
        if len(str) > 0 {
            res.WriteString(string(str[0]))
        }
    }
    return res.String()
}

stringscut

Заменяет некоторые вызовы strings.Index и срезы строк на strings.Cut или strings.Contains:

// before
func nospace(s string) string {
    idx := strings.Index(s, " ")
    if idx == -1 {
        return s
    }
    return strings.ReplaceAll(s, " ", "")
}
// after
func nospace(s string) string {
    found := strings.Contains(s, " ")
    if !found {
        return s
    }
    return strings.ReplaceAll(s, " ", "")
}

stringscutprefix

Заменяет strings.HasPrefix/TrimPrefix на strings.CutPrefix, а strings.HasSuffix/TrimSuffix на string.CutSuffix:

// before
func unindent(s string) string {
    if strings.HasPrefix(s, "> ") {
        return strings.TrimPrefix(s, "> ")
    }
    return s
}
// after
func unindent(s string) string {
    if after, ok := strings.CutPrefix(s, "> "); ok {
        return after
    }
    return s
}

stringsseq

Заменяет обход через strings.Split/Fields на strings.SplitSeq/FieldsSeq:

// before
func main() {
    s := "go is awesome"
    for _, word := range strings.Fields(s) {
        fmt.Println(len(word))
    }
}
// after
func main() {
    s := "go is awesome"
    for word := range strings.FieldsSeq(s) {
        fmt.Println(len(word))
    }
}

testingcontext

Заменяет context.WithCancel на t.Context в тестах:

// before
func Test(t *testing.T) {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    if ctx.Err() != nil {
        t.Fatal("context should be active")
    }
}
// after
func Test(t *testing.T) {
    ctx := t.Context()
    if ctx.Err() != nil {
        t.Fatal("context should be active")
    }
}

waitgroup

Заменяет wg.Add+wg.Done на wg.Go:

// before
func main() {
    var wg sync.WaitGroup

    wg.Add(1)
    go func() {
        defer wg.Done()
        fmt.Println("go!")
    }()

    wg.Wait()
}
// after
func main() {
    var wg sync.WaitGroup

    wg.Go(func() {
        fmt.Println("go!")
    })

    wg.Wait()
}

𝗣 71859 👥 Alan Donovan, Jonathan Amsterdam

]]>
Утечки горутин в Go 1.24+https://antonz.ru/detecting-goroutine-leaks/Fri, 19 Dec 2025 10:00:00 +0000https://antonz.ru/detecting-goroutine-leaks/С ними помогут synctest и pprof.Вы конечно и так в курсе, но на всякий случай:

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

Утрированный пример утечки:

func work() chan int {
    ch := make(chan int)
    go func() {
        ch <- 42
        ch <- 13 // (!) утечка
    }()
    return ch
}

func main() {
    <-work()
    // ...
}

Традиционно Go не очень-то помогал в поиске утечек. Обнаружить их можно было разве что пристально разглядывая профиль или трассировку с продакшена, а в тестах приходилось использовать сторонний пакет goleak от Убера.

Сейчас это меняется.

Сначала в Go 1.24 добавили пакет synctest, который прекрасно справляется с поиском утечек при тестировании. Об этом почему-то никто не говорит — наверно, потому что не проходили мой курс по многозадачности 😁

func Test(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        <-work()
        synctest.Wait()
    })
}
panic: main bubble goroutine has exited but blocked goroutines remain

goroutine 37 [chan send (durable), synctest bubble 1]:
main.work.func1()
    leak_test.go:12 +0x3c
created by main.work in goroutine 36
    leak_test.go:10 +0x6c

Как видите, тест и ругнулся, и точно указал, где возникла утечка.

А теперь в Go 1.26 подвезут новый pprof-профиль под названием goroutineleak — он надежно (без ложных срабатываний) обнаруживает утечки в продакшене (как вы понимаете, synctest-то в продакшене не запустишь).

func main() {
    prof := pprof.Lookup("goroutineleak")

    defer func() {
        time.Sleep(50 * time.Millisecond)
        prof.WriteTo(os.Stdout, 2)
    }()

    <-work()
}
goroutine 3 [chan send (leaked)]:
main.work.func1()
    leak.go:12 +0x3c
created by main.work in goroutine 1
    leak.go:10 +0x74

Как видите, указал ровно на ту же утечку, что synctest.

В общем, рекомендую оба инструмента. За подробностями велкам в статью:

Detecting goroutine leaks in modern Go

]]>
Go-фича: Защита секретовhttps://antonz.ru/accepted/runtime-secret/Tue, 09 Dec 2025 10:00:00 +0000https://antonz.ru/accepted/runtime-secret/Автоматически очищать память, чтобы не допустить утечки секретов.Часть серии Принято! В ней простыми словами объясняются новые фичи Go.

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

Версия 1.26 • Станд. библиотека • Мелочь

Что

Новый пакет runtime/secret позволяет запускать функцию в защищенном режиме (secret mode). После завершения функции все использованные ей регистры и стек сразу очищаются (затираются нулями). Выделенная функцией память в куче очищается после того, как сборщик мусора решит, что она недостижима (unreachable).

secret.Do(func() {
    // Сгенерировть сессионный ключ
    // и зашифровать им данные.
})

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

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

Зачем

Криптографические протоколы, такие как WireGuard или TLS, обладают свойством под названием "прямая секретность" (forward secrecy). Это значит, что даже если злоумышленник получит доступ к долгосрочным секретам (например, к приватному ключу в TLS), он все равно не сможет расшифровать прошлые сессии. Для этого временные ключи (которые используются при создании конкретной сессии) должны удаляться из памяти после использования. Если нет надежного способа очистить эту память, ключи могут остаться там надолго. Если злоумышленник получит доступ к памяти, он сможет восстановить сессионный ключ и расшифровать прошлый трафик, нарушив прямую секретность.

В Go память управляется рантаймом, и нет гарантии, когда и как она очищается. Конфиденциальные данные могут оставаться в куче или на стеке, и их можно получить через дампы памяти или атаки на память. Разработчикам часто приходится использовать ненадежные "хаки" с рефлексией, чтобы попытаться обнулить внутренние буферы в стандартных криптографических библиотеках. Даже так часть данных все равно может остаться в памяти, где разработчик не может их удалить или контролировать.

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

Как

Добавить пакет runtime/secret с функциями Do и Enabled:

// Do invokes f.
//
// Do ensures that any temporary storage used by f is erased in a
// timely manner. (In this context, "f" is shorthand for the
// entire call tree initiated by f.)
//   - Any registers used by f are erased before Do returns.
//   - Any stack used by f is erased before Do returns.
//   - Any heap allocation done by f is erased as soon as the garbage
//     collector realizes that it is no longer reachable.
//   - Do works even if f panics or calls runtime.Goexit. As part of
//     that, any panic raised by f will appear as if it originates from
//     Do itself.
func Do(f func())
// Enabled reports whether Do appears anywhere on the call stack.
func Enabled() bool

Текущее решение имеет несколько ограничений:

  • Работает только на linux/amd64 и linux/arm64. На других платформах функция Do просто вызывает f напрямую.
  • Защита не распространяется на глобальные переменные, которые изменяет f.
  • Если внутри f запускается новая горутина, произойдет паника.
  • Если f вызывает runtime.Goexit, очистка откладывается до выполнения всех отложенных функций.
  • Данные в куче очищаются только если ➊ на них больше нет ссылок, и ➋ сборщик мусора заметил это. Первый пункт зависит от программы, второй — от того, когда рантайм решит провести сборку мусора.
  • Если в f происходит паника, значение паники может содержать ссылки на память, выделенную внутри f. Эта память не будет очищена, пока значение паники доступно.
  • Адреса указателей могут попасть в буферы данных, которые использует сборщик мусора. Не храните конфиденциальную информацию в указателях.

Пакет в основном предназначен для разработчиков, которые работают с криптографическими библиотеками. Большинству приложений лучше использовать более высокоуровневые библиотеки, которые внутри уже используют secret.Do.

В Go 1.26 пакет runtime/secret объявлен экспериментальным. Чтобы его включить, установите переменную GOEXPERIMENT=runtimesecret при сборке.

Пример

Генерируем сессионный ключ, при этом временный приватный ключ и общий секрет остаются в безопасности благодаря secret.Do:

// DeriveSessionKey performs an ephemeral key exchange to agree on a
// session key. It wraps the sensitive mathematics in secret.Do to
// ensure the ephemeral private key and raw shared secret are
// erased from memory immediately after use.
func DeriveSessionKey(peerPublicKey *ecdh.PublicKey) (*ecdh.PublicKey, []byte, error) {
    var pubKey *ecdh.PublicKey
    var sessionKey []byte
    var err error

    // Use secret.Do to contain the sensitive data during the handshake.
    // The ephemeral private key and the raw shared secret will be
    // wiped out when this function finishes.
    secret.Do(func() {
        // 1. Generate an ephemeral private key.
        // This is highly sensitive; if leaked later, forward secrecy is broken.
        privKey, e := ecdh.P256().GenerateKey(rand.Reader)
        if e != nil {
            err = e
            return
        }

        // 2. Compute the shared secret (ECDH).
        // This raw secret is also highly sensitive.
        sharedSecret, e := privKey.ECDH(peerPublicKey)
        if e != nil {
            err = e
            return
        }

        // 3. Derive the final session key (e.g., using HKDF).
        // We copy the result out; the inputs (privKey, sharedSecret)
        // will be destroyed by secret.Do when they become unreachable.
        sessionKey = performHKDF(sharedSecret)
        pubKey = privKey.PublicKey()
    })

    // The session key is returned for use, but the "recipe" to recreate it
    // is destroyed. Additionally, because the session key was allocated
    // inside the secret block, the runtime will automatically zero it out
    // when the application is finished using it.
    return pubKey, sessionKey, err
}

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

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

Завернув вычисления в secret.Do, мы гарантируем, что как только сессионный ключ создан, все "ингредиенты", использованные для его создания, сразу же навсегда удаляются. Это значит, что даже если сервер взломают в будущем, этот конкретный прошлый сеанс нельзя будет раскрыть, что обеспечивает прямую секретность.

Ссылки

𝗣 21865 • 𝗖𝗟 704615 • 👥 Daniel Morsing, Dave Anderson, Filippo Valsorda, Jason A. Donenfeld, Keith Randall, Russ Cox

]]>
Go-фича: Безопасная проверка ошибокhttps://antonz.ru/accepted/errors-astype/Tue, 02 Dec 2025 09:30:00 +0000https://antonz.ru/accepted/errors-astype/errors.AsType — современная альтернатива errors.As.Часть серии Принято! В ней простыми словами объясняются новые фичи Go.

Проверка типа ошибки через errors.AsType — современная и безопасная альтернатива errors.As.

Версия 1.26 • Станд. библиотека • Важно

Что

Новая функция errors.AsType — это generic-версия errors.As:

// go 1.13+
func As(err error, target any) bool
// go 1.26+
func AsType[E error](err error) (E, bool)

Она безопасная (type-safe), работает быстрее и проще в использовании:

// используем errors.As
var appErr AppError
if errors.As(err, &appErr) {
    fmt.Println("Got an AppError:", appErr)
}
// используем errors.AsType
if appErr, ok := errors.AsType[AppError](err); ok {
    fmt.Println("Got an AppError:", appErr)
}

errors.As не объявлена устаревшей (пока), но для нового кода рекомендуется использовать errors.AsType.

Зачем

Функция errors.As требует заранее объявить переменную ошибки нужного типа и передать указатель на нее:

var appErr AppError
if errors.As(err, &appErr) {
    fmt.Println("Got an AppError:", appErr)
}

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

var connErr *net.OpError
var dnsErr *net.DNSError

if errors.As(err, &connErr) {
    fmt.Println("Network operation failed:", connErr.Op)
} else if errors.As(err, &dnsErr) {
    fmt.Println("DNS resolution failed:", dnsErr.Name)
} else {
    fmt.Println("Unknown error")
}

С помощью generic-функции errors.AsType можно указать тип ошибки прямо при вызове. Это делает код короче и позволяет держать переменные ошибок внутри блоков if:

if connErr, ok := errors.AsType[*net.OpError](err); ok {
    fmt.Println("Network operation failed:", connErr.Op)
} else if dnsErr, ok := errors.AsType[*net.DNSError](err); ok {
    fmt.Println("DNS resolution failed:", dnsErr.Name)
} else {
    fmt.Println("Unknown error")
}

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

  • Не использует рефлексию.
  • Нет паники во время выполнения.
  • Меньше аллокаций памяти.
  • Проверка типов на этапе компиляции.
  • Работает быстрее.

Наконец, AsType умеет все то же, что и As, так что ее можно спокойно использовать вместо As в новом коде.

Подробности

Добавить в пакет errors функцию AsType:

// AsType finds the first error in err's tree that matches the type E,
// and if one is found, returns that error value and true. Otherwise, it
// returns the zero value of E and false.
//
// The tree consists of err itself, followed by the errors obtained by
// repeatedly calling its Unwrap() error or Unwrap() []error method.
// When err wraps multiple errors, AsType examines err followed by a
// depth-first traversal of its children.
//
// An error err matches the type E if the type assertion err.(E) holds,
// or if the error has a method As(any) bool such that err.As(target)
// returns true when target is a non-nil *E. In the latter case, the As
// method is responsible for setting target.
func AsType[E error](err error) (E, bool)

Рекомендовать использовать AsType вместо As:

// As finds the first error in err's tree that matches target, and if one
// is found, sets target to that error value and returns true. Otherwise,
// it returns false.
// ...
// For most uses, prefer [AsType]. As is equivalent to [AsType] but sets its
// target argument rather than returning the matching error and doesn't require
// its target argument to implement error.
// ...
func As(err error, target any) bool

Пример

Открыть файл и проверить, связана ли ошибка с путем к файлу:

// go 1.25
var pathError *fs.PathError
if _, err := os.Open("non-existing"); err != nil {
    if errors.As(err, &pathError) {
        fmt.Println("Failed at path:", pathError.Path)
    } else {
        fmt.Println(err)
    }
}
Failed at path: non-existing
// go 1.26
if _, err := os.Open("non-existing"); err != nil {
    if pathError, ok := errors.AsType[*fs.PathError](err); ok {
        fmt.Println("Failed at path:", pathError.Path)
    } else {
        fmt.Println(err)
    }
}
Failed at path: non-existing

Ссылки

𝗣 51945 • 𝗖𝗟 707235

]]>
Курс: Знакомство с Gohttps://antonz.ru/go-intro/Sat, 29 Nov 2025 16:00:00 +0000https://antonz.ru/go-intro/Для всех, кто уверенно программирует на другом языке и хочет попробовать Go.Я опубликовал бесплатный вводный курс по Go. Он идеально подходит для свитчеров — тех, кто уже уверенно программирует на другом языке и хочет попробовать Go.

В отличие от обычных курсов-знакомств, тут никто не рассказывает, что такое переменная и чем она отличается от цикла. Бестолковых задач вроде «что напечатает функция» тоже нет. Все кратко и по делу.

Фрагмент урока о горутинах
Фрагмент урока о горутинах

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

Часть 1. Основы

  • 1.1 О курсе
  • 1.2 Базовые конструкции
  • 1.3 Массивы и карты
  • 1.4 Функции и указатели
  • 1.5 Структуры и методы
  • 1.6 Резюме

Часть 2. Другие темы

  • 2.1 О модуле
  • 2.2 Пакеты и модули
  • 2.3 Дженерики
  • 2.4 Горутины
  • 2.5 Текст
  • 2.6 Резюме

Лекции текстовые, видео нет. Я умею понятно объяснять сложные вещи, так что читать их приятно. Много примеров, нет сухой теории. Нет ИИ-генерированного контента — все лекции написаны лично мной.

Курс интерактивный. Большинство упражнений можно выполнять прямо в браузере. По каждой задачке есть эталонное решение с разбором.

Используется версия Go 1.25.

Записаться на курс

]]>
Go-фича: Метрики горутинhttps://antonz.ru/accepted/goroutine-metrics/Wed, 26 Nov 2025 12:00:00 +0000https://antonz.ru/accepted/goroutine-metrics/Подробные метрики горутин от рантайма.Часть серии Принято! В ней простыми словами объясняются новые фичи Go.

Подробные метрики горутин от рантайма.

Версия 1.26 • Станд. библиотека • Полезно

Что

Новые метрики в пакете runtime/metrics позволяют лучше понять, как работают горутины:

  • Общее количество горутин с начала работы программы.
  • Количество горутин в каждом состоянии.
  • Количество активных потоков.

Зачем

Пакет runtime/metrics уже предоставляет много информации о работе программы, но в нем не хватало метрик по состояниям горутин и количеству потоков.

Метрики по состояниям горутин помогают находить типичные проблемы в продакшене. Например, если растет число горутин в состоянии waiting, это может указывать на проблемы с блокировками. Много горутин в состоянии not-in-go — значит, они застряли в системных вызовах или cgo. Если увеличивается очередь runnable — CPU не справляются с нагрузкой.

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

Как

Добавить следующие метрики в пакет runtime/metrics:

/sched/goroutines-created:goroutines
	Общее количество горутин, запущенных
    с момент старта приложения (накопительный итог).

/sched/goroutines/not-in-go:goroutines
	Примерное количество горутин, которые
    «ушли» в системный или cgo-вызов.

/sched/goroutines/runnable:goroutines
	Примерное количество горутин, которые готовы
    к выполнению, но пока не выполняются.

/sched/goroutines/running:goroutines
	Примерное количество выполняющихся горутин.
    Всегда меньше или равно /sched/gomaxprocs:threads.

/sched/goroutines/waiting:goroutines
	Примерное количество горутин, ожидающих
    на ресурсе (I/O или примитивы синхронизации).

/sched/threads/total:threads
	Текущее количество активных потоков,
    которыми управляет планировщик Go.

Сумма значений показателей по каждому состоянию не обязательно равна общему количеству активных горутин (метрика /sched/goroutines:goroutines, доступна в Go 1.16+).

Все метрики используют счетчики типа uint64.

Пример

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

func main() {
	go work() // omitted for brevity
	time.Sleep(100 * time.Millisecond)

	fmt.Println("Goroutine metrics:")
	printMetric("/sched/goroutines-created:goroutines", "Created")
	printMetric("/sched/goroutines:goroutines", "Live")
	printMetric("/sched/goroutines/not-in-go:goroutines", "Syscall/CGO")
	printMetric("/sched/goroutines/runnable:goroutines", "Runnable")
	printMetric("/sched/goroutines/running:goroutines", "Running")
	printMetric("/sched/goroutines/waiting:goroutines", "Waiting")

	fmt.Println("Thread metrics:")
	printMetric("/sched/gomaxprocs:threads", "Max")
	printMetric("/sched/threads/total:threads", "Live")
}

func printMetric(name string, descr string) {
	sample := []metrics.Sample{{Name: name}}
	metrics.Read(sample)
    // Предполагаем, что значение типа uint64; так делать в продакшене не стоит.
    // Лучше действовать в соответствии с sample[0].Value.Kind.
	fmt.Printf("  %s: %v\n", descr, sample[0].Value.Uint64())
}
Goroutine metrics:
  Created: 52
  Live: 12
  Syscall/CGO: 0
  Runnable: 0
  Running: 4
  Waiting: 8
Thread metrics:
  Max: 8
  Live: 4

Никаких сюрпризов: новые значения метрик читаем так же, как и раньше — с помощью metrics.Read.

Ссылки

𝗣 15490 • 𝗖𝗟 690397, 690398, 690399

]]>
Go-фича: Dialer с контекстомhttps://antonz.ru/accepted/net-dialer-context/Thu, 13 Nov 2025 11:30:00 +0000https://antonz.ru/accepted/net-dialer-context/Подключение к сокетам TCP, UDP, IP или Unix с таймаутом.Часть серии Принято! В ней простыми словами объясняются новые фичи Go.

Методы net.Dialer для подключения по конкретным протоколам с поддержкой контекста.

Версия 1.26 • Станд. библиотека • Мелочь

Что

Тип net.Dialer подключается к сетевому адресу, используя указанный протокол (network) — TCP, UDP, IP или Unix-сокеты.

Новые методы Dialer с поддержкой контекста — DialTCP, DialUDP, DialIP и DialUnix — объединяют эффективную реализацию (как в существующих Dial-функциях) с возможностью отмены (как в Dialer.DialContext).

Зачем

В пакете net уже есть отдельные функции для разных протоколов (DialTCP, DialUDP, DialIP и DialUnix), но их сделали до появления context.Context, поэтому они не поддерживают отмену:

func DialTCP(network string, laddr, raddr *TCPAddr) (*TCPConn, error)
func DialUDP(network string, laddr, raddr *UDPAddr) (*UDPConn, error)
func DialIP(network string, laddr, raddr *IPAddr) (*IPConn, error)
func DialUnix(network string, laddr, raddr *UnixAddr) (*UnixConn, error)

С другой стороны, у типа net.Dialer есть универсальный метод DialContext. Он поддерживает отмену и может использоваться для подключения по любому из поддерживаемых протоколов:

func (d *Dialer) DialContext(ctx context.Context, network, address string) (Conn, error)

Если вы уже знаете протокол и адрес, использовать DialContext менее эффективно, чем специализированные функции вроде DialTCP, по следующим причинам:

  • Резолв адреса: DialContext сам выполняет разрешение адреса (например, DNS-запросы и преобразование в net.TCPAddr или net.UDPAddr) на основе названия протокола и исходного адреса. Специализированные функции принимают уже готовый объект адреса, поэтому этот шаг пропускается.

  • Роутинг по протоколу: DialContext должен определить, какой протокол использовать, и направить вызов нужному обработчику. Специализированные функции уже знают, какой протокол нужен, и этот шаг тоже пропускается.

Таким образом, функции net.Dial* работают быстрее, но не поддерживают отмену операций. Тип net.Dialer поддерживает отмену, но работает медленнее. Решение — добавить в Dialer методы, которые учитывают контекст и работают с конкретными протоколами.

Заодно это позволит использовать новые типы адресов из пакета netip (например, netip.AddrPort вместо net.TCPAddr), которые более предпочтительны в современном Go-коде.

Как

Добавить четыре новых метода в net.Dialer:

DialTCP(ctx context.Context, network string, laddr, raddr netip.AddrPort) (*TCPConn, error)
DialUDP(ctx context.Context, network string, laddr, raddr netip.AddrPort) (*UDPConn, error)
DialIP(ctx context.Context, network string, laddr, raddr netip.Addr) (*IPConn, error)
DialUnix(ctx context.Context, network string, laddr, raddr *UnixAddr) (*UnixConn, error)

Сигнатуры методов похожи на существующие функции пакета net, но дополнительно принимают контекст и используют новые типы адресов из пакета netip.

Пример

Используем метод DialTCP, чтобы подключиться к TCP-серверу:

var d net.Dialer
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

// Подключение не пройдет, потому что сервер не запущен.
raddr := netip.MustParseAddrPort("127.0.0.1:12345")
conn, err := d.DialTCP(ctx, "tcp", netip.AddrPort{}, raddr)
if err != nil {
    log.Fatalf("Failed to dial: %v", err)
}
defer conn.Close()

if _, err := conn.Write([]byte("Hello, World!")); err != nil {
    log.Fatal(err)
}
Failed to dial: dial tcp 127.0.0.1:12345: connect: connection refused (exit status 1)

Используем метод DialUnix, чтобы подключиться к Unix-сокету:

var d net.Dialer
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

// Подключение не пройдет, потому что сервер не запущен.
raddr := &net.UnixAddr{Name: "/path/to/unix.sock", Net: "unix"}
conn, err := d.DialUnix(ctx, "unix", nil, raddr)
if err != nil {
    log.Fatalf("Failed to dial: %v", err)
}
defer conn.Close()

if _, err := conn.Write([]byte("Hello, socket!")); err != nil {
    log.Fatal(err)
}
Failed to dial: dial unix /path/to/unix.sock: connect: no such file or directory (exit status 1)

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

Ссылки

𝗣 49097 • 𝗖𝗟 657296

]]>
Go-фича: Сравнение IP-подсетейhttps://antonz.ru/accepted/netip-prefix-compare/Mon, 20 Oct 2025 08:30:00 +0000https://antonz.ru/accepted/netip-prefix-compare/Как это делают IANA и Python.Часть серии Принято! В ней простыми словами объясняются новые фичи Go.

Сравнивать префиксы IP-адресов так же, как это делает IANA (администрация адресного пространства интернета).

Версия 1.26 • Станд. библиотека • Мелочь

Что

Префикс IP-адреса задает определенную подсеть. Обычно такие префиксы записывают в формате CIDR:

10.0.0.0/8
127.0.0.0/8
169.254.0.0/16
203.0.113.0/24

В Go IP-префикс представлен типом netip.Prefix.

Новый метод Prefix.Compare позволяет сравнить два IP-префикса, что упрощает их сортировку — не придется писать свой код сравнения. Порядок сравнения совпадает с реализацией в Python и с порядком, который использует IANA.

Зачем

Когда команда Go изначально разрабатывала тип IP-подсети (net/netip.Prefix), они решили не добавлять метод Compare, потому что не было общепринятого способа упорядочивать такие значения.

Из-за этого, если разработчику нужно отсортировать IP-подсети — например, чтобы организовать таблицы маршрутизации или провести тесты — приходится писать собственную логику сравнения. Это приводит к повторяющемуся коду, чреватому ошибками.

Чтобы улучшить ситуацию, в Go добавят стандартный способ сравнения IP-префиксов. Это должно уменьшить количество шаблонного кода и помочь программам сортировать IP-подсети одинаково.

Подробности

Добавить метод Compare в тип netip.Prefix:

// Compare возвращает целое число - результат сравнения двух префиксов.
// Результат будет 0, если p == p2, -1, если p < p2, и +1, если p > p2.
func (p Prefix) Compare(p2 Prefix) int

Вот как Compare сравнивает префиксы:

  • Сначала по корректности (некорректные идут перед корректными).
  • Потом по семейству адресов (IPv4 перед IPv6).
    10.0.0.0/8 < ::/8
  • Потом по маскированному IP-адресу (IP подсети).
    10.0.0.0/8 < 10.0.1.0/8
  • Потом по длине префикса.
    10.0.0.0/8 < 10.0.0.0/16
  • Потом по немаскированному адресу (оригинальный IP).
    10.0.0.0/8 < 10.0.0.1/8

Это тот же порядок, что использует netaddr.IPNetwork в Python и IANA.

Пример

Отсортировать список IP-префиксов:

prefixes := []netip.Prefix{
    netip.MustParsePrefix("10.0.1.0/8"),
    netip.MustParsePrefix("203.0.113.0/24"),
    netip.MustParsePrefix("10.0.0.0/8"),
    netip.MustParsePrefix("169.254.0.0/16"),
    netip.MustParsePrefix("203.0.113.0/8"),
}

slices.SortFunc(prefixes, func(a, b netip.Prefix) int {
    return a.Compare(b)
})

for _, p := range prefixes {
    fmt.Println(p.String())
}
10.0.0.0/8
10.0.1.0/8
169.254.0.0/16
203.0.113.0/8
203.0.113.0/24

Ссылки

𝗣 61642 • 𝗖𝗟 700355

]]>
Go-фича: Хешерыhttps://antonz.ru/accepted/maphash-hasher/Sun, 28 Sep 2025 12:30:00 +0000https://antonz.ru/accepted/maphash-hasher/Стандартный подход к хешированию и проверке на равенство в коллекциях.Часть серии Принято! В ней простыми словами объясняются новые фичи Go.

Стандартный подход к вычислению хеша и проверке на равенство в пользовательских структурах данных.

Версия 1.26 • Станд. библиотека • Полезно

Что

Новый интерфейс maphash.Hasher — это стандартный способ хешировать и сравнивать элементы в пользовательских коллекциях, например, в собственных картах или множествах.

// Hasher реализует хеширование и проверку на равенство для типа T.
type Hasher[T any] interface {
    Hash(hash *maphash.Hash, value T)
    Equal(T, T) bool
}

Тип ComparableHasher — это стандартная реализация хешера для сравнимых (comparable) типов — таких как числа, строки и структуры с сравнимыми полями.

Зачем

Пакет maphash предоставляет хеш-функции для срезов байтов и строк, но не объясняет, как создавать собственные структуры данных, основанные на хешировании.

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

Подробности

Добавить интерфейс хешера в пакет maphash:

// Hasher - это тип, который реализует хеширование
// и сравнение на равенство для типа T.
//
// Hasher не должен иметь состояние, и его
// нулевое значение должно быть валидным.
// Поэтому обычно Hasher - это пустая структура.
type Hasher[T any] interface {
    // Hash обновляет хеш в соответствии со значением value.
    //
    // Если два значения равны (Equal), их хеш тоже должен совпадать.
    // То есть, если Equal(a, b) возвращает true, то Hash(h, a) и Hash(h, b)
    // должны записывать одинаковые данные в h.
    Hash(hash *maphash.Hash, value T)
    Equal(T, T) bool
}

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

// ComparableHasher - это реализация Hasher для сравнимых типов.
// Его метод Equal(x, y) логически соответствует x == y.
type ComparableHasher[T comparable] struct{}
func (h ComparableHasher[T]) Hash(hash *Hash, value T)
func (h ComparableHasher[T]) Equal(x, y T) bool

Пример

Вот нечувствительный к регистру строк хешер:

// CaseInsensitive - это хешер для сравнения строк без учета регистра.
type CaseInsensitive struct{}

func (CaseInsensitive) Hash(h *maphash.Hash, s string) {
    // Используем lowercase вместо case folding для простоты.
    h.WriteString(strings.ToLower(s))
}

func (CaseInsensitive) Equal(a, b string) bool {
    return strings.ToLower(a) == strings.ToLower(b)
}

И дженерик-множество Set, с настраиваемым хешером для проверки на равенство и хеширования:

// Set - это универсальная реализация множества
// с возможностью подставить свой хешер.
// Значения хранятся в карте корзин (bucket),
// корзины идентифицируются по хешу от значения.
// Если несколько значений попадают в одну и ту же корзину,
// используется линейный поиск, чтобы найти нужное.
type Set[H maphash.Hasher[V], V any] struct {
    seed   maphash.Seed   // случайный посев для хеширования
    hasher H              // выполняет хеширование и проверку на равенство
    data   map[uint64][]V // корзины значений, индексированные по хешу
}

// NewSet создает новый экземпляр Set с указанной функцией хеширования.
func NewSet[H maphash.Hasher[V], V any](hasher H) *Set[H, V] {
    return &Set[H, V]{
        seed:   maphash.MakeSeed(),
        hasher: hasher,
        data:   make(map[uint64][]V),
    }
}

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

// calcHash вычисляет хеш значения.
func (s *Set[H, V]) calcHash(val V) uint64 {
    var h maphash.Hash
    h.SetSeed(s.seed)
    s.hasher.Hash(&h, val)
    return h.Sum64()
}

Этот хеш используется в методах Has и Add. Он служит ключом в карте корзин, чтобы найти нужную корзину для значения.

Метод Has проверяет, есть ли значение в соответствующей корзине:

// Has возвращает true, если указанное значение есть в множестве.
func (s *Set[H, V]) Has(val V) bool {
    hash := s.calcHash(val)
    if bucket, ok := s.data[hash]; ok {
        for _, item := range bucket {
            if s.hasher.Equal(val, item) {
                return true
            }
        }
    }
    return false
}

Add добавляет значение в соответствующую корзину:

// Add добавляет значение в множество, если его там еще нет.
func (s *Set[H, V]) Add(val V) {
    if s.Has(val) {
        return
    }
    hash := s.calcHash(val)
    s.data[hash] = append(s.data[hash], val)
}

Теперь мы можем создать регистро-независимое множество строк:

func main() {
    set := NewSet(CaseInsensitive{})

    set.Add("hello")
    set.Add("world")

    fmt.Println(set.Has("HELLO")) // true
    fmt.Println(set.Has("world")) // true
}
true
true

Или обычное множество строк с помощью maphash.ComparableHasher:

func main() {
    set := NewSet(maphash.ComparableHasher[string]{})

    set.Add("hello")
    set.Add("world")

    fmt.Println(set.Has("HELLO")) // false
    fmt.Println(set.Has("world")) // true
}
false
true

Ссылки

𝗣 70471 • 𝗖𝗟 657296

]]>
Go-фича: new(expr)https://antonz.ru/accepted/new-expr/Wed, 24 Sep 2025 07:50:00 +0000https://antonz.ru/accepted/new-expr/Использовать new для выражений.Часть серии Принято! В ней простыми словами объясняются новые фичи Go.

Использовать встроенную функцию new для выражений.

Версия 1.26 • Язык • Важно

Что

Раньше new можно было использовать только с типами:

p1 := new(int)
fmt.Println(*p1)
// 0

А теперь можно и с выражениями:

// Указатель на переменную типа int со значением 42.
p := new(42)
fmt.Println(*p)
// 42

Если аргумент expr — это выражение типа T, то new(expr) аллоцирует переменную типа T (то есть выделяет под нее память), инициализирует ее значением expr и возвращает ее адрес, то есть значение типа *T.

Зачем

Есть простой способ создать указатель на составной литерал:

type Person struct { name string }
p := &Person{name: "alice"}

Но нет простого способа создать указатель на значение простого типа:

n := 42
p := &n

Теперь это исправят.

Подробности

Обновить раздел Allocation в спецификации языка следующим образом (далее перевод с английского):

Выделение памяти

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

➀ Если аргумент expr — это выражение типа T или нетипизированная константа, у которой тип по умолчанию — T, то new(expr) выделяет переменную типа T, инициализирует ее значением expr и возвращает ее адрес, то есть значение типа *T.

➁ Если аргумент — это тип T, то new(T) выделяет переменную, инициализированную нулевым значением типа T.

Например, new(123) и new(int) оба возвращают указатель на новую переменную типа int. Значение первой переменной — 123, второй — 0.

➀ — это новая фича, ➁ уже работает как описано.

Примеры

Указатель на простой тип:

// go 1.25
n := 42
p1 := &n
fmt.Println(*p1)

s := "go"
p2 := &s
fmt.Println(*p2)
42
go
// go 1.26
p1 := new(42)
fmt.Println(*p1)

s := "go"
p2 := new(s)
fmt.Println(*p2)
42
go

Указатель на составное значение:

// go 1.25
s := []int{11, 12, 13}
p1 := &s
fmt.Println(*p1)

type Person struct{ name string }
p2 := &Person{name: "alice"}
fmt.Println(*p2)
[11 12 13]
{alice}
// go 1.26
p1 := new([]int{11, 12, 13})
fmt.Println(*p1)

type Person struct{ name string }
p2 := new(Person{name: "alice"})
fmt.Println(*p2)
[11 12 13]
{alice}

Указатель на результат вызова функции:

// go 1.25
f := func() string { return "go" }
v := f()
p := &v
fmt.Println(*p)
go
// go 1.26
f := func() string { return "go" }
p := new(f())
fmt.Println(*p)
go

Передавать nil по-прежнему нельзя:

// go 1.25 and go 1.26
p := new(nil)
// compilation error

Ссылки

𝗣 45624 • 𝗖𝗟 704935, 704737, 704955, 705157

]]>