Избранные фичи Go 1.20

Go 1.20 принес немало новых фич и улучшений. В этой статье я разберу те, которые мне приглянулись. Это не исчерпывающий список; его смотрите в официальных заметках к релизу.

Рассмотрим такие темы:

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

Множественные ошибки

Концепция «ошибки как значения» (в противовес исключениям) получила ренессанс в современных языках вроде Go и Rust. Вы и без меня отлично это знаете, потому что в Go невозможно и шагу ступить, чтобы не споткнуться об ошибку.

Go 1.20 принес нам новую радость — комбинацию ошибок через errors.Join():

errRaining := errors.New("it's raining")
errWindy := errors.New("it's windy")
err := errors.Join(errRaining, errWindy)

Теперь err — это одновременно errRaining и errWindy. Стандартные функции errors.Is() и errors.As() умеют с этим работать:

if errors.Is(err, errRaining) {
    fmt.Println("ouch!")
}
// ouch!

fmt.Errorf() тоже научилась комбинировать ошибки:

err := fmt.Errorf(
    "reasons to skip work: %w, %w",
    errRaining,
    errWindy,
)

Чтобы принимать множественные ошибки в собственном error-типе, достаточно вернуть []error из метода Unwrap():

type RefusalErr struct {
    reasons []error
}

func (e RefusalErr) Unwrap() []error {
    return e.reasons
}

func (e RefusalErr) Error() string {
    return fmt.Sprintf("refusing: %v", e.reasons)
}

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

песочница

Причина ошибки контекста

При отмене контекста происходит ошибка context.Canceled. Это не новость:

ctx, cancel := context.WithCancel(context.Background())
cancel()

fmt.Println(ctx.Err())
// context canceled

Начиная с 1.20, контекст можно создать с помощью context.WithCancelCause(). Тогда cancel() будет принимать один параметр — корневую причину ошибки (cause):

ctx, cancel := context.WithCancelCause(context.Background())
cancel(errors.New("the night is dark"))

Вытащить из контекста причину ошибки поможет context.Cause():

fmt.Println(ctx.Err())
// context canceled

fmt.Println(context.Cause(ctx))
// the night is dark

Возможно, вы спросите — почему context.Cause()? Логичнее ведь было бы добавить метод Cause() в сам контекст, аналогично методу Err()?

Да. Но Context — это интерфейс. А любое изменение интерфейса ломает обратную совместимость. Поэтому сделали так.

песочница

Новые форматы дат

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

Например, разбор даты 25.01.2023 09:30 выглядит так:

const layout = "02.01.2006 15:04"
t, _ := time.Parse(layout, "25.01.2023 09:30")
fmt.Println(t)
// 2023-01-25 09:30:00 +0000 UTC

Базовый формат 01/02 03:04:05PM '06 может быть хорошей мнемоникой для США, но для жителя Европы (или Азии) запомнить это невозможно.

Авторы Go заботливо предусмотрели аж 12 стандартных масок, из которых для не-американца подходят только RFC3339 и RFC3339Nano. Остальные такие же загадочные, как имперская система мер и весов:

Layout      = "01/02 03:04:05PM '06 -0700"
ANSIC       = "Mon Jan _2 15:04:05 2006"
UnixDate    = "Mon Jan _2 15:04:05 MST 2006"
RubyDate    = "Mon Jan 02 15:04:05 -0700 2006"
RFC822      = "02 Jan 06 15:04 MST"
RFC822Z     = "02 Jan 06 15:04 -0700"
RFC850      = "Monday, 02-Jan-06 15:04:05 MST"
RFC1123     = "Mon, 02 Jan 2006 15:04:05 MST"
RFC1123Z    = "Mon, 02 Jan 2006 15:04:05 -0700"
Kitchen     = "3:04PM"

Прошло 10 лет, и разработчики Go начали что-то подозревать. Они с удивлением узнали, что в мире приняты несколько другие форматы дат. И, начиная с версии 1.20, добавили три новые маски:

DateTime = "2006-01-02 15:04:05"
DateOnly = "2006-01-02"
TimeOnly = "15:04:05"

Теперь наконец-то можно делать так:

t, _ := time.Parse(time.DateOnly, "2023-01-25")
fmt.Println(t)
// 2023-01-25 00:00:00 +0000 UTC

Праздник!

песочница

От среза к массиву

Начиная с версии 1.17, в Go можно получить указатель на массив под срезом:

s := []int{1, 2, 3}
arrp := (*[3]int)(s)

Меняя массив через указатель, мы тем самым меняем и срез:

arrp[2] = 42
fmt.Println(s)
// [1 2 42]

В Go 1.20 можно получить еще и копию массива под срезом:

s := []int{1, 2, 3}
arr := [3]int(s)

Изменяя такой массив, мы не меняем срез:

arr[2] = 42
fmt.Println(arr)
// [1 2 42]
fmt.Println(s)
// [1 2 3]

Это, по сути, синтаксический сахар, потому что получить копию массива можно было и раньше:

s := []int{1, 2, 3}
arr := *(*[3]int)(s)

Новая запись приятнее, конечно.

песочница

Другие изменения

bytes.Clone() клонирует срез байт:

b := []byte("abc")
clone := bytes.Clone(b)

math/rand теперь самостоятельно инициализирует генератор случайным стартовым значением. Больше не нужно вызывать rand.Seed().

strings.CutPrefix() и strings.CutSuffix() обрезают префикс/суффикс аналогично TrimPrefix/TrimSuffix, но дополнительно сообщают, был ли этот префикс в строке:

s := "> go!"
s, found := strings.CutPrefix(s, "> ")
fmt.Println(s, found)
// go! true

sync.Map обзавелся атомарными методами Swap, CompareAndSwap и CompareAndDelete:

var m sync.Map
m.Store("name", "Alice")
prev, ok := m.Swap("name", "Bob")
fmt.Println(prev, ok)
// Alice true

time.Compare() сравнивает два времени и возвращает -1/0/1 по результатам сравнения:

t1 := time.Now()
t2 := t1.Add(10 * time.Minute)
cmp := t2.Compare(t1)
fmt.Println(cmp)
// 1

В целом, отличный релиз! Уже хочется попробовать в продакшене.

Подписывайтесь на канал, чтобы не пропустить новые заметки 🚀