2615
Здравый взгляд на язык программирования Go. Злой админ @nalgeon. Добрый админ @mikeberezin. Рекламы нет.
http.DefaultTransport
Выражение 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. Разберем эту загадку в следующем посте.
Читать полностью…
Как сделать бип
Поднимать серьезные темы сразу после праздников было бы странно, поэтому сегодня мы поговорим о том, как дзынькать из Go.
Вот так (готов поспорить, что проще программы вы не видели):
print("\a")
if-err
Есть три школы написания Go-кода.
Наглядная:
err := doSmth()
if err != nil {
return err
}
if err := doSmth(); err != nil {
return err
}doSmth()
UUIDv7
Вы наверняка слышали про UUID — уникальные 128-битные идентификаторы, которые генерируются случайным образом.
Они бывают разных версий, самая популярная из которых — UUIDv4. Обычно, когда говорят о uuid, имеют в виду именно ее. Но не так давно в стандарт добавили новую версию — UUIDv7.
В отличие от v4, UUIDv7 объединяет таймштамп и случайную часть, поэтому айдишники упорядочены по времени с точностью до 1 миллисекунды. Отлично подходит для идентификаторов записей в базах данных, в том числе распределенных.
Вот как выглядит UUIDv7 в виде строки:
0190163d-8694-739b-aea5-966c26f8ad91
└─timestamp─┘ │└─┤ │└───rand_b─────┘
ver │var
rand_a
Go 1.24: Случайный текст
Небольшое, но весьма приятное дополнение стандартной библиотеки.
Появилась функция crypto/rand.Text, которая возвращает криптографически случайную строку:
text := rand.Text()
fmt.Println(text)
// 4PJOOV7PVL3HTPQCD5Z3IYS5TC
Странный пингер
Допустим, у нас есть сервер, который можно тыкать палочкой:
type Server struct {
nPings int
}
func (s *Server) Ping() {
s.nPings++
}s := Server{}
ping := s.Ping // хм
ping()
ping()
ping()
Go 1.24: SHA-3 и его друзья
Новый пакет 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
Go 1.24: Заглушить логи
Мы уже как-то обсуждали несложный способ создать бесшумный логгер (например, для тестов) — использовать slog.TextHandler в связке с io.Discard:
log := slog.New(
slog.NewTextHandler(io.Discard, nil),
)
log.Info("Prints nothing")
log := slog.New(slog.DiscardHandler)
log.Info("Prints nothing")
Go 1.24: Синтетическое время для тестов
Пусть у нас есть функция, которая таймаутит, если не получила значение из канала в течение минуты:
func Read(in chan int) (int, error) {
select {
case v := <-in:
return v, nil
case <-time.After(60 * time.Second):
return 0, errors.New("timeout")
}
}ch := make(chan int)
go func() {
ch <- 42
}()
val, err := Read(ch)
fmt.Printf("val=%v, err=%v\n", val, err)
// val=42, err=<nil>
func TestReadTimeout(t *testing.T) {
synctest.Run(func() {
ch := make(chan int)
_, err := Read(ch)
if err == nil {
t.Fatal("expected timeout error, got nil")
}
})
}
Go 1.24: os.Root
Новый тип os.Root ограничивает операции с файловой системой определенной директорией.
Функция OpenRoot открывает директорию и возвращает Root:
dir, err := os.OpenRoot("data")
fmt.Println(dir.Name(), err)
// data <nil>file, err := dir.Open("01.txt")
fmt.Println(file.Name(), err)
// data/01.txt <nil>
file, err = dir.Open("../main.txt")
fmt.Println(err)
// openat ../main.txt: path escapes from parentfile, err := dir.Create("new.txt")
stat, err := dir.Stat("02.txt")
err = dir.Remove("03.txt")dir, err := os.OpenRoot(path)
defer dir.Close()
// do stuff
Go 1.24: Конкурентно-безопасная карта
Лирическое отступление. Обычная карта в Go никогда не скукоживается, только растет пока не поглотит вселенную. GC не освобождает память, занятую самой картой, даже если удалять из нее элементы. Не изменилось это и в новой «швейцарской» карте в Go 1.24.
Но есть в Go еще одна карта, конкурентно-безопасная (sync.Map). И по странному стечению обстоятельств, в Go 1.24 у нее тоже новая реализация! Теперь она основана на concurrent hash-trie (помесь хэш-таблицы и префиксного дерева) и работает быстрее, особенно при модификациях карты.
Кроме того, новая sync.Map лучше освобождает память, чем предыдущая. Та тоже умела это делать, но там использовалась «поколенческая» модель, и память собиралась с запаздыванем. В новой никаких поколений нет, и память освобождается по мере удаления элементов.
Исходно новую расчудесную карту сделали для пакета unique в Go 1.23 — там как раз нужен был конкурентно-безопасный кэш. А теперь заметили, что и для пакета sync новая реализация отлично подходит. В результате sync.Map теперь по сути фасад к HashTrieMap.
Если вы страшный ретроград, вернуться к старой sync.Map можно через переменную GOEXPERIMENT=nosynchashtriemap при сборке.
Карточный релиз какой-то получается!
Go 1.24: Улучшенная очистка
Это последняя заметка с жестью, дальше пойдут более человеколюбивые фичи, чесслово :)
Помните наш блоб?
b := newBlob(1000)
fmt.Printf("b=%v, type=%T\n", b, b)
b=Blob(1000 KB), type=*main.Blob
runtime.SetFinalizer, который сложно использовать. Теперь есть его улучшенная версия — runtime.AddCleanup: func main() {
b := newBlob(1000)
now := time.Now()
// Регистрируем функцию, которую рантайм
// вызовет после сборки памяти объекта b.
runtime.AddCleanup(b, cleanup, now)
time.Sleep(10 * time.Millisecond)
b = nil
runtime.GC()
time.Sleep(10 * time.Millisecond)
}func cleanup(created time.Time) {
fmt.Printf(
"object is cleaned up! lifetime = %dms\n",
time.Since(created)/time.Millisecond,
)
}object is cleaned up! lifetime = 10ms
Go 1.24: Псевдонимы generic-типов
Приближается февраль, а вместе с ним релиз Go 1.24. Ну а мы начинаем марафон новых фич и изменений!
Сначала напоминалка: псевдоним типа (type alias) в Go создает синоним для типа, не создавая новый тип.
Когда тип определен на основе другого типа, типы отличаются:
type ID int
var n int = 10
var id ID = 10
id = ID(n)
fmt.Printf("id is %T\n", id)
id is main.ID
type ID = int
var n int = 10
var id ID = 10
id = n // works fine
fmt.Printf("id is %T\n", id)
id is int
Set как generic-псевдоним для map с логическими значениями (не то чтобы это было сильно полезно, но):type Set[T comparable] = map[T]bool
set := Set[string]{"one": true, "two": true}
fmt.Println("'one' in set:", set["one"])
fmt.Println("'six' in set:", set["six"])
fmt.Printf("set is %T\n", set)
'one' in set: true
'six' in set: false
Set is map[string]bool
Нас ждет sync/v2
Один из core-разработчиков Go внес предложение добавить вторую версию пакета sync.
В ней будут wait-группы, мьютексы и условные переменные (как в v1), плюс generic-версии Map и Pool.
На мой взгляд, v2-версии пакетов стдлибы это большое зло, потому что они по сути умножают размер интерфейса пакета в два раза. Надо знать и уметь работать как с v1, так и v2, да еще чего доброго в каких-то кодовых базах будут использоваться обе версии.
Но когда я озвучил свои сомнения, то получил ответ, из которого однозначно следует, что sync/v2 будет принят.
Так что радуйтесь, любители перемен!
Скорость алгоритмов: O(1)
O(1), так же известно как «константное время». Самый лучший вариант, скорость алгоритма не зависит от количества котиков входных данных.
🐾 Пример
Вы — счастливый обладатель N котиков. Каждый котик знает, как его зовут. Если позвать «Феликс!», то прибежит только он, а остальным N-1 жопкам пофиг.
🐈 🐈 🐈 🐈 🐈
🐈 🐈⬛ 🐈 🐈 🐈
↓
мяу!
(first + i*size) — а значит, доступ к i-му элементу выполняется за константное время. Изменение i-го элемента аналогично — тоже O(1).s := make([]int, 1e6)
i := rand.IntN(1e6)
s[i] = 42 // O(1)
fmt.Println(s[i]) // O(1)
m["answer"] = 42, то Go превращает "answer" в целое число (хеш строки), после чего считает по нему нужный индекс в массиве. По этому индексу и находится значение 42.d := map[string]int{}
d["answer"] = 42 // O(1)
fmt.Println(d["answer"]) // O(1)
Проверяем интернет
До недавнего времени я не знал, что у гугла есть специальный быстрый урл, который можно использовать для проверки доступности интернета (а вы знали?):
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
}
Возведение в степень
В 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
UUIDv7 на Go
Давайте реализуем алгоритм генерации значения UUIDv7 с нуля, средствами стандартной библиотеки.
Генерим 16 случайных байт (128 бит):
var val [16]byte
_, err := rand.Read(val[:])
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
(воскресное несерьезное)
Ты: хм, какой бы перспективный язык выучить? Питон? Котлин? Может Сишарп?
Вселенная:
Метод-значение
Это вы конечно молодцы, что так хорошо знаете возможности языка!
Действительно, метод у значения структуры (или указателя на значение) — это просто функция с конкретным получателем (тем самым значением структуры). Такой метод-значение (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: пропуск нулевых значений в JSON
Новая опция 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"}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 — именно он используется при маршалинге, чтобы определить, нулевое значение или нет.
Go 1.24: Больше итераторов
Как известно, в 1.23 авторы Go воспылали необъяснимой стратью к итератором, и этот пожар с тех пор только разгорается жарче.
Вот притащили еще горстку в пакете strings.
Lines итерирует по строкам, разделенным \n:
s := "one\ntwo\nsix"
for line := range strings.Lines(s) {
fmt.Print(line)
}
// one
// two
// six
s := "one-two-six"
for part := range strings.SplitSeq(s, "-") {
fmt.Println(part)
}
// one
// two
// six
s := "one-two-six"
for part := range strings.SplitAfterSeq(s, "-") {
fmt.Println(part)
}
// one-
// two-
// six
s := "one two\nsix"
for part := range strings.FieldsSeq(s) {
fmt.Println(part)
}
// one
// two
// six
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
Go 1.24: Контекст для тестов
Допустим, мы хотим протестировать этот жутко полезный сервер:
// Дает ответы на все вопросы.
type Server struct{}
// Возвращает ответ сервера.
func (s *Server) Get(query string) int {
return 42
}
// Запускает сервер. Остановка через отмену контекста.
func startServer(ctx context.Context) *Server {
go func() {
select {
case <-ctx.Done():
// Освобождаем ресурсы.
}
}()
return &Server{}
}
func Test(t *testing.T) {
srv := startServer(context.Background())
if srv.Get("how much?") != 42 {
t.Fatal("unexpected value")
}
}func Test(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
srv := startServer(ctx)
if srv.Get("how much?") != 42 {
t.Fatal("unexpected value")
}
}func Test(t *testing.T) {
srv := startServer(t.Context())
if srv.Get("how much?") != 42 {
t.Fatal("unexpected value")
}
}
Go 1.24: B.Loop
Вы наверняка знакомы с циклом в бенчмарках (for range b.N):
var sink int
func BenchmarkSlicesMax(b *testing.B) {
// Setup the benchmark.
s := randomSlice(10_000)
b.ResetTimer()
// Run the benchmark.
for range b.N {
sink = slices.Max(s)
}
}
func BenchmarkSlicesMax(b *testing.B) {
// Setup the benchmark.
s := randomSlice(10_000)
// Run the benchmark.
for b.Loop() {
slices.Max(s)
}
}
Скукоживание карт в Go
Вижу, не все могут поверить в коварство гошной карты, которая делает кусь не отдает память.
Характерный комментарий:
Если есть сервер на го с сессиями которые реализованы в виде мапы, то даже после отключения клиентов и удаления ключей из нее память не будет освобождаться?🤌
Штош. Давайте разбираться.
Вот наш клиент с идентификатором и телом в 40 байт:
type Client struct {
id uint64
body [40]byte
}printAlloc("initial")
m := make(map[int]Client)
for i := range 10000 {
m[i] = Client{id: uint64(i)}
}
runtime.GC()
printAlloc("after create")initial: heap size = 109 KB
after create: heap size = 1110 KB
for i := range 10000 {
delete(m, i)
}
runtime.GC()
printAlloc("after delete")after delete: heap size = 1110 KB
m := make(map[int]*Client)
for i := range 10000 {
m[i] = &Client{id: uint64(i)}
}
for i := range 10000 {
delete(m, i)
}
after create: heap size = 898 KB
after delete: heap size = 429 KB
type Client struct {
id uint64
body [1024]byte
}m := make(map[int]Client)
for i := range 10000 {
m[i] = Client{id: uint64(i)}
}
for i := range 10000 {
delete(m, i)
}
after create: heap size = 11683 KB
after delete: heap size = 434 KB
Go 1.24: Швейцарские таблицы 🇨🇭
Спустя много лет команда Go решила изменить реализацию map!
Теперь она основана на SwissTable и предлагает несколько оптимизаций (со слов разработчиков, лично не проверял):
— Чтение и запись в больших картах (>1024 записей) быстрее на ~30%.
— Запись в аллоцированных картах (с установленной емкостью) быстрее на ~35%.
— Итерация в целом быстрее на ~10%, для карт с низкой наполненностью (большая емкость, мало записей) на ~60%.
Вернуться к старой реализации можно через переменную окружения GOEXPERIMENT=noswissmap при сборке (надо сказать, что не все остались довольны новыми картами).
Если интересно, вот исходники.
Слабые указателиЕсли вы сильный программист, то не читайте дальше
Слабый указатель (пакет weak) ссылается на объект, как обычный указатель. Но в отличие от обычного указателя, слабый указатель не способен удержать объект в памяти. Если на объект ссылаются только слабые указатели, сборщик мусора может освободить занимаемую им память.
Предположим, у нас есть тип blob (реализация скрыта для краткости):
// Blob is a large byte slice.
type Blob []byte
b := newBlob(1000)
weak.Pointer) из обычного с помощью weak.Make. А получить доступ к оригинальному указателю поможет Pointer.Value:wb := weak.Make(newBlob(1000))
fmt.Println(wb.Value())
// Blob(1000 KB)
b := newBlob(1000)
fmt.Println("before GC =", b)
runtime.GC()
fmt.Println("after GC =", b)
before GC = Blob(1000 KB)
after GC = Blob(1000 KB)
wb := weak.Make(newBlob(1000))
fmt.Println("before GC =", wb.Value())
runtime.GC()
fmt.Println("after GC =", wb.Value())
before GC = Blob(1000 KB)
after GC = <nil>
Pointer.Value возвращает nil, если сборщик мусора уже освободил значение по указателю.
Будущее стдлибы
По-хорошему, у каждого предложения на доработку (языка или стдлибы) должно быть обоснование. В нем описывают существующую проблему, варианты решения, и аргументируют, что предлагаемый вариант лучше альтернатив («ничего не делать» тоже альтернатива).
Забавно, что в качестве обоснования для создания sync/v2 исходно было указано буквально следующее:
> Мы успешно реализовали math/rand/v2, давайте теперь еще sync/v2 запилим.
Это, мягко говоря, не то что ожидаешь видеть для крупной доработки. Когда я указал на это Яну (автор предложения и один из основных участников команды разработки Go), он добавил подробностей.
Из них однозначно считывается, что если в пакете стдлибы используется any вместо дженериков — это достаточное основание сделать v2-пакет.
Как вы понимаете, почти все пакеты стдлибы сделаны до появления в языке дженериков. Соответственно, я делаю вывод, что нас ожидает еще много v2-пакетов.
Инжой.
Заглушить логи
С появлением пакета log/slog в Go 1.21 наконец-то стало возможно нормально вести журналы без использования внешних библиотек.
Если еще не пробовали, то рекомендую простенький туториал и доку, она понятная и с примерами.
log := slog.New(
slog.NewTextHandler(os.Stdout, nil),
)
log.Info("operation", "count", 3, "took", 50*time.Millisecond)
time=2024-12-09T10:20:47.660+00:00 level=INFO msg=operation count=3 took=50ms
slog, а о том, как в нем заглушить логи (например, для тестов или бенчмарков).io.Discard в качестве приемника для slog.TextHandler:log := slog.New(
slog.NewTextHandler(io.Discard, nil),
)
log.Info("Prints nothing")
Скорость алгоритмов: O-нотация
Скорость работы алгоритмов принято оценивать в О-нотации: у медленного она «о», у более быстрого «ооо», у самого быстрого «ооооо ваще огонь».
Вру, конечно.
На самом деле, скорость оценивают в количестве действий, которое выполняет алгоритм. Например, если у вас дома 10 котов, и нужно определить самого увесистого, то придется взвесить каждого кота, то есть выполнить 10 операций.
Алгоритм, понятное дело, должен работать одинаковым образом что для 10 котиков, что для 100, что для 10000. Оценка же скорости нужна одна. Поэтому оценивают не конкретное количество операций, а количество операций относительно количества входных данных (котов в нашем случае).
Если у вас n котов, то чтобы определить самого жирненького, придется выполнить n взвешиваний. В таком случае говорят, что скорость работы алгоритма — O(n).
В реальности действий всегда будет не n, а несколько больше. Например, сначала нужно откалибровать весы, а в конце убрать их обратно в шкаф. Получается уже n+2 действия. Но поскольку дополнительных действий константное количество, их в оценке не учитывают: O(n+2) = O(n)
А что делать, если котики отказываются взвешиваться без рыбов? Тогда на каждого кота приходится 2 действия: выдать рыбку и взвесить, поэтому общее количество действий 2n. Но фактор 2× тоже константный, поэтому и его в оценке не учитывают: O(2n) = O(n)
Ровно так это работает и в коде:
var fattest Cat
for _, cat := range cats {
if cat.weight > fattest.weight {
fattest = cat
}
}
3n+1 (+1 — инициализация переменной fattest). Но скорость работы алгоритма — O(n)