encoding/json видит только экспортируемые поля: те, что начинаются с заглавной буквы. Структура, у которой все поля с маленькой буквы, кодируется в {}, и ошибки при этом не будет.
Тег на неэкспортируемом поле не делает ничего: go vet сообщает struct field total has json tag but is not exported. Чтобы поле попало в вывод, его нужно экспортировать.
json:"user_id" меняет имя ключа, json:"-" убирает поле, omitempty пропускает пустые значения, а ,string передаёт число внутри JSON-строки.
При декодировании ключ сопоставляется с тегом или именем поля, а если точного совпадения нет, то без учёта регистра.
Ловушка в том, что никто не ругается: {} считается валидным JSON, поэтому тест, проверяющий только err, будет проходить вечно.
GoMiddle
Стандартная библиотека на практике
io и bufio, encoding/json, time, strconv, regexp, embed
Закрыто вопросов
0 / 23
- 0Повторить
- 4Часто спрашивают
Вопросы
os.ReadFile загружает весь файл в память одним вызовом и подходит для небольших файлов вроде конфигов. Всё большое или неограниченное открывают через os.Open и читают потоком как io.Reader: построчно через bufio.Scanner или кусками через io.Copy.
**os.ReadFile / os.WriteFile: открыть, прочитать или записать и закрыть одним вызовом; расход памяти равен размеру файла.
os.Open + bufio.Scanner**: по одной строке в постоянной памяти. После цикла проверяйте s.Err(): ошибка чтения тоже просто завершает цикл.
**io.Copy(dst, src): перекладывает байты из любого reader'а в любой writer через буфер 32 КБ, а между файлами и сокетами может отдать копирование ядру.
defer f.Close()** ставят сразу после проверки ошибки. Для файла, в который пишете, проверяйте и ошибку Close: неудачная запись может проявиться только там.
os.Create обрезает существующий файл; для дозаписи нужен os.OpenFile(name, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644).
bufio.Writer держит вывод в буфере в памяти (по умолчанию 4 КБ) и пишет в нижележащий файл, только когда буфер заполнился или когда вы вызвали **Flush**.
Write копирует данные в буфер и возвращает n, nil, поэтому всё, что осталось в буфере к выходу из функции, теряется.
f.Close() закрывает файл, а не обёрнутый вокруг него writer: Close ничего не знает о буфере.
Вызывайте Flush после цикла и проверяйте его ошибку: неудачная запись в файл запоминается и возвращается именно там. Голый defer w.Flush() эту ошибку выбрасывает.
Выведет "A" "65" 0 strconv.Atoi: parsing "65 ": invalid syntax. string(n) трактует целое число как кодовую точку Unicode, а не как число для форматирования; нужные преобразования делают strconv.Itoa и strconv.Atoi, причём Atoi не принимает пробел в конце.
string(int) компилируется по историческим причинам, но go vet (его запускает и go test) сообщает conversion from int to string yields a string of one rune. Если вам действительно нужен символ, пишите string(rune(n)).
strconv.Itoa / Atoi работают с int; для других размеров и систем счисления есть ParseInt(s, 10, 64), ParseFloat, ParseBool и FormatInt(n, 16).
Ошибки разбора имеют тип *strconv.NumError, оборачивающий strconv.ErrSyntax или strconv.ErrRange, поэтому с ними работает errors.Is.
Ничего не обрезается за вас: ввод от пользователя или из файла сначала пропускайте через strings.TrimSpace.
Layout в Go представляет собой пример одного эталонного времени Mon Jan 2 15:04:05 MST 2006, записанный так, как вы хотите видеть результат. Всё, что не является частью этой метки, копируется в вывод дословно, поэтому YYYY-MM-DD печатается как есть.
Цифры служат мнемоникой: 1 месяц, 2 день, 3 час (15 для 24-часового формата), 4 минута, 5 секунда, 6 год, -0700 зона.
Берите константы: time.RFC3339 для API, time.DateOnly и time.DateTime (Go 1.20) для простых дат.
time.Parse с layout без зоны возвращает UTC, а не локальное время. time.ParseInLocation читает тот же текст как настенное время в указанной вами зоне.
//go:embed копирует файлы из директории пакета в бинарник на этапе сборки: в string, []byte или embed.FS. Дальше код читает их из памяти, и рабочая директория процесса перестаёт иметь значение.
Один файл кладут в string или []byte; много файлов кладут в embed.FS, который реализует fs.FS, поэтому его напрямую принимают template.ParseFS и http.FileServerFS (Go 1.22).
Директива стоит прямо над переменной уровня пакета, а файл импортирует embed (для string и []byte хватает пустого импорта _). Шаблоны путей задаются относительно директории пакета, и .. в них нельзя.
Если шаблон указывает на директорию, файлы с именами на . или _ пропускаются; чтобы их включить, добавьте префикс all:.
Баг, который он чинит: os.ReadFile("templates/x.html") разрешается от рабочей директории процесса. Он работает, пока вы запускаете из директории с файлами (обычный go run .), и ломается под systemd или в контейнере с другим WORKDIR.
Встроенные файлы доступны только на чтение и фиксируются при сборке: чтобы изменить файл, нужно пересобрать бинарник.
crypto/rand нужен для всего, что атакующий не должен угадать: токенов, id сессий, ключей, ссылок для сброса пароля. math/rand/v2 предлагает быстрый некриптографический генератор для джиттера, выборок, перемешивания и симуляций.
Вывод math/rand может предсказать любой, кто видел его достаточно или знает сид, поэтому секреты он не генерирует никогда.
Сидирование: начиная с Go 1.20 глобальные функции math/rand получают случайный сид при старте, а rand.Seed объявлен устаревшим; начиная с Go 1.24 его вызов ничего не делает, если не задан GODEBUG=randseednop=0.
В math/rand/v2 (Go 1.22) глобального Seed нет вовсе, Intn переименован в IntN, появился дженерик rand.N (работает и с time.Duration), а для воспроизводимой последовательности есть источники PCG и ChaCha8.
crypto/rand.Read(b) заполняет слайс байтами от ОС; с Go 1.24 он никогда не возвращает ошибку, а rand.Text() выдаёт готовый токен из 26 символов со 128 битами случайности.
Все потоковые типы стандартной библиотеки опираются на два интерфейса с одним методом (**io.Reader и io.Writer**), поэтому сжатие, хеширование и подсчёт сводятся к обёрткам, которые надеваются друг на друга, а всю цепочку крутит io.Copy.
Read(p []byte) (int, error) не упоминает ни файлов, ни сокетов, поэтому *os.File, net.Conn и тело запроса взаимозаменяемы, а тест подсовывает strings.NewReader("...") вместо мока.
io.TeeReader(r, w) попутно копирует в w всё, что прочитано из r; io.MultiWriter(a, b) дублирует каждую запись.
io.Copy(dst, src) крутит цикл с буфером 32 КБ, поэтому расход памяти не зависит от размера файла.
io.Pipe() соединяет горутину, которая пишет, с API, которому нужен io.Reader (например, с телом HTTP-запроса), без промежуточного буфера.
io.LimitReader(r, n) ограничивает поток, но сообщает об обрезке обычным io.EOF; если обрезку нужно заметить, читайте n+1 байт.
Закрывать обёртки обязательно: gzip.Writer.Close дописывает трейлер, и без него архив не прочитается.
bufio.Scanner работает как удобный токенизатор (строка или слово на каждый Scan()), но он отказывается от любого токена больше 64 КБ (bufio.MaxScanTokenSize). bufio.Reader устроен как обычный буферизованный reader без такого лимита: ReadString('\n') или ReadBytes вы вызываете сами.
На слишком длинном токене Scan() просто возвращает false, а причина видна только в s.Err(). Цикл без этой проверки останавливается на полпути и рапортует об успехе.
Лимит действует на токен, а не на файл: гигабайты коротких строк проходят, а одна строка на 70 КБ фатальна.
Поднять его можно через s.Buffer(make([]byte, 0, 64*1024), 4*1024*1024) до первого Scan: стартовый размер и максимум, до которого буфер может вырасти.
Reader.ReadString наращивает строку сколько нужно и, если в конце файла нет перевода строки, возвращает последнюю строку вместе с io.EOF, поэтому сначала обработайте данные, потом ошибку.
s.Bytes() указывает в буфер сканера и перезаписывается следующим Scan; если значение нужно сохранить, скопируйте его.
omitempty пропускает false, 0, "", nil-указатели и интерфейсы, а также слайсы, мапы и массивы нулевой длины; структура для него пустой не бывает, поэтому нулевой time.Time уходит как "0001-01-01T00:00:00Z". omitzero (Go 1.24) пропускает поле с нулевым значением его типа.
omitzero к тому же вызывает метод IsZero() bool, если он у типа есть, поэтому он и работает для time.Time.
omitempty на числе означает, что 0 отправить нельзя: клиент не отличит счёт 0 от отсутствия счёта. Если 0 значим, используйте *int; nil пропускается, указатель на 0 уходит как 0.
С обеими опциями, json:",omitempty,omitzero", поле выбрасывается, если этого требует любое из правил.
Без omitempty nil-слайс кодируется в null, а выделенный пустой кодируется в []; клиенту, который ждёт массив, нужен второй вариант.
До Go 1.24 обходились полем *time.Time или собственным MarshalJSON.
Выведет float64, а затем {"order_id":12345678901234568} (на единицу больше). Декодирование в any превращает каждое JSON-число в float64, который хранит целые числа точно только до 2^53, поэтому id округлился на входе, а обратно записалось уже округлённое значение.
В any декодер кладёт только bool, float64, string, []any, map[string]any и nil. Целочисленного варианта нет, и ошибки тоже нет.
dec.UseNumber() декодирует числа в json.Number (строку с исходным текстом и методами Int64() и Float64()), так что большие id выживают.
Ретранслятору, который только пересылает, число вообще не нужно декодировать: держите payload в json.RawMessage и передавайте исходные байты дальше.
Если значение вам важно, декодируйте в структуру с полями int64; неизвестные ключи при этом пропускаются сами.
У JavaScript-клиентов тот же предел 2^53, поэтому API часто отдают 64-битные id строками через json:",string".
Неизвестные ключи молча отбрасываются, поэтому переименованный ключ исчез, а MaxConns остался нулём; известные ключи к тому же сопоставляются без учёта регистра, что прячет другие опечатки.
Ключ, которому не соответствует ни одно поле, разбирается и выбрасывается, а поле сохраняет то, что в структуре уже лежало; у свежей структуры это ноль.
Сопоставление пробует тег, затем имя поля, затем сравнение без учёта регистра, так что {"MAX_CONNS":7} действительно заполнит поле с тегом json:"max_conns".
dec.DisallowUnknownFields() превращает первый случай в ошибку; используйте его для конфигов и внутренних API.
Decode читает одно значение, поэтому проверьте и что следующий Decode возвращает io.EOF.
Заполняйте структуру дефолтами до декодирования, а после него валидируйте: конфиг с MaxConns <= 0 не должен давать сервису стартовать.
o объявлена один раз, вне цикла, а json.Unmarshal заполняет только те поля, что есть во входных данных. Строка без coupon сохраняет Coupon предыдущего заказа, и заказу начисляется чужая скидка.
Почему: Unmarshal в существующее значение не обнуляет его: поля, ключей которых нет в JSON, остаются как были. Это задокументировано и удобно, когда JSON накладывают поверх значений по умолчанию, но здесь роль «значений по умолчанию» играет предыдущая строка.
Исправление: объявить var o Order внутри цикла, чтобы каждая строка декодировалась в нулевое значение; o = Order{} в начале итерации работает так же. Цикл с json.Decoder, который вызывает Decode(&o) по NDJSON, нужно исправить так же.
Со слайсами хуже: для поля []string Unmarshal сбрасывает длину в ноль и переиспользует базовый массив, поэтому следующая строка перезаписывает теги заказа, который уже добавлен в out. В поле-мапу новые ключи сливаются со старыми.
Как поймать: линтеры это не ловят. Ловит табличный тест, где за строкой со всеми полями идёт строка без необязательных.
json.Marshal проверяет, реализует ли тип json.Marshaler, раньше, чем смотрит на поля. type payload = Event объявляет алиас, тот же тип с теми же методами, и встраивание продвигает MarshalJSON в анонимную структуру, поэтому внутренний json.Marshal снова вызывает этот же метод, и так бесконечно.
Каждый вызов добавляет кадры стека, пока не упрётся в лимит стека Go в 1 ГБ, и рантайм останавливается с fatal error: stack overflow. Это не паника: recover её не поймает, и умирает весь процесс.
Простой вариант, return json.Marshal(e) внутри Event.MarshalJSON, зацикливается точно так же.
Исправление занимает один символ: type payload Event без = объявляет определённый тип с полями и тегами Event, но с пустым набором методов, поэтому его кодирует стандартный энкодер.
Внешнее поле At string затеняет поднятое (promoted) поле At, так что формат меняется только у этого поля.
UnmarshalJSON объявляют на указателе, и там работает тот же приём с *payload.
encoding/json/v2 появился в Go 1.25 как эксперимент за флагом GOEXPERIMENT=jsonv2: переписанная реализация со строгими настройками по умолчанию и быстрым декодированием. easyjson и sonic выбирают, когда профиль показывает, что JSON съедает CPU на горячем пути.
Строже по умолчанию: дублирующиеся ключи дают ошибку (v1 берёт последний), невалидный UTF-8 тоже даёт ошибку (v1 подставляет U+FFFD), а имена сопоставляются с учётом регистра (v1 откатывается к сравнению без учёта регистра).
Разумнее на выходе: nil-слайс кодируется в [], nil-мапа в {}, а не в null; omitempty означает «пропустить, если значение закодируется в пустой JSON».
Потоки и опции: MarshalWrite и UnmarshalRead работают с io.Writer / io.Reader, тип может реализовать MarshalJSONTo(*jsontext.Encoder) error и писать вывод потоком, а поведение настраивается опциями на каждый вызов, а не глобальными настройками.
С включённым экспериментом v1 encoding/json работает на новом движке, но сохраняет семантику v1.
easyjson генерирует код без рефлексии, но его нужно перегенерировать при каждом изменении структуры; sonic использует JIT и SIMD на amd64/arm64 ценой unsafe и платформенных ограничений.
Пока он остаётся за GOEXPERIMENT, в продакшен-коде по умолчанию оставляйте encoding/json.
Пакет reflect позволяет коду изучать и менять значения, тип которых известен только во время выполнения: их вид, поля, теги структур и методы. На нём построены encoding/json, fmt, text/template и сканирование в database/sql, и так они работают с вашими типами без сгенерированного кода.
reflect.TypeOf(v) даёт статическое описание (поля, теги через Field(i).Tag.Get("json")); reflect.ValueOf(v) даёт сами данные, а Kind() говорит, структура это, указатель, слайс или что-то ещё.
Скорость: каждое значение проходит через any, часто с аллокацией в куче, а поля ищутся во время выполнения, поэтому компилятор не может ничего заинлайнить или оптимизировать. Рассчитывайте на замедление на порядок по сравнению с прямым кодом; encoding/json кэширует списки полей для каждого типа, чтобы это смягчить.
Безопасность: ошибки типов переезжают из компиляции в паники: Elem на не-указателе, SetInt на строковом поле, запись в неэкспортируемое или неадресуемое значение. Чтобы изменить структуру, передают указатель и вызывают Elem(); CanSet() подскажет заранее.
Альтернативы: дженерики для кода, одинакового для разных типов, интерфейсы для поведения, кодогенерация (stringer, easyjson, sqlc) для горячих путей. Рефлексию оставляют для действительно произвольных типов: сериализаторов, валидаторов, загрузчиков конфигов.
regexp в Go реализует RE2: он сопоставляет через автоматы, а не перебором с возвратами, поэтому время линейно от размера входа, и никакой паттерн или строка не вызовут катастрофический backtracking (ReDoS). Цена в том, что нет обратных ссылок и нет lookahead и lookbehind.
(\w+) \1 и foo(?=bar) не компилируются: regexp.Compile возвращает invalid escape sequence для первого и invalid or unsupported Perl syntax для второго.
Линейно не значит быстро: на простых паттернах движок с возвратами может выиграть, а сопоставление идёт по семантике leftmost-first, как в Perl (regexp.CompilePOSIX даёт leftmost-longest).
Компиляция разбирает паттерн и строит программу, и это стоит гораздо дороже одного сопоставления. regexp.MustCompile внутри функции платит эту цену на каждом вызове; var re = regexp.MustCompile(...) на уровне пакета платит её один раз, а *Regexp безопасен для конкурентного использования.
MustCompile паникует на плохом паттерне; это правильно для константы, проверяемой при старте, и неправильно для паттерна от пользователя: там вызывают Compile и возвращают ошибку.
Многие «регулярки» в кодовой базе на самом деле сводятся к strings.HasPrefix, strings.Contains или strings.Cut, которые вообще ничего не компилируют.
Начните со структуры с дефолтами, пусть переменные окружения их переопределяют, а флаги командной строки переопределяют и то и другое; затем один раз при старте провалидируйте готовую структуру и откажитесь стартовать, если что-то не так. os.LookupEnv и flag справляются с этим без фреймворка.
os.Getenv возвращает "" и для незаданной, и для пустой переменной; os.LookupEnv их различает.
flag разбирает типизированные значения (flag.Duration, flag.Int) и бесплатно печатает справку. Разбирайте флаги один раз в main или через собственный flag.FlagSet, как в примере, но никогда в init или внутри библиотеки.
В контейнерах основным каналом служат переменные окружения; для большого структурированного конфига подходит JSON-файл, декодированный с DisallowUnknownFields.
Собирайте все ошибки валидации и возвращайте их вместе через errors.Join, чтобы один деплой показал все ошибки сразу.
Передавайте Config явно, а не читайте отовсюду глобальную переменную пакета: тогда тесты соберут свой.
net.Listen("tcp", addr) возвращает listener; цикл вызывает Accept и отдаёт каждое net.Conn в собственную горутину. Для продакшена добавляют дедлайны, чтобы молчащий клиент не держал горутину вечно, и остановку, которая закрывает listener, будит простаивающие обработчики и ждёт их.
net.Conn реализует io.Reader и io.Writer, поэтому bufio, io.Copy и io.ReadFull работают с ним без изменений.
TCP передаёт поток байтов без границ сообщений: один Write может прийти несколькими Read. Сообщения нужно разделять разделителем или префиксом длины.
Дедлайны задают абсолютное время, а не таймаут: вызывайте SetReadDeadline(time.Now().Add(d)) перед каждым чтением и ставьте также дедлайн на запись, иначе клиент, который перестал читать, удерживает обработчик в Write. Чтение по таймауту падает с ошибкой, для которой errors.Is(err, os.ErrDeadlineExceeded) истинно.
Закрытие listener'а заставляет Accept вернуть net.ErrClosed: это и есть сигнал остановиться; дальше обработчиков ждут через sync.WaitGroup.
Закрытие listener'а не останавливает уже работающие обработчики: дайте им сигнал выхода или дедлайн чтения в прошлом и ограничьте ожидание таймаутом.
Недоверенный клиент может прислать бесконечную строку: ограничьте длину строки (Scanner.Buffer или LimitReader на сообщение) и при необходимости лимитируйте число одновременных соединений.
time.Time, полученный из time.Now(), несёт двое часов: настенное показание и монотонное. Sub, Since и Until берут монотонное, если оно есть у обоих значений, а эти часы идут только вперёд.
Настенные часы двигают NTP, переход на летнее время и ручной date. Монотонные берутся из ОС (CLOCK_MONOTONIC в Linux), перевести их нельзя, поэтому time.Since(start) остаётся верным и во время скачка.
Всё, что отбрасывает монотонное показание, оставляет только арифметику настенного времени: Round(0), UTC(), In, форматирование и разбор обратно, MarshalJSON, время, прочитанное из базы, или перевод через Unix()/UnixNano(). Вычитание таких значений сводится к обычной арифметике настенного времени, отсюда и минус.
Поэтому прошедшее время меряют через time.Since, а моменты хранят настенными: в RFC 3339 или Unix-секундах. Разность двух сохранённых меток нельзя называть задержкой.
Два time.Time сравнивают через t.Equal(u): == сравнивает ещё и монотонное показание, и зону.
Один json.Decoder поверх reader'а декодирует по одному значению за раз в постоянной памяти, а поле json.RawMessage придерживает байты data недекодированными, пока type не скажет, в какую структуру они ложатся; так каждая строка декодируется в Go-значения один раз.
dec := json.NewDecoder(r), затем for { err := dec.Decode(&ev); if err == io.EOF { break } ... }, а не for dec.More(), потому что More() возвращает false и при ошибке чтения и скрывает её. Декодер держит только свой буфер и текущее значение, а переводы строк между значениями пропускает сам.
В отличие от bufio.Scanner, лимита на токен у него нет: строка на 10 МБ пройдёт, декодер просто нарастит буфер.
Переключитесь по ev.Type и разберите ev.Data в нужную структуру. Отвергнутая строка записывается обратно, так ни разу и не превратившись в Go-типы.
enc.Encode(v) пишет одно значение и следом перевод строки; это уже готовый NDJSON. Подложите под него bufio.Writer и проверьте ошибку Flush.
После синтаксической ошибки декодер не может восстановить синхронизацию, поэтому остановитесь и сообщите dec.InputOffset(); ошибка типа в одной записи поток не ломает.
io.ReadAll плюс json.Unmarshal требует держать в памяти все 40 ГБ, а декодирование сначала через map[string]any строит Go-значения для каждой строки дважды и превращает числа во float64.
unsafe.Pointer преобразуется в любой тип указателя и в uintptr и обратно, позволяя переинтерпретировать память и делать арифметику указателей в обход системы типов. unsafe.String делает строку поверх байтов слайса без копии, поэтому, когда эти байты меняются, вместе с ними меняется и якобы неизменяемая строка.
Что это позволяет: *T в unsafe.Pointer и дальше в *U читает те же байты как другой тип (именно так устроен math.Float64bits), unsafe.Add делает арифметику указателей, Sizeof и Offsetof показывают раскладку в памяти, а unsafe.Slice, SliceData, String и StringData (Go 1.17 и 1.20) преобразуют слайсы, строки и указатели друг в друга.
**Правило для uintptr**: uintptr остаётся обычным числом. GC не держит живым то, на что оно указывает, а стеки переезжают при росте, поэтому превращайте его обратно в указатель в том же выражении; иначе go vet сообщает possible misuse of unsafe.Pointer.
Почему строка без копии ломается: string(b) копирует, и именно эта копия делает строки неизменяемыми. Строка из unsafe.String делит буфер, поэтому следующий Read в него или буфер из sync.Pool, взятый другим запросом, молча её перезаписывает: ключ в map меняется после вставки (seen["alice"] в примере равно false), а кэш показывает данные чужого запроса.
Обратное направление ещё хуже: запись через unsafe.Slice(unsafe.StringData(s), len(s)) в строковый литерал попадает в память только для чтения, и процесс падает с fatal error: fault, который recover не ловит.
Обычно ничего этого не нужно: компилятор и так обходится без копии в m[string(b)], string(b) == "x", switch string(b) и for range string(b). Берите unsafe, только когда профиль показывает эту копию, а в эти байты гарантированно больше никто не пишет.
Цикл проверяет err раньше, чем использует n. gzip.Reader возвращает последний кусок данных вместе с io.EOF, поэтому цикл выходит и теряет эти байты: хешируется обрезанный поток, а ошибки нет.
**Контракт io.Reader**: Read может вернуть n > 0 и ненулевую ошибку в одном вызове, и вызывающий код обязан обработать n байт, прежде чем смотреть на err. gzip.Reader делает именно так в конце потока; os.File и bytes.Reader возвращают 0, io.EOF отдельным вызовом, поэтому тесты на обычных файлах проходят.
Исправление: сначала записать buf[:n], потом проверять err. Ещё проще использовать io.Copy(h, r), который соблюдает контракт за вас, как и io.ReadAll, если данные помещаются в память.
Не только gzip: данные вместе с ошибкой может вернуть любой reader, включая сторонние и оборачивающие, поэтому цикл, который выходит по err != nil до использования n, неверен в принципе, а не только для этого источника; у bufio.Reader.ReadString та же форма «данные плюс EOF».
Как поймать: в тестах оберните источник в iotest.DataErrReader, который отдаёт последние данные вместе с io.EOF; свои reader'ы проверяйте через iotest.TestReader.