Go 1.24: Больше итераторов
Как известно, в 1.23 авторы Go воспылали необъяснимой стратью к итератором, и этот пожар с тех пор только разгорается жарче.
Вот притащили еще горстку в пакете strings.
Lines итерирует по строкам, разделенным \n:
SplitSeq итерирует по частям, разделенным произвольным разделителем:
SplitAfterSeq как SplitSeq, но делит после разделителя:
FieldsSeq итерирует по частям, разделенным пробельными символами (unicode.IsSpace) и их последовательностями:
FieldsFuncSeq как FieldsSeq, но логику «пробельных» символов определяете вы сами:
Ровно такие же итераторы добавили в пакет bytes.
Редакция Thank Go увлечение итераторами решительно осуждает 😑
Как известно, в 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 увлечение итераторами решительно осуждает 😑
Go 1.24: SHA-3 и его друзья
Новый пакет crypto/sha3 реализует хеш-функцию SHA-3 и другую криптографическую хурму, про которую вам вряд ли интересно знать (смотрите FIPS 202 если вдруг интересно):
И еще несколько crypto-пакетиков:
— crypto/hkdf по RFC 5869
— crypto/pbkdf2 по RFC 8018
—crypto/meme для генерации мем-коинов
Новый пакет 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
—
Go 1.24: пропуск нулевых значений в JSON
Новая опция
Вообще у нас уже был для этого
Вот omitempty:
А вот omitzero:
Если у типа есть метод
Если метода нет, используется стандартное понятие нулевого значения (0 для целого, "" для строки, и так далее).
Наверно, стоило бы сразу задизайнить так omitempty, но кто же знал :)
Новая опция
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, но кто же знал :)
Странный пингер
Допустим, у нас есть сервер, который можно тыкать палочкой:
Давайте попингуем его вот так:
Как думаете, что произойдет?
Опрос следует (чур в комментах не спойлерить)
Допустим, у нас есть сервер, который можно тыкать палочкой:
type Server struct {
nPings int
}
func (s *Server) Ping() {
s.nPings++
}
Давайте попингуем его вот так:
s := Server{}
ping := s.Ping // хм
ping()
ping()
ping()
Как думаете, что произойдет?
Опрос следует (чур в комментах не спойлерить)
Что будет при таком вызове ping?
Final Results
18%
Ругань компилятора
11%
Паника в рантайме
15%
Паника у меня
56%
Увеличится s.n_pings
Метод-значение
Это вы конечно молодцы, что так хорошо знаете возможности языка!
Действительно, метод у значения структуры (или указателя на значение) — это просто функция с конкретным получателем (тем самым значением структуры). Такой метод-значение (method value) можно использовать как обычную функцию — вызывать напрямую или передавать в качестве параметра, например.
Предположим, у нас есть тип-монитор, который умеет периодически проверять доступность сервиса, ведет историю проверок, считает процент доступности и выполняет другие полезные действия.
Монитору совершенно не нужно знать о конкретной реализации сервиса. Достаточно получить в конструкторе функцию-пинг, и дальше вызывать ее:
И если у нашего сервера есть подходящий по сигнатуре метод:
То можно прямо его и передать без всяких оберток:
Такие вот элементы функционального программирования в глубоко процедурном языке :)
Это вы конечно молодцы, что так хорошо знаете возможности языка!
Действительно, метод у значения структуры (или указателя на значение) — это просто функция с конкретным получателем (тем самым значением структуры). Такой метод-значение (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)
Такие вот элементы функционального программирования в глубоко процедурном языке :)
Go 1.24: Случайный текст
Небольшое, но весьма приятное дополнение стандартной библиотеки.
Появилась функция crypto/rand.Text, которая возвращает криптографически случайную строку:
Результат содержит как минимум 128 случайных бит. Этого достаточно для защиты от атак перебором, а вероятность коллизий крайне мала. Так что функция отлично подходит для генерации секретных ключей, паролей, и тому подобного добра.
Использует алфавит Base32.
Небольшое, но весьма приятное дополнение стандартной библиотеки.
Появилась функция crypto/rand.Text, которая возвращает криптографически случайную строку:
text := rand.Text()
fmt.Println(text)
// 4PJOOV7PVL3HTPQCD5Z3IYS5TC
Результат содержит как минимум 128 случайных бит. Этого достаточно для защиты от атак перебором, а вероятность коллизий крайне мала. Так что функция отлично подходит для генерации секретных ключей, паролей, и тому подобного добра.
Использует алфавит Base32.
UUIDv7
Вы наверняка слышали про UUID — уникальные 128-битные идентификаторы, которые генерируются случайным образом.
Они бывают разных версий, самая популярная из которых — UUIDv4. Обычно, когда говорят о uuid, имеют в виду именно ее. Но не так давно в стандарт добавили новую версию — UUIDv7.
В отличие от v4, UUIDv7 объединяет таймштамп и случайную часть, поэтому айдишники упорядочены по времени с точностью до 1 миллисекунды. Отлично подходит для идентификаторов записей в базах данных, в том числе распределенных.
Вот как выглядит UUIDv7 в виде строки:
128-битное значение складывается из нескольких частей:
— timestamp (48 бит) — юниксовое время в мс.
— ver (4 бит) — версия UUID (7).
— rand_a (12 бит) — случайные биты.
— var (2 бита) — равно 10.
— rand_b (62 bits) — случайные биты.
Интересно, что весь алгоритм UUIDv7 реализуется в пару десятков строк кода на Go. Рассмотрим их следующим постом.
Вы наверняка слышали про 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. Рассмотрим их следующим постом.
UUIDv7 на Go
Давайте реализуем алгоритм генерации значения UUIDv7 с нуля, средствами стандартной библиотеки.
Генерим 16 случайных байт (128 бит):
Получаем текущее время в миллисекундах и записываем его в значение UUID:
Наконец, заполняем версию и вариант:
Вот и все, uuid готов!
песочница
Давайте реализуем алгоритм генерации значения 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 готов!
песочница
if-err
Есть три школы написания Go-кода.
Наглядная:
Краткая:
Беспечная:
К какой относитесь вы?
Мое мнение: когда изучаешь Go, выбор в пользу краткой кажется очевидным. Но чем больше работаешь с кодом, тем больше замечаешь, что она плохо читается (фокус на if вместо вызова doSmth). Поэтому я перешел на наглядную.
А беспечную люблю использовать в одноразовых скриптах 😈
Есть три школы написания Go-кода.
Наглядная:
err := doSmth()
if err != nil {
return err
}
Краткая:
if err := doSmth(); err != nil {
return err
}
Беспечная:
doSmth()
К какой относитесь вы?
Мое мнение: когда изучаешь Go, выбор в пользу краткой кажется очевидным. Но чем больше работаешь с кодом, тем больше замечаешь, что она плохо читается (фокус на if вместо вызова doSmth). Поэтому я перешел на наглядную.
А беспечную люблю использовать в одноразовых скриптах 😈
Возведение в степень
В Go нет оператора для возведения в степень. Я не знаю, почему.
Я мог бы рассказать вам, что команда 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
Мда.
Как сделать бип
Поднимать серьезные темы сразу после праздников было бы странно, поэтому сегодня мы поговорим о том, как дзынькать из Go.
Вот так (готов поспорить, что проще программы вы не видели):
Делает дзынь во многих терминалах (вроде даже в cmd.exe в windows).
Живите теперь с этим.
Поднимать серьезные темы сразу после праздников было бы странно, поэтому сегодня мы поговорим о том, как дзынькать из Go.
Вот так (готов поспорить, что проще программы вы не видели):
print("\a")
Делает дзынь во многих терминалах (вроде даже в cmd.exe в windows).
Живите теперь с этим.
Проверяем интернет
До недавнего времени я не знал, что у гугла есть специальный быстрый урл, который можно использовать для проверки доступности интернета (а вы знали?):
Доступность сервисов гугла на него не влияет, так что урл отвалится только если гугл ляжет целиком (ну или если его забанит самизнаетекто).
Пример проверки:
Урл даже доступен по http, чтобы не тратить время на установку шифрованного соединения. Но и по https тоже можно вызвать, конечно.
Хорошо придумали.
До недавнего времени я не знал, что у гугла есть специальный быстрый урл, который можно использовать для проверки доступности интернета (а вы знали?):
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 тоже можно вызвать, конечно.
Хорошо придумали.
А вы понимаете, что здесь происходит?
http.DefaultTransport.(*http.Transport)
http.DefaultTransport.(*http.Transport)
Final Results
22%
Конечно!
30%
Примерно да
21%
Не совсем
26%
Дичь какая-то
http.DefaultTransport
Выражение
Думаю, по поводу
— Функции (например,
— Типы (например,
— Константы (например,
Реже пакеты экспортируют переменные. Как правило, это ошибки (например,
Поскольку транспорт в пакете http представлен типом
Но на самом деле он выглядит так:
Хотя де-факто
Выражение
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
. Разберем эту загадку в следующем посте.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 этого достаточно.Разберем эту загадку в следующем посте (я это уже говорил, но... 😅)
Почему RoundTripper?
Почему же
Вы не найдете ответа на этот вопрос в официальной документации, так что вот моя версия.
➊ Такое объявление явно декларирует, что
➋ Страховка на будущее. Если
➌ Объявив
Вот только на практике это не так.
А как — в следующем посте (длинная серия получилась, вы уж простите 🤷♀️)
Почему же
DefaultTransport
объявлен как интерфейс RoundTripper
, а не как конкретный тип *Transport
, хотя по факту он содержит именно *Transport
?var DefaultTransport RoundTripper = &Transport{...}
Вы не найдете ответа на этот вопрос в официальной документации, так что вот моя версия.
➊ Такое объявление явно декларирует, что
DefaultTransport
соответствует ожиданиям Client
— тот ведь хочет видеть именно RoundTripper
. Но DefaultTransport
и так соответствует RoundTripper
— явное указание интерфейса в Go не требуется.➋ Страховка на будущее. Если
*Transport
перестанет соответствовать интерфейсу RoundTripper
, код не скомпилируется. Строго говоря, и это не обязательно — клиент уже использует DefaultTransport
как RoundTripper
во внутреннем методе transport
.➌ Объявив
DefaultTransport
интерфейсом, разработчики стандартной библиотеки оставили себе (теоретическую) возможность заменить в будущем реализацию (условно, сделать вместо Transport
новый MagicTransport
), не сломав обратную совместимость.Вот только на практике это не так.
А как — в следующем посте (длинная серия получилась, вы уж простите 🤷♀️)
Протекший транспорт
На практике во многих проектах
Если разработчики стдлибы заменят реализацию
На мой взгляд, объявление
А проверку на соответствие интерфейсу можно было бы сделать явной:
Такие дела.
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)
Такие дела.
Синтаксического сахара в обработке ошибок не будет
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навсегда надолго останется уродливой и многословной. Ровно такой, какая она и должна быть 😁
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