Thank Go!


Гео и язык канала: Россия, Русский
Категория: Технологии


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

Связанные каналы  |  Похожие каналы

Гео и язык канала
Россия, Русский
Категория
Технологии
Статистика
Фильтр публикаций


Шпаргалка по многозадачности

Подготовил интерактивную памятку по многозадачности в Go:

— Горутины, каналы, селект.
— Конвейеры, время, контекст.
— Группы ожидания, гонки, мьютексы.
— Семафоры, сигналы, атомики.
— Тестирование, внутрянка, диагностика.

Все на англ. Делать на русском лень, простите.

Есть и PDF-версия.

https://antonz.org/go-concurrency-distilled


Множественный разврат возврат

Сколько значений можно вернуть из функции через multi-return? Да сколько угодно!

Например, функция readFloat в стандартной библиотеке возвращает семь штук.

Вот это я понимаю, функциональное программирование 😁


Ошибка ошибка ошибка ошибка

Это выражение выглядит странновато, но оно полностью корректно:


error.Error(errors.ErrUnsupported)


errors.ErrUnsupported — реальная, хотя и не слишком известная ошибка в пакете errors:


var ErrUnsupported = New("unsupported operation")


error — реальное название интерфейсного типа, а Error — метод на этом интерфейсе:


type error interface {
Error() string
}


Метод любого типа можно вызвать как обычную функцию, так что эти два вызова эквивалентны:


var err = errors.New("oops")
err.Error() // "oops"
error.Error(err) // "oops"


В результате получается так:


s := error.Error(errors.ErrUnsupported)
println(s)
// unsupported operation


🤓




JSON v2 в Go 1.27

Вторая версия json-пакета впервые появилась как экспериментальная в Go 1.25. Два релиза ее обкатывали, и наконец окончательно выпустили в 1.27.

В связи с этим актуализировал свой гайд по отличиям между json и json/v2 (их много!).

Все разобрано на конкретных примерах:

https://habr.com/ru/articles/923404/


strings.CutLast

Из небольших изменений в Go 1.27 я больше всего рад strings.CutLast. Эта функция делит строку по последнему вхождению разделителя.

Конечно, реализовать ее несложно и самому:


// CutLast делит s на части до и после последнего вхождения sep.
func CutLast(s, sep string) (before, after string, found bool) {
if i := strings.LastIndex(s, sep); i >= 0 {
return s[:i], s[i+len(sep):], true
}
return s, "", false
}


Но приятно, что теперь делать этого не придется:


s := "go|for|it"
_, last, ok := strings.CutLast(s, "|")
fmt.Println(last, ok)
// it true


Интересно, что Cut добавили еще в 1.20, а CutLast почему-то пробыла в немилости аж 7 релизов :)


Generic-методы и их последствия

Думаю, «джавафикация» экосистемы Go только усилится с появлением в 1.27 дженерик-методов (это методы, у которых может быть собственный параметр типа).


// Sum - сумма целых чисел.
type Sum[T int | int64] struct {
v T
}

// Add добавляет число к сумме, приводя его к целому, если нужно.
func (s *Sum[T]) Add[V int | int64 | float64](v V) *Sum[T] {
s.v += T(v)
return s
}


В этом примере T — параметр типа на структуре, а V — параметр типа на методе. В предыдущих версиях Go параметры вроде V приводили к ошибке "method must have no type parameters", а теперь будут разрешены.


var s Sum[int]
s.Add(30).Add(10).Add(2.3)
fmt.Println(s.Val())
// 42


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

2.5k 0 10 24 27

Тур по Go 1.27

Со дня на день выйдет Go 1.27, так что самое время почитать, что там внутри.

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

Краткое содержание для ленивых:

— Generic-методы с собственными (не привязанными к типу) параметрами
— Компактные литералы для embedded-структур
— Диагностика утечек горутин в продакшене (было experiment в 1.26)
— UUID v4 и v7
— JSON v2 в продакшене (был experiment в 1.26)
— Универсальный SIMD (experiment)
— strings.CutLast (давно о ней мечтал)
— Интерфейс maphash.Hasher
— synctest.Sleep (= time.Sleep + synctest.Wait)

https://victoriametrics.com/blog/go-1-27


Делаем себе сложно

Хотел я вам написать, как можно начать с очень простой реализации Backward, и прийти к чему-то похожему на стдлибу.

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

Вам же заметку рекомендую — в ней есть jira, жабы, и нелепый авторский юмор.

https://habr.com/ru/articles/1065634


Смотрите какую гусеницу красивую нашел: func Backward[Slice ~[]E, E any](s Slice) iter.Seq2[int, E]
Опрос
  •   Весьма элегантно
  •   Страшновато, но что поделать
  •   Гадость какая
  •   Ты это выдумал, удали
141 голосов


Что внутри у интерфейса

Многие знают, что интерфейс в Go реализован как обертка, внутри которой два указателя — на конкретное значение и на его тип.

Но строго говоря, все несколько сложнее.

Вот интерфейс:

type iface struct {
tab *abi.ITab
data unsafe.Pointer
}

data — это действительно указатель. Если мы заворачиваем в интерфейс значение, в data будет указатель на него (а само значение переедет в кучу при необходимости).

Если же заворачиваем указатель (например *os.File в io.Reader), то в data будет ровно этот самый указатель.

А вот tab — не просто конкретный тип, а целая пачка метаданных:

type ITab struct {
Inter *InterfaceType
Type *Type
Hash uint32
Fun [1]uintptr
}

Inter — это тип интерфейса (например, io.Reader), Type — конкретный тип (например, *os.File).

Hash — это хеш от конкретного типа. Он используется для быстрого выбора подходящей ветки в переключателях типа (type switch). О нем еще поговорим отдельно.

А Fun — это массив указателей на методы типа, которые реализуют интерфейс (например, (*os.File).Read).

Из объявления Fun [1]uintptr может показаться, что у интерфейса может быть только один метод (что, как мы знаем, не так). Но на самом деле это фикция. Указателей будет ровно столько, сколько методов у интерфейса, просто Fun[0] — первый из них.

Ах да, пустой интерфейс (any) реализован иначе. О нем тоже отдельно поговорим.

Уфф.


Метод-выражение

Все, конечно, знают, что такое метод структуры:

type counter struct {
val uint
}

func (c *counter) inc() {
c.val++
}

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

// реализованы одинаково
func (c *counter) inc()
func inc(c *counter)

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

inc := (*counter).inc
c := new(counter)
inc(c)
inc(c)
inc(c)
println(c.val)
// 3

Называется такое чудо «метод-выражение» (method expression).

P.S. А бывает еще метод-значение — это более причудливый зверь, замыкание на получателе.


Рубрика «ненормальное программирование»

Запихиваем максимальное количество встроенных функций в одно выражение:

println(max(
len(append(make([]int, 1, 2), *new(int))),
cap(make(chan struct{}, 3)),
copy(make([]byte, 5), "hello"),
min(int(real(complex(1, 2))), int(imag(complex(3, 4)))),
map[any]int{}[recover()],
))

Здесь их 13 🤓 В порядке появления:

println
max
len
append
make
new
cap
copy
min
real
complex
imag
recover

Для большего погружения, попробуйте завтра писать код в таком стиле, и обязательно отправьте его на ревью.


18 встроенных функций Go

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

make создает новый срез, карту или канал.
new аллоцирует память под переменную.

append добавляет одно или несколько значений в срез.
copy копирует элементы из одного среза в другой (или байты из строки в срез).
delete удаляет элемент из карты.
clear очищает срез или карту.

len и cap возвращают длину/емкость контейнера (строка, срез, карта, канал).

close закрывает канал.

min и max возвращают минимальный/максимальный из аргументов.

complex, real и imag собирают и разбирают комплексные числа.

panic выбрасывает панику, а recover ее ловит.

print и println печатают аргументы.

Можете ради интереса посчитать, сколько из них вы реально используете :)


Встроенные функции

Все регулярно используют встроенные функции (built-in). Это функции, которые не требуют импорта пакета, вроде len, make или panic.

Но что такое вообще встроенная функция? Например, len в документации заявлена как часть пакета builtin:

func len(v Type) int

Во-первых, что это еще за builtin-пакет и тип Type в нем?

Во-вторых, len явно дженерик, так где же наша любимая абракадабра вида len[S ~[]E, E any](x S), ну или хотя бы len[T any](s []T)?

И как вообще это добро работало начиная с версии Go 1.0, когда никаких дженериков даже на горизонте не было?

Суровая правда в том, что len — вообще не функция. У нее нет собственного фрейма на стеке, как у обычных функций, и других присущих им особенностей.

На самом деле, компилятор превращает вызов len(s) в прямое чтение s.len, где len — это обычное поле структуры среза (или аналогичное поле строки, карты или канала):

type slice struct {
array unsafe.Pointer
len int
cap int
}

Не все билтины настолько просты. Например, make и panic компилируются в реальные вызовы функций (runtime.makeslice, runtime.gopanic) — с фреймом и всем полагающимся.

Общее у всех встроенных функций одно: компилятор не вызывает их буквально как написано, а подставляет вместо этого что-то свое.

Что касается Type, то это просто заглушка. Пакет builtin не компилируется и существует только ради документации.

Такое вот коварство.


Функциональные опции

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

И особенно я не рад, что он пробрался в стдлибу вместе с новым пакетом json/v2 (финализирован в Go 1.27):

type Options interface { ... }

func Deterministic(v bool) Options
func FormatNilMapAsNull(v bool) Options
func FormatNilSliceAsNull(v bool) Options
// ...

func Marshal(in any, opts ...Options) (out []byte, err error)

Обычная структура (см. скриншот) лучше во всех отношениях, на мой взгляд.

А вы что думаете?


string ⇄ []byte

Все знают, как преобразовать байтовый срез в строку и обратно:

b := []byte{...}
s := string(b) // 1

s := "..."
b := []byte(s) // 2

И ➀ string(b) и ➁ []byte(s) создают копию данных длиной len(x) — на стеке или в куче, в зависимости от ситуации.

Цикл с преобразованием string(b) тоже создает копию:

for _, r := range string(b) {
...
}

А вот цикл с преобразованием []byte(s) копию никогда не создает — итерация идет прямо по байтам оригинальной строки:

for _, ch := range []byte(s) {
...
}

Такая оптимизация компилятора.


Флаг -race

Флаг -race включает проверку на гонки в коде (потенциальный конфликт при доступе к общим данным из нескольких горутин). Если при выполнении рантайм видит гонку, он пишет, где именно та произошла.

Несложно догадаться, что проверка эта небесплатная. С включенным детектором гонок приложение использует в 5–10 раз больше памяти, а работает в 2–20 раз медленнее.

Надо ли собирать продакшен с включенным -race? Разве что если в ваши планы входит потом «оптимизировать» производительность в 10 раз и получить за это почет и уважение. Ну или по башке, если кто-то проверит, что именно вы там наоптимизировали 😅

3k 0 10 2 39

Надо ли собирать приложение с флагом --race для продакшена?
Опрос
  •   Да, чтобы гонок не было.
  •   Да, чтобы метрики собирались.
  •   Да, чтобы горутины быстрее работали.
  •   Нет.
477 голосов


Грязный make

А хотите по-настоящему злую штуку? В стандартной библиотеке есть функция, которая создает срез байт, не зануляя память:

// MakeNoZero makes a slice of length n
// and capacity of at least n bytes
// without zeroing the bytes.
// It is the caller's responsibility
// to ensure uninitialized bytes
// do not leak to the end user.
func MakeNoZero(n int) []byte

Функция находится в пакете internal/bytealg, так что напрямую ее не вызвать. Но через go:linkname — можно:

//go:linkname makeNoZero internal/bytealg.MakeNoZero
func makeNoZero(n int) []byte

func main() {
data := makeNoZero(10)
// ...
}

Никогда так не делайте 😈

Показано 20 последних публикаций.