16. Стандартная библиотека
«Стандартная библиотека языка программирования должна быть компактной, ортогональной и безупречно надежной».
— Кен Томпсон
Архитектурная карта доменов стандартной библиотеки Go
Стандартная библиотека Go (stdlib) спроектирована по принципу компонуемых интерфейсов (io.Reader, io.Writer, fmt.Stringer, error):
flowchart TD
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
Stdlib["Go Standard Library"]:::entry
Stdlib --> IO["I/O & FS: io, bufio, os, io/fs, path/filepath"]:::process
Stdlib --> Sync["Concurrency: sync, sync/atomic, context"]:::accent
Stdlib --> Net["Network: net, net/http, net/url, crypto/tls"]:::process
Stdlib --> Data["Data & Formats: encoding/json, encoding/binary, database/sql, uuid"]:::success
Stdlib --> Diag["Diagnostics: runtime, runtime/pprof, runtime/trace, log/slog"]:::accent
Эволюция сериализации: encoding/json v1 vs encoding/json/v2
В Go 1.25 представлен пакет encoding/json/v2, устранивший исторические архитектурные дефекты v1 (медленная рефлексия, промежуточные аллокации, неоднозначности omitempty):
flowchart TD
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
subgraph V1["encoding/json v1 (Классический)"]
Reflect1["Тяжелая рефлексия reflect.TypeOf/ValueOf"]:::warning
Buffer1["Выделение временных []byte буферов на каждый шаг"]:::warning
Omit1["omitempty: не скрывает false и нулевые структуры"]:::warning
Dup1["Дубликаты ключей: тихо побеждает последнее значение"]:::warning
end
subgraph V2["encoding/json/v2 (Go 1.25+)"]
SAX["jsontext: потоковый токенизатор без промежуточных аллокаций"]:::success
FormatOpt["json.WithMarshalers / json.WithZeroFields опции"]:::accent
Strict["Strict mode: дублирование ключей вызывает синтаксическую ошибку"]:::process
ZeroOmit["omitzero: честный пропуск zero values любых типов"]:::success
end
Навигатор по вопросам главы
- Как устроен
fmt? - Как работает
io.Reader? - Что такое
io.Writer? - Как работает
io.Copy? - Что такое
io.Pipe? - Как работает
bufio.Scanner? - Как работает
encoding/json? - Как работает
MarshalиUnmarshal? - Как работают теги структуры (struct tags)?
- Как работает
time.Time? - Как работает
time.Ticker? - Как работает
time.Timer? - Как работает
time.Afterи почему это может привести к утечке памяти? - Как работает
net/httpклиент? - Как работает
net/httpсервер? - Что такое
http.Handlerиhttp.HandlerFunc? - Как работает
database/sql? - Что такое
sql.DBиsql.Tx? - Как работает пул соединений в
database/sql? - Как работает
os.File? - Как работает
os/signal? - Как работает
flag? - Как работает
context? - Как работает
syncпакет? - Как работает
sort? - Как работает
sort.Sliceиsort.SliceStable? - Как работает
crypto/tls? - Как работает
net/http/httptestдля тестирования? - Как работает
text/templateиhtml/template? - Как предотвратить XSS с помощью
html/template? - Как работает
reflect? - Когда стоит избегать
reflect? - Как работает
unsafe? - Как работает
syscall? - Как работает
os/execи как безопасно выполнять внешние команды? - Как работает
logиlog/slog(Go 1.21+)? - Что такое
io/fsи зачем он нужен? - Как работает
embedдля встраивания файлов? - Как работает
testingпакет для бенчмарков? - Как работает
runtime/debugиdebug/elf? - Что такое
container/listиcontainer/heap? - Как работает
ringизcontainer/ring? - Как работает
math/bigи когда его использовать? - Что такое
net/urlи как правильно парсить URL? - Как работает
sync/atomic.Value? - Как работает
runtime/metrics? - Что такое
mime/multipartи как обрабатывать загрузку файлов? - Как работает
net/rpcиnet/rpc/jsonrpc? - Как работает
archive/zipиarchive/tar? - Как работает
compress/gzip? - Что такое encoding/json/v2 в Go 1.25?
- Что такое пакет uuid в Go 1.27?
Как устроен fmt?
Краткий ответ
Пакет fmt реализует форматированный ввод-вывод на основе глаголов printf-стиля и интерфейсов Stringer, Formatter, GoStringer. Внутри принтера (pp) накапливается буфер, куда копируются строки, форматированные значения через рефлексию и интерфейс Formatter. Для эффективного вывода в io.Writer используется Fprint, а для строк – Sprintf с динамическим []byte-буфером.
Развёрнутый ответ
Основные функции: Print, Printf, Println, Sprintf, Fprintf. Все они делегируют работу внутреннему состоянию pp (private printer) из fmt/print.go. Упрощённо:
func Fprintf(w io.Writer, format string, a ...any) (int, error) {
p := newPrinter()
p.doPrintf(format, a)
n, err := w.Write(p.buf)
p.free()
return n, err
}
newPrinter() берёт pp из пула (sync.Pool), сбрасывает буфер и состояние. pp.doPrintf парсит строку формата: символ % запускает разбор флагов (-, +, #, , 0), ширины, точности и глагола. Для глаголов применяется рефлексия (пакет reflect) для извлечения значения, затем в зависимости от типа и интерфейсов вызываются:
Stringer(если%sили%v)error(для%s/%v; еслиerrorреализуетError())GoStringer(%#v)- Родной
Formatter(приоритетный интерфейс, может взять управление полностью)
Форматирование чисел, строк, байтовых срезов обрабатывается напрямую для производительности, без рефлексии. Все результаты пишутся в p.buf — []byte с переиспользованием из пула.
Под капотом (internals)
ppхранитbufтипаbuffer(обёртка над[]byteс методамиWriteByte,WriteString).- Пул
ppиспользуетsync.Pool, чтобы минимизировать аллокации. После каждого вызоваp.free()возвращаетppв пул, но предварительно обнуляет поля, чтобы избежать утечек ссылок. - Разбор формата реализован конечным автоматом в функции
fmt/format.go:parseArgNumberиdoPrintf. Сканирование идёт байт за байтом. - Для каждого аргумента вызывается
p.printArgс рефлексивным значением. Если реализованfmt.Formatter, вызываетсяFormat(State, verb), гдеState– внутренняя структура, передающая флаги, ширину и буфер. - Обработка
%vи%+vдля структур рефлексивно проходит поля: для%+vдобавляются имена полей. - Escape-последовательности
\n,\tинтерпретируются компилятором, а%кодируется как%%. Sprintfаллоцирует новый буфер, но для крупных строк может выполняться лишнее копирование; альтернатива —strings.Builderсfmt.Fprintf.
Типичные ошибки кандидатов
- Забытый
\nвPrintf.fmt.Printf("result %d", val)не дописывает перевод строки, в отличие отlog.Printf. Решение: добавить\nили использоватьfmt.Println. - Смешивание
logиfmtдля одновременного вывода.fmt.Printпишет вos.Stdout, аlog– вos.Stderr; это вызывает неупорядоченный вывод. Следует использовать один способ или слаженную синхронизациюlog.SetOutput. - Использование
fmt.Sprintfдля конкатенации без форматирования. Например,s := fmt.Sprintf("foo %s", bar), когда достаточноs := "foo " + bar; это лишние аллокации и разбор формата. Используйтеstrings.Builderили конкатенацию. - Неучёт того, что
Stringerможет вызываться рекурсивно при печати самого себя. Если внутриString()вызватьfmt.Sprintf("%v", s), получится бесконечная рекурсия. Выход: передавать напрямую внутреннюю строку, либо вызыватьSprintfс другим глаголом (%s), если гарантируется нерекурсивность.
Возможные дополнительные вопросы интервьюера
Каким образом интерфейс fmt.Formatter позволяет кастомизировать вид печати?
Структура реализует метод Format(f fmt.State, verb rune), получая доступ к флагам (f.Flag('#')), ширине (f.Width()), точности и может писать напрямую в буфер через f.Write. Это даёт полный контроль над представлением.
Чем отличается %v от %+v от %#v?
%v — значение по умолчанию, %+v — в структурах добавляет имена полей, %#v — Go-синтаксис-представление значения (вызывает GoStringer, для строк добавляет кавычки, для чисел тип и т.д.).
Как снизить аллокации при логировании форматированных строк?
Использовать log/slog с аттрибутами-ключами без форматирования, либо fmt.Fprintf в strings.Builder, взятый из пула (sync.Pool), с последующим сбросом и возвратом буфера.
Как работает io.Reader?
Краткий ответ
io.Reader — фундаментальный интерфейс с методом Read(p []byte) (n int, err error), моделирующий потоковое чтение. Код читает до len(p) байт, возвращает количество прочитанных байт и ошибку. Сочетание n > 0 и err == io.EOF — норма, сигнализирующая об успешном окончании потока.
Развёрнутый ответ
Контракт Reader строг:
- Может вернуть ненулевое
nиio.EOFодновременно: данные дочитаны, следующий вызов вернёт0, io.EOF. - После возвращения ошибки (включая
io.EOF) поведение последующих вызовов не гарантировано, но стандартные реализации обычно стабильно возвращают0, err. - Реализация не должна изменять переданный буфер
pпосле возврата; если ошибка, буфер может быть частично заполнен. - Вызывающая сторона должна сначала обработать прочитанные байты (
n), а потом проверять ошибку, даже если этоEOF.
Классическая обёртка: io.LimitReader, io.TeeReader, io.MultiReader. Все они строятся поверх Reader, компонуя поведение без аллокации больших буферов.
Под капотом (internals)
- Интерфейс – просто 1 метод, нет структур. Компилятор мономорфизирует вызовы, itab хранит указатель на метод.
- Стандартные реализации:
os.File.Readвызывает системныйread,bytes.Buffer.Readкопирует из внутреннего слайса,strings.Reader.Readотдаёт из строки. io.Copyиспользуетio.Readerи выделяет временный буфер (по умолчанию 32 КБ) для минимизации системных вызовов.- При проксировании соблюдается правило: если базовый
Readвернул0, io.EOF, обёртка также обязана вернуть0, io.EOF(или ошибку).
Типичные ошибки кандидатов
- Преждевременная проверка ошибки до учёта прочитанных байт. Код вида
n, err := r.Read(buf); if err != nil { return err }теряет последние байты. Исправить: сначала обработатьbuf[:n], потом обрабатыватьio.EOF. - Игнорирование короткого чтения. Полагают, что
Readзаполняет весь буфер; это не так. Всегда нужно использоватьio.ReadFullилиio.ReadAtLeast, если нужен точный размер. - Повторный вызов
Readпослеio.EOF. Некоторые источники (например,os.Fileпосле закрытия) могут возвращать другую ошибку, но логика должна прекращать чтение послеEOF.
Возможные дополнительные вопросы интервьюера
Как оптимизировать многократное чтение одного и того же ввода?
Использовать io.TeeReader для дублирования или io.MultiReader повторно, но если нужно перечитать — вероятно, следует прочитанные данные сохранить в bytes.Buffer и использовать bytes.Reader.
Какие интерфейсы расширяют io.Reader для эффективного копирования?
io.WriterTo у источника (ReadFrom у назначения) позволяют обойтись без промежуточного буфера, напрямую передавая данные. Например, os.File реализует ReadFrom с системным вызовом sendfile.
Почему io.Reader возвращает io.EOF, а не отдельный флаг?
Это позволяет использовать один канал ошибок и отличать «конец данных» от настоящих ошибок; обеспечивает идиоматическую обработку «прочитали и закончили» без дополнительных проверок.
Что такое io.Writer?
Краткий ответ
io.Writer — интерфейс с методом Write(p []byte) (n int, err error), который обязан записать все байты из p или вернуть ошибку, объясняющую, почему не удалось. Это минимальная абстракция для вывода.
Развёрнутый ответ
Контракт: если len(p) > 0, реализация должна записать всё; возврат n < len(p) всегда сопровождается не-nil ошибкой. Нельзя одновременно вернуть n == len(p) и err != nil – это нарушило бы инвариант. Стандартные приёмы: io.MultiWriter, io.WriteString (принимает строку без копирования в слайс, оптимизируя запись).
Обёртки io.LimitReader — для чтения ограниченного объёма; для Writer аналог io.LimitedWriter не требуется, так как контроль потока лежит на том, кто пишет.
Под капотом (internals)
- Как и
Reader, itab с одним методом. os.File.Writeделает системныйwrite,bytes.Buffer.Writeрасширяет внутренний слайс,strings.Builderв Go 1.10+ также реализуетio.WriterчерезWriteStringбез копирования.io.Copyвнутри использует буфер, читает изReader, пишет вWriter. ЕслиWriterреализуетReaderFrom,Copyпередаёт управление ему, минуя буфер.net.Connреализует оба интерфейса; запись в соединение не буферизируется вnet, но обёрткиbufio.Writerдобавляют буферизацию.
Типичные ошибки кандидатов
- Писать, не проверяя ошибку и количество записанных байт.
w.Write(data)может записать меньше, если ошибка неnil, но код продолжает считать, что запись успешна. Всегда обрабатывать:n, err := w.Write(data); if err != nil .... - Повторная запись того же слайса без сдвига. Если
n < len(p), нужно дописыватьp[n:]после устранения причины ошибки (но обычно ошибка неисправима и проще завершиться). - Предположение, что
Writeгарантированно пишет всё приlen(p) > 0. Это гарантировано только при успешном выполнении, но ошибка может возникнуть после частичной записи; при ошибкеnможет быть> 0. Код должен сохранять прогресс.
Возможные дополнительные вопросы интервьюера
Как реализовать эффективный WriteString без аллокации?
Проверить, реализует ли Writer интерфейс io.StringWriter. Если да — вызвать WriteString, иначе преобразовать строку в []byte (аллокация). Стандартные пакеты проверяют этот интерфейс.
Для чего нужен io.Writer без возврата количества байт?
Контракт един — всегда нужно возвращать n. Но io.Discard просто проглатывает данные и всегда успешно возвращает len(p), nil.
В чём отличие между io.Writer и io.Closer при композиции?
io.Writer — только запись. Для управления временем жизни соединения или файла нужен io.Closer. io.WriteCloser объединяет оба интерфейса, например os.File, net.Conn, gzip.Writer.
Как работает io.Copy?
Краткий ответ
io.Copy(dst Writer, src Reader) перекачивает все данные из src в dst, возвращая общее количество скопированных байт и первую ошибку. Использует внутренний буфер 32 КБ, но если dst реализует ReaderFrom (либо src — WriterTo), происходит прямая передача без промежуточного буфера.
Развёрнутый ответ
func Copy(dst Writer, src Reader) (written int64, err error) {
return copyBuffer(dst, src, nil)
}
В copyBuffer проверяются интерфейсы:
- Если
dstреализуетReaderFrom, вызываетсяdst.ReadFrom(src)(например,os.File→sendfile). - Если
srcреализуетWriterTo, вызываетсяsrc.WriteTo(dst)(например,bytes.Reader→dst.Writeмножественных кусков). - Иначе выделяется буфер (по умолчанию 32768 байт), циклично
src.Read(buf)→dst.Write(buf[:nr]).
CopyN копирует ровно n байт, CopyBuffer позволяет задать внешний буфер.
Под капотом (internals)
- Буфер изначально
nil, иcopyBufferиспользуетbuf := make([]byte, 32*1024)при первом вызове, но если передан пользовательский буфер, применяется он. - При
ReadFromоптимизации, ядро Linux:sendfileкопирует из файлового дескриптора в сокет без user-space буфера. ДляTCPConnReadFromреализован черезsplice(2)(Go 1.17+). - Резерв буфера не меняет размер в цикле; при медленном приёмнике
dst.Writeможет уйти в блокировку, но буфер остаётся того же размера.
Типичные ошибки кандидатов
- Закрытие источников внутри
Copy.io.Copyне закрывает ниsrc, ниdst. Необходимо закрыть источник с проверкой ошибки:if closeErr := src.Close(); closeErr != nil && err == nil { err = closeErr }. - Использование
Copyбез ограничения для удалённых источников, что может привести к неконтролируемому потреблению памяти. Рекомендуетсяio.CopyNилиio.LimitReader. - Игнорирование
io.EOFпроверкойerr != nil. Часто пишутif err != nil { return err }, теряяio.EOF, который означает успешное завершение.
Возможные дополнительные вопросы интервьюера
Как io.Copy определяет, использовать ли sendfile?
Проверяется, что dst — *os.File (обычно) и его дескриптор можно использовать, src — тоже *os.File или net.TCPConn. В Linux запускается syscall.Sendfile. В других ОС/условиях — буферизированное копирование.
Можно ли скопировать данные из io.Reader в несколько Writer одновременно?
Да, io.MultiWriter + io.TeeReader: io.Copy(io.MultiWriter(dst1, dst2), src). Копирование с размножением.
Почему io.Copy не обрабатывает сигналы прерывания?
Обработка сигналов — задача более высокого уровня. io.Copy при блокирующем вызове Read/Write может быть прервана только если используется контекст; для этого применяют os.File с неблокирующим режимом или net.Conn c таймаутами, но io.Copy сам не обрабатывает отмены.
Что такое io.Pipe?
Краткий ответ
io.Pipe создаёт синхронную in-memory пару *PipeReader и *PipeWriter, позволяющую горутинам обмениваться данными через io.Reader/Writer. Запись в PipeWriter блокируется, пока читатель не прочитает данные (синхронная буферизация), и наоборот. Удобен для соединения компонентов, ожидающих потоки.
Развёрнутый ответ
r, w := io.Pipe()
go func() {
defer w.Close()
w.Write([]byte("hello"))
}()
data, _ := io.ReadAll(r)
PipeWriter.Write помещает данные во внутренний кольцевой буфер и ждёт, пока PipeReader.Read их не заберёт. Ёмкость буфера ограничена размером переданного слайса; блокировка гарантирует, что данные не потеряются. Закрытие w приводит к io.EOF в r. Закрытие r вызывает ошибку ErrClosedPipe при следующей записи в w.
Под капотом (internals)
- Внутри
pipeсодержит однобайтовый буфер? Нет, структураpipeс полями:wrCh,rdCh(каналы для синхронизации), mutex, буфер-слайсdata,rerr,werr. - Реализация на основе взаимной блокировки:
Writeзахватывает mu, кладётbвdata, сигнализирует читателю, разблокирует mu и ждётrdCh(готовности чтения).Readждёт сигнала, копирует данные, уведомляет писателя. - Это не кольцевой буфер, а строгий handshake. Поэтому
Pipeне подходит для высокопроизводительного стриминга, но даёт надёжную синхронизацию, аналогично каналуchan []byte.
Типичные ошибки кандидатов
- Забыть закрыть
PipeWriterв горутине. Читатель зависнет навсегда, ожидаяEOF. Всегда используйтеdefer w.Close()после открытия пайпа. - Дедлок при записи и чтении в одной горутине.
Writeблокируется доRead, поэтомуio.Copy(w, src)иio.ReadAll(r)должны быть в разных горутинах. - Использование
io.Pipeдля буферизованной передачи без параллелизма. Это усложнение; проще применитьbytes.Buffer.
Возможные дополнительные вопросы интервьюера
Где io.Pipe используется в стандартной библиотеке?
net/http: httptest.NewRequest и http.NewRequest с телом, чтобы передать io.Reader парсеру; mime/multipart.NewWriter ожидает писателя, а os/exec.Cmd.StdinPipe использует io.Pipe для связи с stdin процесса.
Как прервать зависший Pipe?
Закрыть любую из сторон. Закрытие w разблокирует Read с io.EOF, закрытие r разблокирует Write с ErrClosedPipe. Можно комбинировать с контекстом: при отмене закрывать r.
Можно ли реализовать асинхронный пайп с буфером?
Да, через каналы chan []byte, но io.Pipe синхронный. Для асинхронного накопления используют bufio.Reader или кастомный буфер.
Как работает bufio.Scanner?
Краткий ответ
bufio.Scanner предоставляет удобный интерфейс для построчного (или по произвольному токену) чтения из io.Reader с настраиваемой функцией разделения (bufio.SplitFunc). Он последовательно читает токены с помощью Scan(), а текущий токен доступен через Text() или Bytes(). Встроенные сплиттеры: ScanLines, ScanWords, ScanRunes, ScanBytes.
Развёрнутый ответ
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
Scanner внутренне буферизирует чтение (bufio.Reader), вызывая SplitFunc для нахождения конца токена. SplitFunc получает последний кусок данных, возвращает advance, token, err. ScanLines возвращает токен без \n. Если токен не помещается во внутренний буфер, Scanner автоматически увеличивает буфер до MaxScanTokenSize (по умолчанию 64 КБ); при превышении выдаёт ErrTooLong.
Под капотом (internals)
- Буфер
bufс начальным размеромstartBufSize = 4096, при нехватке удваивается доMaxScanTokenSize. - Цикл
Scan: вызываетr.Readдля заполнения буфера, затем применяет сплиттер. Если сплиттер просит больше данных (err == nil,advance == 0), считывается ещё. SplitFuncработает по сигнатуреfunc(data []byte, atEOF bool) (advance int, token []byte, err error). ПриatEOF == trueи невозможности найти токен должен вернутьtokenс оставшимися данными.- Внутренний
bufio.Readerиспользуется не напрямую, а через собственную обёртку, чтобы поддерживать откат и поиск.
Типичные ошибки кандидатов
- Считать, что
Scannerработает с любым размером строки. По умолчанию лимит 64 КБ; на больших токенах (например, JSON без пробелов)Scan()вернётfalseс ошибкойbufio.ErrTooLong. Решение: установитьscanner.Buffer([]byte{}, bufio.MaxScanTokenSize*2). - Не проверять
Err()после цикла. Многие думают, чтоScan() == falseвсегда означает конец ввода; но это может быть ошибка. Всегда вызыватьscanner.Err(). - Многократный вызов
Text()в цикле без сохранения. Каждый вызовText()возвращаетstring(token)нового токена; токен перезаписывается следующимScan(). Сохраняйте строку. - Использовать
Scannerдля высоконагруженной бинарной обработки. Из-за построчного парсинга и конвертации в строку это неоптимально; для бинарных данных лучше ручнойio.ReadFull.
Возможные дополнительные вопросы интервьюера
Как написать собственный SplitFunc, выделяющий сообщения в формате, заканчивающиеся \n\n?
Реализовать функцию, которая ищет index \n\n, возвращает advance = index+2, token = data[:index], иначе 0, nil, nil (нужно больше данных), при atEOF вернуть остаток.
Как Scanner обрабатывает строки длиннее буфера?
Буфер увеличивается динамически, но останавливается на MaxScanTokenSize. Если сплиттер не вернёт токен до лимита, Scan возвращает ошибку ErrTooLong.
Почему Scanner не реализует io.Reader?
Потому что его задача — возвращать токены, а не непрерывный поток байт. Для потокового чтения используют bufio.Reader.
Как работает encoding/json?
Краткий ответ
Пакет encoding/json реализует кодирование/декодирование JSON через интерфейсы json.Marshaler/json.Unmarshaler и рефлексию. Marshal обходит структуру, преобразуя поля согласно тегам json:"name,options", и записывает результат в []byte. Unmarshal разбирает JSON-токены и распределяет значения в структуры/слайсы/мапы. Потоковые варианты — NewEncoder/NewDecoder.
Развёрнутый ответ
Основные функции:
b, err := json.Marshal(v) // v -> JSON
err := json.Unmarshal(b, &v) // JSON -> v
Для потоков:
enc := json.NewEncoder(w)
err := enc.Encode(v)
dec := json.NewDecoder(r)
err := dec.Decode(&v)
Преобразование управляется тегами структуры. Поля без тега используют имя поля; тег - исключает поле. omitempty пропускает нулевые значения. string форсирует строковое представление числа. Интерфейсы: json.Marshaler (метод MarshalJSON), json.Unmarshaler (UnmarshalJSON), также encoding.TextMarshaler/TextUnmarshaler для строкового представления.
Под капотом (internals)
MarshalсоздаётencodeState(пул состояний с внутреннимbytes.Buffer), рекурсивно вызываетreflect-basedmarshalфункцию. Для каждого типа существует таблица методов кодирования (encoder func), кэшируемая в map, чтобы избежать повторного обхода структуры. Рефлексия анализирует структуру, собирает поля, исключая неэкспортируемые и с тегом-. Встроенные поля анонимных структур продвигаются (promote).UnmarshalиспользуетdecodeStateс поточным декодеромscanner(внутренний лексер). Лексер не буферизирует весь ввод, а читает токены постепенно. Структура также кэширует информацию о полях. При декодировании создаётся карта соответствия ключей JSON и полей структуры, поддерживается case-insensitive matching.- Для
json.RawMessageоткладывается парсинг; дляjson.Delayed(Go 1.16+) — ленивый разбор. Encoder/Decoderдержат ссылку наio.Writer/io.Readerи работают поточно, что позволяет обрабатывать огромные массивы без полной загрузки в память.
Типичные ошибки кандидатов
- Передавать не-указатель в
Unmarshal. Ошибка компиляции? Нет,Unmarshalпринимаетany, но если передать значение, а не указатель, данные будут десериализованы во временный объект и потеряны.Unmarshalпроверяет, что второй аргумент — не nil-указатель; если нет, возвращаетInvalidUnmarshalError. - Путать
json:"-"иjson:"-,".json:"-"полностью исключает поле, аjson:"-,"создаёт ключ-в JSON. Аналогичная путаница сomitempty. - Не удалять пустые поля, когда
omitemptyне учитывает вложенные структуры.omitemptyработает только для нулевого значения; для вложенной структуры со всеми нулевыми полями она не считается пустой, если не является указателем. Приходится использовать указатель на структуру, чтобыomitemptyсрабатывал (nil). - Конкурировать за один
Encoder/Decoderбез синхронизации.Encode/Decodeне потокобезопасны; параллельные вызовы требуют внешней синхронизации.
Возможные дополнительные вопросы интервьюера
Как обработать unknown-поля при десериализации, чтобы не падало с ошибкой?
Начиная с Go 1.10, завести поле с тегом json:"-" для захвата: Extra map[string]json.RawMessagejson:"-"и реализоватьUnmarshalJSON`, который сначала разбирает в map[string]json.RawMessage, затем в основную структуру, распределяя неизвестные ключи.
Как десериализовать полиморфные JSON-объекты?
Использовать json.RawMessage для временного хранения вложенного блока, затем по дискриминатору вызывать json.Unmarshal в конкретный тип. Или написать собственный UnmarshalJSON.
Какие уязвимости безопасности связаны с encoding/json?
Атаки через гигантские числа (resource exhaustion), рекурсивные структуры (stack overflow), десериализация в interface{}, приводящая к map-спам. Использовать Decoder.UseNumber для чисел, DisallowUnknownFields для строгой проверки, и лимиты размера через io.LimitReader.
Как работает Marshal и Unmarshal?
Краткий ответ
json.Marshal преобразует Go-значение в JSON-строку ([]byte), обходя структуры рефлексивно и сериализуя поля согласно тегам и реализованным интерфейсам Marshaler. json.Unmarshal разбирает JSON-данные и заполняет целевой объект, используя теги и Unmarshaler, поддерживая гибкое сопоставление ключей.
Развёрнутый ответ
Оба опираются на рефлексию и кэширование информации о типах. Примеры:
type User struct {
Name string `json:"name"`
Age int `json:",omitempty"`
}
u := User{Name: "Alice"}
b, _ := json.Marshal(u) // {"name":"Alice"}
var u2 User
json.Unmarshal(b, &u2)
Marshal поддерживает MarshalJSON() для кастомной логики; если его нет, используется reflect для перебора полей. Указатели автоматически разыменовываются, нулевые указатели сериализуются как null. Карты кодируются как объекты JSON, ключи которых преобразуются в строки по тем же правилам.
Unmarshal ищет поле сначала по точному совпадению имени, затем case-insensitive. При нахождении JSON-ключа, отсутствующего в структуре, если DisallowUnknownFields установлен в декодере, возвращается ошибка. Иначе игнорируется. Числа могут быть декодированы в float64 (стандарт), но с UseNumber() остаются json.Number.
Под капотом (internals)
- Генерация кодеков кэшируется в
sync.Mapпоreflect.Type. Кэш хранитstructFieldsи функции-энкодеры/декодеры, построенные на этапе первого обращения к типу. - Для структур строится
fields— список экспортируемых полей с учётом встроенных типов. Сортировка по глубине встраивания, затем по алфавиту. При конфликте имён приоритет выше у более глубоко вложенного поля. Marshalработает через внутренний буфер, дополняя его черезe.WriteByteи аналоги.Unmarshalиспользует поточный сканер JSON (внутреннийscannerизencoding/json/scanner.go), который потребляетio.Reader, идентифицирует токены. Декодирование управляется конечным автоматом, который сопоставляет токены с ожидаемой структурой.- Анонимные поля структур промоутятся, если не конфликтуют с полями более высокого уровня.
Типичные ошибки кандидатов
- Забыть про экспорт полей. Неэкспортированные поля игнорируются и при маршалинге, и при анмаршалинге, что часто приводит к пустым результатам. Решение: начинать имена полей с заглавной буквы или использовать кастомный
MarshalJSON. - Попытка десериализовать число из строки без опции
string. JSON-строка"123"не превратится вintавтоматически, если не указан тегjson:",string"для поля. Ошибкаcannot unmarshal string into Go value of type int. Исправить: добавить опциюstring. - Неверное использование
omitemptyдля булевых полей.boolсfalseне опускается, потому чтоfalse— не нулевое значение? Нет,false— нулевое значение, оно опускается. Ноintс0опускается. Путаница с тем, что считать zero-value: 0, "", false, nil.time.Time{}не является zero-value в смыслеomitempty, если это не указатель. - Не копировать данные из
json.RawMessageперед сохранением.RawMessage— это[]byte, и при повторном использовании буфера декодера данные могут быть перезаписаны. Решение: делатьcopyили сериализовать сразу.
Возможные дополнительные вопросы интервьюера
Как Marshal справляется с циклическими ссылками?
Никак. При обнаружении цикла через рекурсию указателей возникает ошибка json: unsupported value: encountered a cycle via .... Нужно реализовать MarshalJSON с явной обработкой или использовать маршаллинг в промежуточный DAG.
Как сериализовать time.Time в unix timestamp?
Использовать кастомный тип с MarshalJSON/UnmarshalJSON, либо применить struct с полем time.Time и переопределить методы. Альтернативно, json.NewEncoder не имеет опции, поэтому только кастомная сериализация.
Чем отличается NewEncoder от Marshal с последующей записью?
NewEncoder пишет потоково, не аллоцирует полный слайс JSON, поэтому меньше памяти на больших объектах, и может записать несколько JSON-объектов последовательно (разделённых \n). Marshal возвращает полный []byte.
Как работают теги структуры (struct tags)?
Краткий ответ
Теги структуры — строковые литералы в обратных кавычках после типа поля, интерпретируемые пакетами через reflect.StructTag. Основной формат: key:"value", пары разделяются пробелами. encoding/json, xml, gorm и другие используют теги для управления поведением при маршалинге, валидации, ORM и т.д.
Развёрнутый ответ
type User struct {
Name string `json:"name" validate:"required"`
Age int `json:"age,omitempty" db:"user_age"`
}
Получение тега через рефлексию:
field, _ := reflect.TypeOf(User{}).FieldByName("Name")
tag := field.Tag
fmt.Println(tag.Get("json")) // "name"
Парсинг: StructTag.Get(key) ищет key:"value", возвращает значение до следующей пары или конца строки. Пробелы разделяют пары; значение может содержать опции через запятую (соглашение). Теги не влияют на компиляцию, только на рантайм-рефлексию.
Под капотом (internals)
- В структуре
reflect.StructTagлежит простоstring. При компиляции теги сохраняются в бинарный образ — в отладочную секциюruntime.moduledataчерезstructField.nameиstructField.tag. StructTag.Getсканирует строку, ищет подстрокуkey + ":\"", экранирование кавычек не поддерживается; поэтому значение не может содержать двойные кавычки.- В Go 1.17+ введён
reflect.StructTag.Lookup(key)возвращает значение и признак наличия, позволяя различить пустой тег и отсутствующий. - При старте runtime строит таблицы типов; для каждого поля сохраняется
tagв структуреstructField. Это позволяет избежать дополнительного разбора при каждом вызовеTag.Get.
Типичные ошибки кандидатов
- Множественные пробелы и форматирование. Если есть лишние пробелы,
Tag.Get("json")всё равно сработает, но ручное конструирование тегов безGetможет быть хрупким. Лучше использоватьGet. - Использование неверного формата
key:value.json:"name,omitempty"— правильно;json="name"неверно (знак равенства вместо двоеточия) — тег не будет распознанGet. - Забыть про конфликты имён встроенных полей. Продвижение (promotion) полей из встроенных структур может привести к неожиданному перекрытию тегов. Например, два встроенных поля с одинаковым именем, но разными тегами — предпочтение у более глубокого.
- Использование нескольких значений без учёта
Lookupдля определения наличия.tag.Get("json")вернёт первую пару; если нужен второй ключ с тем же именем — невозможно, уникальность по ключу.
Возможные дополнительные вопросы интервьюера
Как реализовать свой обработчик тегов для конфигурации?
Использовать reflect для перебора полей, получать field.Tag.Get("config"), парсить значение согласно своему синтаксису. Кэшировать разобранные данные в sync.Map для типа.
Может ли тег содержать двойную кавычку?
Стандартный парсер не поддерживает экранирование; кавычка внутри значения разорвёт формат. Обходной путь — использовать другое кодирование, например base64 или замену символов.
В чём разница между json:"-" и json:"-,"?
json:"-" — исключить поле, json:"-," — включить поле с именем - в JSON (имя ключа — "-"). Аналогично с другими пакетами.
Как работает time.Time?
Краткий ответ
time.Time — неизменяемая структура для представления момента времени с наносекундной точностью, календарным разбором (год, месяц, день) и часовым поясом. Внутренне хранит wall uint64 (настенные часы, монотонное смещение) и ext int64 (секунды с 0001-01-01), а также указатель на *Location. Реализует методы форматирования, арифметики и сравнения.
Развёрнутый ответ
Создание:
t := time.Now() // wall + монотонный сдвиг
t2 := time.Date(2026, 5, 1, 12, 0, 0, 0, time.UTC)
Хранение: wall содержит битовое поле: старшие биты — секунды с 1 января 1885 года (для быстрого извлечения года/дня), младшие — монотонный сдвиг и флаг hasMonotonic. Если монотонные часы не нужны, ext хранит секунды с 1-01-01. Такая двойная природа позволяет сравнивать и считать длительности без скачков системных часов. Location содержит имя и информацию о переходе на летнее время.
Форматирование: через магическую ссылочную дату Mon Jan 2 15:04:05 MST 2006 (= 01/02 03:04:05PM '06 -0700). time.Parse разбирает строку по этой же маске.
Под капотом (internals)
- Структура занимает 24 байта (8 wall, 8 ext, 8 *Location) на 64-битной машине.
Now()делает системный вызовclock_gettime(CLOCK_REALTIME)иclock_gettime(CLOCK_MONOTONIC)(Linux). Монотонные часы сохраняются в младших 33 битахwallс флагомhasMonotonic = 1 << 63.- При
t.Add(d)монотонный сдвиг вычитается и пересчитывается. Before,After,Equalиспользуют монотонные часы при наличии, иначеext.- Форматирование избегает аллокаций:
AppendFormatпишет в предоставленный[]byte.
Типичные ошибки кандидатов
- Использовать
==для сравнения времени, игнорируя локаль и монотонные часы. Два времени могут быть равны по моменту, но различаться по локальному представлению;t1.Equal(t2)нормализует сравнение. - Передавать
time.Timeкак ключ в map с разными локациями одного момента. Хешtime.Timeвключает локаль, поэтомуtime.Date(2026,5,1,0,0,0,0,UTC)и то же время вAmerica/New_Yorkмогут дать разные хеши. Лучше нормализовать к UTC. - Игнорировать ошибки
time.Parseиtime.ParseInLocation. Некорректная строка формата возвращаетtime.Time{}; код продолжает работать с нулевым временем. Всегда проверять ошибку. - Некорректно использовать
time.Afterв циклах с множеством горутин, что ведёт к утечке (см. далее).
Возможные дополнительные вопросы интервьюера
Как сериализовать time.Time в JSON с определённым форматом?
Реализовать кастомный тип с методами MarshalJSON/UnmarshalJSON с вызовом t.Format(time.RFC3339).
Чем опасен переход на летнее время для таймеров?
time.Timer не учитывает скачки часов; при переводе стрелок назад можно получить ложное срабатывание или задержку. Используйте монотонные часы при расчёте интервалов.
Как получить номер недели в году?
_, week := t.ISOWeek().
Как работает time.Ticker?
Краткий ответ
time.Ticker предоставляет канал C, в который с фиксированным интервалом отправляются значения time.Time. Он создаётся time.NewTicker(d), и должен быть остановлен Ticker.Stop() для освобождения ресурсов. Используется для периодических задач.
Развёрнутый ответ
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case t := <-ticker.C:
log.Println("tick at", t)
case <-ctx.Done():
return
}
}
Особенности: первое событие приходит через интервал, а не сразу. Таймер можно перезапустить только созданием нового. Stop() останавливает тикер, но не закрывает канал; если горутина заблокирована на чтении, она не разблокируется — нужно использовать дополнительный сигнальный канал. Чтобы избежать утечек, всегда останавливайте тикер, когда он больше не нужен.
Под капотом (internals)
- Внутренне
Ticker— это обёртка надruntimeTimer(структура изruntime/time.go), связанным с горутиной-таймером (timerproc). КаналC— буферизованный канал размера 1. - Планировщик runtime хранит кучу таймеров (heap) на основе
time-wheel(в Go 1.14+ перешли наtime.heep), эффективно обрабатывая миллионы таймеров. - Когда таймер срабатывает, он помещает
nowв каналCи перезаряжает таймер на следующий интервал. Если канал заполнен (при медленном потребителе), tick пропускается — дропается. Это поведение «drop if channel full». - Остановка удаляет таймер из кучи, освобождает горутину.
Типичные ошибки кандидатов
- Забыть вызвать
Stop(). Тикер останавливается только сборщиком мусора, когда на него нет ссылок, но до тех пор таймер вызывает срабатывания и занимает ресурсы. - Пытаться читать из
ticker.CпослеStop()без дополнительного контроля. Канал не закрывается, горутина может зависнуть. Всегда добавляйте контекст или флаг выхода. - Использовать
time.Tick(синглтон) в долгоживущих процессах, так как он не может быть остановлен и утекает. Всегда используйтеNewTickerиStop. - Полагать, что тики не пропускаются. При перегрузке получателя тики отбрасываются; если нужна точная периодичность без пропусков, лучше реализовать свой цикл с
time.Sleepи фиксацией следующего момента.
Возможные дополнительные вопросы интервьюера
Как реализовать тикер с немедленным первым событием?
Создать канал, послать в него начальное значение, затем запустить NewTicker и в цикле читать из начального канала, а потом из тикера; либо использовать time.AfterFunc с немедленным вызовом и периодическим перезапуском.
Можно ли перезапустить остановленный тикер?
Нет, Reset для тикера не существует из-за сложности согласования с заполненным каналом. Нужно создать новый.
Как тикер взаимодействует с GC?
Сам тикер не защищён от GC; если на него нет ссылок, финализатор не гарантирует остановку таймера, но рантайм всё равно может освободить память. Но лучше явно останавливать.
Как работает time.Timer?
Краткий ответ
time.Timer откладывает однократное событие в будущем. Создаётся NewTimer(d), предоставляет канал C, который получит значение ровно один раз после истечения времени (или после вызова Reset). Обязан быть остановлен при ненадобности через Stop() для предотвращения утечек.
Развёрнутый ответ
timer := time.NewTimer(5 * time.Second)
defer timer.Stop()
select {
case <-timer.C:
// сработал
case <-ctx.Done():
if !timer.Stop() {
<-timer.C // drain
}
}
Stop() возвращает true, если таймер был остановлен до срабатывания; false, если уже сработал. В последнем случае необходимо вычитать значение из канала (если не было прочитано ранее), чтобы избежать утечки памяти горутины-отправителя. Reset(d) перезапускает таймер, должен вызываться только на остановленном или уже сработавшем таймере, иначе возможно гонка — правильный паттерн: if !timer.Stop() { <-timer.C }; timer.Reset(d).
Под капотом (internals)
- Аналогично
Ticker, базируется наruntimeTimer, один общий таймерный heap на процесс. - При создании добавляется в кучу. Когда истекает, горутина-таймер отправляет текущее время в канал
C. - Канал
Cимеет размер 1. Если канал не освобождён, повторная отправка невозможна, поэтомуResetна уже сработавшем без освобождения канала приведёт к блокировке рантайм-лупы и утечке. - В Go 1.23 появился
time.Timer.Resetс улучшенной безопасностью, но drain всё ещё нужен для совместимости.
Типичные ошибки кандидатов
- Игнорировать возвращаемое значение
Stop()и не дренировать канал. Это гонка: междуStop()и возвратом таймер мог сработать и послать в канал; в канале останется значение, и следующийResetможет привести к «отравленному» каналу или утечке. Правильный паттерн:if !timer.Stop() { select { case <-timer.C: default: } }или<-timer.Cпосле проверки. - Повторный вызов
Resetна несработавшем таймере без предварительногоStop. С Go 1.23 это разрешено? В документации сказано, чтоResetдолжен вызываться только на остановленном или сработавшем таймере. Вызов на активном таймере приводит к гонке между старым и новым сроком. Нужно сначалаStop, drain, потомReset. - Использование
time.Afterкак долгоживущего таймера, не имея возможности остановить. Это приводит к утечкам (см. следующий вопрос). - Неправильная обработка канала в
selectс несколькими таймерами: если таймер не нужен, но он сработал в фоне, его канал может остаться невычитанным.
Возможные дополнительные вопросы интервьюера
Как реализовать надёжный таймаут с контекстом?
Использовать context.WithTimeout, который управляет таймером сам и гарантирует освобождение ресурсов после отмены.
Почему нельзя просто пересоздавать таймеры через time.After в цикле?
Каждый time.After создаёт таймер, который не удаляется до истечения, даже если вы выходите из select по другому событию. Множество вызовов в цикле создаёт горутины и память, постепенно утекающие.
Как Timer.Reset взаимодействует с непрочитанным каналом?
Если канал не был дренирован, при срабатывании после Reset сначала будет доставлено старое значение (с предыдущим временем), а новое событие не сможет быть отправлено, пока канал не опустошат. Это ломает логику.
Как работает time.After и почему это может привести к утечке памяти?
Краткий ответ
time.After(d) возвращает канал, который получит сигнал через d. Он эквивалентен NewTimer(d).C, за исключением того, что возвращённый таймер нельзя остановить. В циклических или долгоживущих select конструкциях это создаёт множество таймеров, которые не освобождаются до истечения, даже если они больше не нужны, что ведёт к утечкам памяти и горутин.
Развёрнутый ответ
Проблемный шаблон:
for {
select {
case <-time.After(5 * time.Second):
// делает что-то
case v := <-ch:
// обрабатывает v
}
}
Каждую итерацию создаётся новый таймер, который останется в куче минимум на 5 секунд после истечения, даже если ch получает данные и цикл немедленно продолжается. Сборщик мусора не может удалить таймер, пока он не сработает и канал не будет закрыт (но канал не закрывается). Решение: использовать time.NewTimer с явным Stop и drain:
timer := time.NewTimer(0)
defer timer.Stop()
for {
timer.Reset(5 * time.Second)
select {
case <-timer.C:
// таймаут
case v := <-ch:
// обработать
}
}
Под капотом (internals)
- Каждый
time.AfterпорождаетruntimeTimerс каналом, который регистрируется в глобальной куче таймеров. Куча защищена мьютексом, но при большом количестве таймеров могут возрасти задержки. - Освобождение после истечения: горутина
timerproc(или в Go 1.14+ специальная горутина) отправляет время в канал и удаляет таймер из кучи. Если на канал никто не читает, структура таймера остаётся в памяти до завершения отправки. - Если программа создаёт таймеры быстрее, чем они истекают, происходит линейный рост памяти и потребления CPU на обслуживание кучи.
Типичные ошибки кандидатов
- Повсеместное использование
time.Afterвselectвнутри цикла без осознания последствий. Это классическая ловушка. - Попытка обойти утечку через
time.Tickдля периодического опроса.time.Tickтакже нельзя остановить, он тоже даёт утечку, но медленнее (один таймер). ЛучшеNewTicker. - Использование
time.Afterс очень маленькими интервалами как способyield. Это создаёт огромное давление на GC и планировщик; правильно использоватьruntime.Goschedили каналы.
Возможные дополнительные вопросы интервьюера
Когда всё же допустимо использовать time.After?
В краткосрочных операциях, например, единичный select с таймаутом, где утечка ограничена временем жизни горутины (короткий запрос). Но и там лучше явный таймер.
Как отладить утечку таймеров в продакшене?
Смотреть метрики: количество горутин, runtime.NumGoroutine(), профилирование кучи, pprof показывает time.After в графе вызовов. Также использовать GODEBUG=scheddetail=1.
Может ли GC собрать неиспользованный канал от time.After?
Нет, потому что канал всё ещё находится в куче таймеров и является достижимым. Только после срабатывания и удаления таймера канал может быть собран.
Как работает net/http клиент?
Краткий ответ
http.Client — высокоуровневый HTTP-клиент, выполняющий запросы (Do, Get, Post) с настройками таймаутов, кук, редиректов и транспорта. Внутри использует Transport (обычно http.DefaultTransport), который управляет пулом соединений, TLS-рукопожатиями, keep-alive и автоматическим следованием редиректам.
Развёрнутый ответ
client := &http.Client{
Timeout: 10 * time.Second,
}
resp, err := client.Get("https://example.com")
Основной метод — Do(req); Get/Post — удобные обёртки, создающие Request. Client применяет политику редиректов (по умолчанию до 10), проверку кук через Jar. Transport выполняет реальную сетевую коммуникацию. Можно подменить на кастомный для тестирования (httptest), логирования, прокси.
Client безопасен для одновременного использования несколькими горутинами. Response.Body обязан быть закрыт через defer resp.Body.Close(), иначе соединение не вернётся в пул.
Под капотом (internals)
DefaultTransport=&http.Transport{...}, инициализируется лениво.Transport.roundTripуправляет кешем TCP-соединений (idleConnmap, по ключуconnectMethodKey). При новом запросе ищет свободное idle-соединение; если нет, создаёт новое черезDialContext.- Поддержка HTTP/1.1, HTTP/2 (через
golang.org/x/net/http2или встроенный с Go 1.6+).Transportавтоматически согласовывает протокол через TLS-ALPN. DialContextдля каждого соединения оборачиваетnet.Dialerс таймаутамиDial,KeepAlive. НастраиваетсяDialTLSContextдля кастомного TLS.- Пул idle-соединений имеет ограничение
MaxIdleConns,MaxIdleConnsPerHost,IdleConnTimeout. Если пул переполнен, самые старые закрываются.
Типичные ошибки кандидатов
- Не закрывать
resp.Body. Даже если ответ не читается, тело должно быть закрыто, иначе соединение не вернётся в пул, и горутина утечёт. - Использовать дефолтный клиент без таймаутов.
http.DefaultClientне имеетTimeout, что может привести к зависанию навечно. Всегда задаватьTimeoutили контекст. - Игнорировать буферизацию тела при чтении маленьких ответов.
io.ReadAllчитает всё; для потоковых ответов следует ограничить черезio.LimitReader. - Делить один
http.Transportс кастомными настройками между разными клиентами без учёта конфликтов. Если менятьTransportпосле первого использования, это вызовет гонки. Лучше создавать новый транспорт для специфичных конфигураций. - Не проверять редиректные петли. Хотя
Clientограничивает 10 редиректами, кастомныйCheckRedirectдолжен предотвращать опасные редиректы (смену протокола или хоста).
Возможные дополнительные вопросы интервьюера
Как настроить прокси?
Через Transport.Proxy задать функцию, возвращающую *url.URL прокси, или ProxyFromEnvironment.
Как тестировать HTTP-клиент без сети?
Подменить Transport на httptest.NewServer или http.RoundTripper который возвращает фиктивные ответы.
Что такое http.RoundTripper и как он связан с Client?
Это интерфейс с методом RoundTrip(*Request) (*Response, error). Client вызывает Transport.RoundTrip. Позволяет встроить промежуточные слои: кеширование, аудит, заглушки.
Как работает net/http сервер?
Краткий ответ
http.Server слушает TCP-порт, принимает соединения и для каждого запускает горутину, обрабатывающую запросы через цепочку Handler. Сервер поддерживает keep-alive, таймауты чтения/записи, грациозную остановку, HTTP/2 через TLS. Стандартный DefaultServeMux — маршрутизатор по паттернам.
Развёрнутый ответ
srv := &http.Server{
Addr: ":8080",
Handler: mux,
}
log.Fatal(srv.ListenAndServe())
ListenAndServe создаёт net.Listener. При поступлении соединения Serve в цикле принимает соединения, запуская на каждое горутину go c.serve(ctx). Внутри соединения читается запрос (http.ReadRequest), создаётся http.Response, и вызывается server.Handler.ServeHTTP(w, r). Ответ записывается обратно в сетевое соединение. Если Handler — nil, используется DefaultServeMux.
Под капотом (internals)
- Соединение обслуживается в горутине, считывающей запросы с помощью
bufio.Reader. Поддержка keep-alive активирована по умолчанию; после отправки ответа соединение возвращается в пул ожидания (conn.reader). - Конвейерная обработка (pipelining) поддерживается: несколько запросов могут читаться подряд без закрытия соединения.
- Таймауты:
ReadTimeout,WriteTimeout,IdleTimeout. Первый защищает от медленных клиентов, второй — от медленной записи, третий — закрывает простаивающие keep-alive соединения. Shutdown(ctx)переводит сервер в режим закрытия: перестаёт принимать новые соединения, активные завершаются до истечения контекста.- HTTP/2: если сервер запущен с
TLSConfig, активируется HTTP/2 (с помощьюgolang.org/x/net/http2/h2cдля открытого текста или автосогласования TLS). Соединения мультиплексируются в одном TCP-соединении. - По умолчанию лимит на количество одновременных соединений отсутствует; защиту обеспечивает
net.Listenerс accept backlog и ОС-лимиты.
Типичные ошибки кандидатов
- Не обрабатывать ошибку
http.ErrServerClosedпослеShutdown. Это ожидаемая ошибка; её следует игнорировать при graceful shutdown. - Задавать таймауты только на клиенте, не задавая на сервере. Slowloris атаки могут исчерпать ресурсы. Устанавливайте
ReadHeaderTimeoutиReadTimeout. - Использовать глобальное состояние без защиты в обработчиках, что ведёт к гонкам.
- Забыть вызвать
Closeнаr.Bodyв обработчике. Хотя сервер автоматически закрывает тело после выхода обработчика, для гарантии и чистоты рекомендуетсяdefer r.Body.Close().
Возможные дополнительные вопросы интервьюера
Как одновременно слушать HTTP и HTTPS?
Запустить две горутины: srv.ListenAndServe() для HTTP и srv.ListenAndServeTLS(certFile, keyFile) для HTTPS. Или используйте tls.Listen.
Как ограничить количество входящих запросов?
Семафор с буферизованным каналом в middleware, либо golang.org/x/net/netutil.LimitListener.
Что такое ResponseController (Go 1.20+)?
Позволяет управлять поведением ответа: SetReadDeadline, SetWriteDeadline, Hijack для обновления протокола (WebSocket).
Что такое http.Handler и http.HandlerFunc?
Краткий ответ
http.Handler — интерфейс с методом ServeHTTP(ResponseWriter, *Request), основная абстракция обработки HTTP-запроса. http.HandlerFunc — адаптер, позволяющий использовать функцию func(ResponseWriter, *Request) как Handler. Маршрутизаторы, middleware реализуют Handler.
Развёрнутый ответ
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
Это позволяет писать обработчики как обычные функции:
var handler http.Handler = http.HandlerFunc(myFunc)
В DefaultServeMux регистрируются обработчики через Handle(pattern, handler) или HandleFunc(pattern, func).
Под капотом (internals)
ServeMuxхранит внутреннее представление маршрутов (muxEntry), осуществляет поиск по longest match и обход редиректов (например,/tree→ перенаправление на/tree/).HandlerFunc— просто тип функции, реализующий интерфейс; при вызовеServeHTTPвызывает базовую функцию. Никакой магии времени выполнения нет.
Типичные ошибки кандидатов
- Регистрировать функцию напрямую в
mux.Handleбез приведения типов.mux.Handle("/", myFunc)гдеmyFuncпросто функция — приведёт к ошибке компиляции, так какHandleожидаетHandler. Нужноhttp.HandlerFunc(myFunc). - Путать
HandleиHandleFunc.HandleFuncавтоматически оборачивает функцию вHandlerFunc, поэтому можноmux.HandleFunc("/", myFunc). - Реализовывать
ServeHTTPна структуре без поддержки конкурентного доступа к её полям. Обработчики вызываются в разных горутинах, любые общие поля должны быть защищены.
Возможные дополнительные вопросы интервьюера
Как создать цепочку middleware?
Middleware — функция func(http.Handler) http.Handler. Оборачиваем: handler = middleware1(middleware2(handler)).
Как DefaultServeMux сопоставляет пути с шаблонами?
Использует сравнение по длине: для / всё подходит, для /images/ — любые пути с этим префиксом. Если зарегистрированы и /images/, и /images/thumbnails/, то более специфичный матчится первым.
В чём разница между http.Handler и http.HandlerFunc в плане производительности?
Никакой; вызов ServeHTTP у HandlerFunc — прямой вызов функции, у кастомной структуры — вызов метода через интерфейс, но devirtualization inlining может свести разницу к нулю.
Как работает database/sql?
Краткий ответ
database/sql предоставляет универсальный интерфейс для работы с реляционными БД через драйверы (database/sql/driver). Центральные типы: sql.DB — пул соединений; sql.Tx — транзакция; sql.Rows — курсор результатов. Пакет обеспечивает управление соединениями, повторные попытки, контекстную отмену и сканирование результатов в Go-типы.
Развёрнутый ответ
Основные сценарии:
db, err := sql.Open("postgres", dsn)
rows, err := db.QueryContext(ctx, "SELECT id, name FROM users")
defer rows.Close()
for rows.Next() {
var id int; var name string
rows.Scan(&id, &name)
}
err = rows.Err()
sql.DB открывает ленивые соединения, управляет их пулом. SetMaxOpenConns, SetMaxIdleConns, SetConnMaxLifetime контролируют размеры и время жизни. При запросах драйвер возвращает driver.Rows, которые адаптируются в sql.Rows.
Транзакции: db.BeginTx(ctx, nil), возвращает *Tx, который должен быть зафиксирован (Commit) или откачен (Rollback). Вложенные транзакции не поддерживаются; их эмулируют savepoints через ExecContext.
Под капотом (internals)
sql.DBне открывает соединения сразу.connector(реализацияdriver.Connector) связываетDBс драйвером.- Пул соединений реализован внутри
sql.DB(freeConn— список свободных соединений,numOpen, мьютекс). При запросеconn(ctx)ищет свободное, если нет — создаёт новое, с учётом лимитов. Если лимит превышен, горутина встаёт в очередь ожидания с контекстом. QueryContext/ExecContextвызываютdb.connдля получения соединения, выполняют запрос, возвращают соединение в пул или закрывают, если ошибка фатальна.- При превышении
maxIdleлишние соединения закрываются по таймеру.ConnMaxLifetimeгарантирует закрытие старых для безопасности смены паролей, сброса DNS. Scanиспользует рефлексию для преобразования драйвер-значений в переданные указатели. Поддерживаютсяsql.Scanner,driver.Valuer.
Типичные ошибки кандидатов
- Не закрывать
rows. Это удерживает соединение в пуле и может исчерпать пул. Всегдаdefer rows.Close(), даже еслиScanможет вернуть ошибку. - Игнорировать
rows.Err()после цикла. Ошибка, возникшая во время итерации, не выбрасывается вScan, а сохраняется. Необходимо проверить после цикла. - Не использовать контекстные версии (
QueryContext,ExecContext), что лишает возможности таймаутов и отмены. - Смешивать
sql.DBиsql.Txбез передачи контекста. Транзакции привязаны к соединению, нужно гарантировать корректную передачуtxв репозиторий.
Возможные дополнительные вопросы интервьюера
Что делать, если драйвер не поддерживает context.Context?
Контекст игнорируется, запрос нельзя отменить. Стоит выбрать драйвер с поддержкой или реализовать внешний таймаут с закрытием соединения через db.Conn.
Как реализовать паттерн «репозиторий» с транзакциями?
Передавать абстракцию Querier (интерфейс с методами QueryContext, ExecContext), реализованную как *sql.DB и *sql.Tx.
Чем sql.DB отличается от sql.Conn?
Conn — отдельное выделенное соединение из пула, полученное через db.Conn(ctx). Полезно для специфичных операций с временными настройками сессии.
Что такое sql.DB и sql.Tx?
Краткий ответ
sql.DB — пул соединений с БД, безопасный для конкурентного использования, предоставляющий методы Query, Exec без привязки к конкретному соединению. sql.Tx — транзакция, связанная с ровно одним соединением; предоставляет те же методы (плюс Commit/Rollback) и гарантирует атомарность.
Развёрнутый ответ
db, _ := sql.Open("mysql", dsn)
// db — готова к использованию, сама открывает соединения
tx, _ := db.BeginTx(ctx, nil)
tx.ExecContext(ctx, "...")
tx.Commit() // или Rollback
После создания Tx, все операции внутри транзакции выполняются на одном и том же соединении. После Commit или Rollback соединение возвращается в пул, а Tx становится невалидным.
Под капотом (internals)
sql.DBсодержит мутекс, счётчик соединений, слайсfreeConn.connвнутренний включаетdriver.Conn, слот для сессии.BeginTxберёт соединение из пула (conn), вызываетdriver.Conn.BeginTx(если есть) илиBegin. Возвращает*Tx, который хранит ссылку на это соединение.- При
Commit/Rollbackдрайвер завершает транзакцию, после чего соединение можно использовать повторно. Если во время транзакции произошла ошибка (например,driver.ErrBadConn), транзакция помечается как «плохая» и соединение закрывается.
Типичные ошибки кандидатов
- Забыть
RollbackилиCommitпри ошибке. Еслиtxне освободить, соединение никогда не вернётся в пул, что исчерпает пул. Используйтеdefer tx.Rollback()сразу послеBeginTx,tx.Commit()отменит откат. - Передавать
Txв горутины без синхронизации.Txне горутино-безопасен, как иsql.DBне для одного соединения, ноTxтем более — все вызовы должны быть последовательными. - Использовать
txпослеCommit/Rollback. Паника или ошибка. Нужно обнулить.
Возможные дополнительные вопросы интервьюера
Как организовать вложенные транзакции?
Использовать savepoints через tx.Exec("SAVEPOINT sp"), но это сильно зависит от драйвера. Лучше пересмотреть архитектуру.
Чем sql.DB отличается от sql.Conn?
Conn — выделенное соединение, получаемое через db.Conn(ctx). Транзакция же также получает выделенное соединение, но Conn не начинает транзакцию.
Можно ли одновременно запускать несколько Query на одном *sql.Tx?
Только последовательно, потому что драйвер может быть не потокобезопасным. Параллельные запросы в одной транзакции требуют внешней синхронизации.
Как работает пул соединений в database/sql?
Краткий ответ
Пул соединений внутри sql.DB хранит простаивающие соединения в двусвязном списке (freeConn). При запросе (conn) сначала ищется простаивающее, если нет — создаётся новое при недостигнутом maxOpen. При лимите горутина блокируется в очереди ожидания с учётом контекста. Соединения с истёкшим временем жизни или превышающие maxIdle закрываются фоновой горутиной.
Развёрнутый ответ
Конфигурация:
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(5 * time.Minute)
db.SetConnMaxIdleTime(1 * time.Minute)
Когда вызывается db.QueryContext, берётся соединение: метод db.conn(ctx). Он ищет в freeConn, проверяет, не истекло ли время жизни или idle. Если подходящего нет, создаёт новое, вызывая connector.Connect. Если число открытых соединений достигло maxOpen, горутина ожидает на условной переменной (db.connRequests map), пока не освободится соединение или не истечёт контекст.
После использования соединение возвращается в пул (putConn), где попадает в freeConn как idle. Фоновая горутина connectionOpener периодически закрывает простаивающие соединения, превышающие maxIdle или maxLifetime.
Под капотом (internals)
sql.DB.freeConn— это[]*driverConn, не список, а слайс? На самом деле в Go < 1.17 былfreeConnкак двусвязный список, сейчас реализация на основеcontainer/list? Исходный код (database/sql/sql.go): действительноfreeConn— это[]*driverConn, мутексmu, и поиск линейный? Нет, смотря версию. В последней версии используетсяlistизcontainer/list, так как нужна быстрая вставка/удаление.connectorреализован драйвером (driver.Connector). При вызовеOpenбез контекстаConnectможет быть простым.- Каждое
driverConnсодержитdriver.Conn, мьютекс для сериализации доступа, временные меткиcreatedAt,lastUsed. db.maxLifetimeиmaxIdleTimeпроверяются при взятии из пула и периодически для удаления.
Типичные ошибки кандидатов
- Не установить
maxOpen, что может привести к исчерпанию ресурсов БД. По умолчаниюmaxOpenне ограничен (0 = unlimited). В продакшене обязательно задавать. - Задавать
maxIdleбольшеmaxOpen— бессмысленно, лишние idle никогда не создадутся; но ошибки нет, просто избыток. - Игнорировать
ConnMaxLifetimeпри использовании прокси/балансировщиков. Старые соединения могут указывать на недоступные узлы. Настройка помогает ротировать. - Не проверять ошибки при
SetConnMaxLifetimeна отрицательное значение — оно воспринимается как 0 (бесконечно).
Возможные дополнительные вопросы интервьюера
Что происходит при превышении maxOpen и истечении контекста?
Горутина удаляется из очереди ожидания, возвращается ошибка ctx.Err().
Как пул обрабатывает ошибки «плохого соединения» (bad connection)?
Когда операция возвращает driver.ErrBadConn, соединение помечается как неисправное и удаляется из пула вместо возврата. Следующий запрос может создать новое.
Можно ли использовать пул для разных баз данных с одним sql.DB?
Нет, sql.DB привязан к одному драйверу и одному DSN.
Как работает os.File?
Краткий ответ
os.File — структура, представляющая открытый файловый дескриптор. Реализует интерфейсы io.Reader, io.Writer, io.Seeker, io.Closer. Операции чтения/записи перенаправляют системные вызовы (read, write, pread, pwrite) с учётом буферизации ОС. Возможно небуферизированное прямое чтение (syscall.O_DIRECT).
Развёрнутый ответ
Открытие: os.Open (только чтение), os.Create (чтение/запись, перезаписывает), os.OpenFile с флагами.
f, err := os.Open("file.txt")
defer f.Close()
data := make([]byte, 1024)
n, err := f.Read(data)
Методы: ReadAt, WriteAt для случайного доступа без изменения позиции. Seek устанавливает смещение.
Под капотом (internals)
os.Fileсодержит*file(внутренняя структура), в которойfd int, имя, флаги и*syscall.FD(pollable).- Чтение через
fd.Readвызываетsyscall.Read(илиpreadдляReadAt) в зависимости от ОС. - Поддержка
poll.Descдля асинхронных операций с сетевыми дескрипторами (не относится к обычным файлам, кроме специальных). - На Linux для больших файлов может использоваться
sendfileчерезio.Copy. os.Fileблокирует операции; для неблокирующего ввода/вывода файлов нужно использоватьsyscall.O_NONBLOCKи select/poll черезsyscall.RawConn, но обычно обычные файлы всегда готовы к I/O.
Типичные ошибки кандидатов
- Не закрывать файлы. Утечка файловых дескрипторов в долгоживущих приложениях.
defer f.Close()не панацея: в циклах может накапливаться до выхода из функции. - Обрабатывать только ошибку открытия, игнорировать ошибки чтения/записи.
Readможет вернутьn < len(buf)иerr(например,io.EOF); нужно проверять. - Использовать
Seekбез учётаWhence. Неправильные константы (0,1,2) соответствуютio.SeekStart,io.SeekCurrent,io.SeekEnd. - Думать, что запись в файл гарантирует немедленную фиксацию на диске. Запись идёт в кеш ОС; для гарантии нужно вызвать
f.Sync().
Возможные дополнительные вопросы интервьюера
Как получить размер файла без чтения?
f.Stat() возвращает FileInfo, откуда берётся Size(). Или os.Stat(name).
Чем отличается os.Open от os.OpenFile?
Open вызывает OpenFile(name, O_RDONLY, 0). OpenFile позволяет задать флаги и права.
Что такое os.File.Fd() и зачем он нужен?
Возвращает целочисленный файловый дескриптор для прямых системных вызовов (например, syscall.Flock). Опасен, так как defer f.Close() закроет его, и ручные вызовы могут повредить.
Как работает os/signal?
Краткий ответ
Пакет os/signal доставляет уведомления о сигналах ОС в канал. signal.Notify(ch, sig...) регистрирует канал; при получении сигнала горутина рантайма отправляет его в канал. signal.Ignore отключает обработку по умолчанию. signal.Reset восстанавливает. Для graceful shutdown используют signal.NotifyContext.
Развёрнутый ответ
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
<-ctx.Done()
log.Println("Shutting down...")
Канал должен быть буферизованным (или готовым к чтению), чтобы не потерять сигналы. Рекомендуемый размер: 1 или больше. NotifyContext создаёт контекст, который отменяется при первом сигнале.
Под капотом (internals)
- Внутренний обработчик сигналов рантайма (
os/signal.signal_recv) запускает горутину, которая вызываетsignal_recvиз пакетаruntimeдля получения сигналов. - При
signal.Notifyсоздаётся структураhandler, которая помещается в глобальный список. Горутинаloopвsignal_unix.goчитает сигналы черезsignal_recvи отправляет в зарегистрированные каналы. Отправка неблокирующая: если канал заполнен, сигнал теряется, и в лог печатается предупреждение. signal.Ignoreотправляет специальную команду рантайму, чтобы он не использовал стандартный обработчик (например, дляSIGPIPE).signal.Resetсбрасывает к дефолтному поведению.- Рантайм перехватывает сигналы до того, как они попадут в стандартный обработчик, используя
sigactionиSA_ONSTACKдля гарантии обработки даже при переполнении стека.
Типичные ошибки кандидатов
- Не буферизировать канал или не читать из него вовремя. Если канал
signal.Notify(ch, syscall.SIGINT)небуферизирован и никто не читает в момент прихода сигнала, сигнал теряется, а обработчик по умолчанию может убить процесс. - Вызывать
signal.Notifyнесколько раз для одного канала — это приводит к нескольким регистрациям и дублированию сигналов? Нет, если канал тот же, вторая регистрация игнорируется. - Не вызывать
stop()послеsignal.NotifyContext. Это приводит к утечке горутины обработчика сигналов. Всегдаdefer stop(). - Игнорировать
syscall.SIGTERM, ожидая толькоSIGINT(Ctrl+C). В production-окружениях обычно посылаютSIGTERM.
Возможные дополнительные вопросы интервьюера
Как протестировать обработку сигналов?
Послать сигнал процессу через syscall.Kill(syscall.Getpid(), syscall.SIGINT), но это повлияет на весь тестовый процесс. Лучше использовать signal.Notify с каналом и ручной отправкой сигнала в тестовой горутине.
Можно ли перехватить SIGKILL или SIGSTOP?
Нет, эти сигналы не перехватываются. SIGKILL сразу убивает процесс.
Как signal.NotifyContext интегрируется с HTTP-сервером?
ctx от signal.NotifyContext передаётся в srv.Shutdown(ctx), чтобы дать завершиться активным запросам при получении сигнала.
Как работает flag?
Краткий ответ
Пакет flag реализует разбор аргументов командной строки в стиле POSIX/GNU. Определяются переменные с помощью flag.String, flag.Int и т.д., затем вызывается flag.Parse. Поддерживаются флаги с дефисами, двойными дефисами, -flag value, -flag=value, булевы флаги -flag. Нестандартные аргументы размещаются в flag.Args().
Развёрнутый ответ
var port int
flag.IntVar(&port, "port", 8080, "server port")
flag.Parse()
fmt.Println("Port:", port)
Перед Parse должны быть объявлены все флаги. После Parse флаги доступны по значениям. Порядок флагов не важен. Если указан неизвестный флаг, программа паникует (или можно установить flag.CommandLine.SetOutput и ловить ошибку). Также есть flag.FlagSet для создания подкоманд.
Под капотом (internals)
flagхранит картуformal map[string]*Flag.Parseперебираетos.Args[1:]. Если аргумент начинается с-или--, он считается флагом. Ищется вformal, затем парсится значение.- Булевы флаги могут идти без значения.
- Ошибки разбора записываются в
flag.CommandLine.Output()и вызываютos.Exit(2)или панику в тестах. - В Go 1.18+ функция
flag.Funcпозволяет задать произвольный тип со своей логикой парсинга.
Типичные ошибки кандидатов
- Вызывать
flag.Parseдо объявления всех флагов. Флаги, объявленные позже, не будут распознаны; лучше объявить вinit(). - Путать
flag.String(возвращает указатель) иflag.StringVar(принимает указатель). Часто новички пишутflag.String("name", "default", "usage")и забывают разыменовать. - Использовать
flag.Parse()в тестах без сброса состояния. Глобальныйflag.CommandLineможет быть уже разобран; нужно либо сбрасывать, либо использовать локальныйFlagSet. - Игнорировать
flag.Args()для позиционных аргументов. Позиционные аргументы доступны после вызоваParse.
Возможные дополнительные вопросы интервьюера
Как реализовать подкоманды как в go build?
Создать flag.NewFlagSet с именем подкоманды, вызвать flagSet.Parse(os.Args[2:]) после проверки первого аргумента.
Как задать обязательный флаг?
Пакет flag не поддерживает обязательность напрямую. Проверять, был ли изменён флаг, сравнив со значением по умолчанию (или с помощью flag.Visit).
Можно ли использовать flag с конфигурационными файлами?
Можно комбинировать: сначала flag.Parse, затем применить файл конфигурации, переопределяя значения флагов.
Как работает context?
Краткий ответ
Контекст — иерархическая структура для передачи крайних сроков, отмены и scope-данных через API. context.Context предоставляет методы Deadline(), Done(), Err(), Value(). Создаются контексты через Background(), WithCancel, WithDeadline, WithTimeout, WithValue. Отмена родителя распространяется на всех потомков. Главный принцип: передавать контекст первым аргументом.
Развёрнутый ответ
ctx, cancel := context.WithTimeout(context.Background(), 5 * time.Second)
defer cancel()
select {
case <-ctx.Done():
return ctx.Err()
case result := <-doWork(ctx):
}
context.WithValue хранит пары ключ-значение, рекомендуемый ключ — неэкспортируемый тип. Контекст неизменяем; WithValue возвращает новый контекст, оборачивающий родителя.
Под капотом (internals)
context.Background()иTODO()возвращают пустой контекстbackground(глобальная переменнаяbackgroundтипаemptyCtx), который никогда не отменяется.WithCancelвозвращаетcancelCtx, содержащий родителя,doneканал (закрытый при отмене), мьютекс и список дочернихcanceller. При вызовеcancel()закрывается каналdone, затем рекурсивно отменяются дочерние.WithDeadlineоборачивается вtimerCtx, который используетtime.Timerдля автоматического вызоваcancelпо истечении срока. Таймер очищается при отмене родителя.WithValueсоздаётvalueCtxс полямиkey,val.- Проверка
ctx.Done()возвращает канал, который закрыт при отмене; горутины могут одновременно ждать.
Типичные ошибки кандидатов
- Хранить контекст в структуре долгосрочно. Контексты должны передаваться аргументами, а не храниться как поле, если только это не
http.Requestс контекстом. Нарушает идиому «передавать контекст по цепочке вызовов». - Забывать вызвать
cancelпри созданииWithCancel,WithTimeout. Утечка горутин и таймеров. Обязательноdefer cancel(). - Использовать строки в качестве ключа в
WithValue. Риск коллизий между пакетами. Лучше создать свой типtype myKey struct{}. - Передавать контекст как
nil(прямо или неявно). Принимающая сторона должна ожидать корректный контекст;nilприводит к панике. Если контекст не нужен, передаватьcontext.TODO().
Возможные дополнительные вопросы интервьюера
Как реализовать собственный контекст с таймаутом, не используя стандартный?
Нужно аккуратно имплементировать Context интерфейс, управлять каналом Done. Но проще использовать готовый, оборачивая.
Может ли WithValue повлиять на производительность?
Цепочки контекстов могут стать глубокими; поиск Value идёт вверх по цепочке. В горячих путях лучше избегать излишних WithValue. Использовать локальные структуры.
Почему context не имеет метода Cause для ошибки?
С Go 1.20 Cause добавлен: context.Cause(ctx) возвращает причину отмены, переданную в WithCancelCause.
Как работает sync пакет?
Краткий ответ
sync предоставляет примитивы синхронизации: Mutex, RWMutex, WaitGroup, Cond, Once, Pool, Map. Они реализуют блокировки, ожидание событий, одноразовое выполнение и конкурентные структуры. Внутренне используют атомарные операции и early-спиннинг перед парковкой в планировщике.
Развёрнутый ответ
Основные примитивы:
Mutex: эксклюзивная блокировка,Lock/Unlock. Не реентерабельный.RWMutex: разделяемая/эксклюзивная.RLock/RUnlock,Lock.WaitGroup: ожидание группы горутин.Add,Done,Wait.Once: гарантирует однократное выполнение функции, даже при конкурентных вызовах.Cond: условная переменная на основеLocker, редкая необходимость.Pool: временный пул объектов для снижения аллокаций. Не подходит для долгосрочного кэширования.Map: конкурентная map, оптимизированная для стабильных ключей (в основном читаемых).
Под капотом (internals)
Mutex:- Внутри состояние (state) — 32 бита: заблокирован, разбужен, голодание, количество ожидающих. При
Lockиспользован fast path (атомарный CAS). При конкуренции горутина крутится (spin) несколько тактов, затем вызываетruntime_SemacquireMutex, которая паркует горутину в очередь ожидания. Unlockатомарно снимает флаг, при наличии ожидающих вызываетruntime_Semrelease.- «Голодающий» режим предотвращает бесконечное ожидание; после 1 мс переключается в режим голодания.
- Внутри состояние (state) — 32 бита: заблокирован, разбужен, голодание, количество ожидающих. При
RWMutex: аналогично, с разделением читающих/пишущих через атомарный счётчик.WaitGroup: внутриstate1— массив из трёхuint32: счётчик, количество ждущих, семафор.Addатомарно изменяет счётчик.Waitпри ненулевом счётчике увеличивает ждущих и вызываетsemacquire.Once: флагdone(uint32) и мьютекс. Быстрый путь: атомарное чтениеdone. Если 0, захватывается мьютекс, двойная проверка, вызовf, установкаdone=1.Pool:Newфункция. Внутри — per-P локальный пул и shared pool. ПриGetсначала проверяется локальный, при промахе крадёт из shared/других P, иначе вызываетNew.Putкладёт в локальный. Объекты в пуле очищаются GC при сборке мусора (между циклами), поэтому не годятся для долгоживущих кешей.Map: гибридная структура: read-only map (атомарный указатель) и dirty map с мьютексом. При большом количестве чтений обходится без блокировок. При записи промотируется dirty.
Типичные ошибки кандидатов
- Копирование
Mutex/WaitGroupпосле использования. Они не должны копироваться после первого использования; копия даст независимое состояние, гонки и дедлоки. Передавайте только по указателю. - Повторный
UnlockилиUnlockбезLock. Паникаsync: unlock of unlocked mutex. Используйтеdefer mu.Unlock()сразу послеLock. WaitGroup.AddпослеWait. Возникает гонка;Addдолжен вызываться до того, как горутина, вызывающаяWait, продолжит, либо внутри горутин доDone. Правило:Addдо запуска горутин.- Использование
sync.Mapвсегда как заменуmap + Mutex.sync.Mapэффективен только при преобладании чтений над записями и стабильных ключах. В иных случаях обычная map с мьютексом быстрее. - Утечки в
Poolиз-за сохранения ссылок на объекты, которые не возвращаются в пул с обнулением полей. Нужно обнулять передPut.
Возможные дополнительные вопросы интервьюера
Как реализовать реентерабельный мьютекс?
Go осознанно не имеет реентерабельных мьютексов; их можно эмулировать через sync.Mutex + идентификатор горутины, но это не идиоматично. Обычно перепроектируют код.
Как RWMutex защищает от писательской голодовки?
При ожидании писателя новые читатели блокируются, давая приоритет писателю. Это предотвращает бесконечное накопление читателей.
Что происходит с объектами в sync.Pool при GC?
Все объекты, не возвращённые в пул, могут быть собраны; при следующей сборке мусора victim кеш очищается. Пул — временное хранилище, не кеш.
Как работает sort?
Краткий ответ
Пакет sort предоставляет сортировку срезов через sort.Slice и sort.SliceStable, а также интерфейсы для пользовательских коллекций (sort.Interface). Алгоритм — интроспективная сортировка (комбинация быстрой, пирамидальной и вставками). Стандартная сортировка не стабильна; для стабильности используется SliceStable или sort.Stable.
Развёрнутый ответ
people := []Person{{"Bob", 30}, {"Alice", 25}}
sort.Slice(people, func(i, j int) bool { return people[i].Name < people[j].Name })
// стабильная
sort.SliceStable(people, func(i, j int) bool { return people[i].Age < people[j].Age })
Для сортировки коллекций, реализующих sort.Interface (Len, Less, Swap), можно использовать sort.Sort. Встроенные типы: sort.Ints, sort.Strings, sort.Float64s.
Под капотом (internals)
sort.Sort(data)вызываетquickSort(data, 0, data.Len(), maxDepth(data.Len()))— быстрая сортировка с выбором медианы по трём элементам. Если глубина рекурсии превышаетmaxDepth(≈ 2*log2(n)), переключается на пирамидальную сортировку (heapSort), гарантируя O(n log n) в худшем случае. Для коротких срезов (≤12) применяется сортировка вставками.SliceStableиспользует адаптивную сортировку слиянием (stable), эффективную для уже частично упорядоченных данных, с линейной сложностью на отсортированных данных.- В Go 1.22 в
sortдобавили generic-версииslices.Sortв пакетеslices, ноsortостаётся.
Типичные ошибки кандидатов
- Неправильная функция
Less, нарушающая транзитивность или антисимметричность. Это приводит к панике, бесконечному циклу или неверному порядку.Lessдолжен быть строгим частичным порядком. - Изменять слайс во время сортировки. Гонка, но в однопоточной программе тоже поведение неопределённое.
- Полагаться на стабильность
sort.Sort. Она не гарантирует стабильность; для стабильности использоватьsort.StableилиSliceStable. - Забыть о том, что
sort.Sliceпо-умолчанию не стабильна и для структур с одинаковыми ключами может менять порядок. Это важно для многоуровневой сортировки: сначала стабильная по второму ключу, потом нестабильная по первому (порядок второго сохранится). НоSliceStableвсегда сохраняет.
Возможные дополнительные вопросы интервьюера
Как отсортировать map по ключам?
Извлечь ключи в слайс, отсортировать, итерироваться по слайсу и получать значения из map.
Какая сложность sort.Slice?
O(n log n) в среднем и худшем, память O(log n) для стека.
Чем отличается sort.Slice и sort.SliceStable внутренне?
Slice использует quicksort/интроспективную; SliceStable — стабильную сортировку слиянием.
Как работает sort.Slice и sort.SliceStable?
Краткий ответ
sort.Slice сортирует срез, используя функцию сравнения, но не гарантирует стабильность. sort.SliceStable сохраняет исходный порядок элементов с равными ключами. Обе функции опираются на рефлексию для обмена элементами, что порождает аллокацию interface{}, но при сравнении вызывается предоставленная функция less.
Развёрнутый ответ
sort.Slice(s, func(i, j int) bool { return s[i] < s[j] })
sort.SliceStable(s, func(i, j int) bool { return s[i].Field < s[j].Field })
Обе функции принимают срез как interface{} и замыкание. Создаётся вспомогательная структура sort.Interface через reflect.Swapper.
Под капотом (internals)
Sliceполучает срез черезany, извлекаетreflect.Value, строитswapчерезreflect.Swapper, возвращая функцию, которая меняет элементы местами.- Затем создаётся
sort.InterfaceсLen,Less(замыкание),Swap. ВызываетquickSortилиstable. - Из-за использования
reflect.Swapper, каждая операция обмена вызывает рефлексивные вызовы (v.Index(i).Set(vj)), что несколько медленнее прямых операций над конкретными типами. Вslices.Sort(Go 1.21) всё компилируется без рефлексии благодаря дженерикам. SliceStableвызываетstable(data, data.Len()), гдеdata— враппер надsort.Interface, используется стабильная сортировка слиянием.
Типичные ошибки кандидатов
- Сравнение, изменяющее срез или имеющее побочные эффекты. Функция
lessвызывается многократно во время сортировки; побочные эффекты приведут к гонкам или повреждению данных. - Опечатки в
less, путающее порядок аргументов. Например,return s[j] < s[i]вместоs[i] < s[j]инвертирует сортировку. Проверять модульными тестами. - Использование
SliceStableбез необходимости, где производительность критична. Стабильная сортировка медленнее; если стабильность не нужна, применятьSlice. - Полагать, что
sort.Sliceработает с nil-срезом. Работает;Lenдля nil = 0, сортировка корректна.
Возможные дополнительные вопросы интервьюера
Как избежать накладных расходов reflect.Swapper?
Использовать slices.SortFunc из golang.org/x/exp/slices или стандартного пакета slices в Go 1.21+, где дженерики исключают рефлексию.
Можно ли сортировать срез с помощью sort.Slice и параллельной горутиной?
Нет, это небезопасно. Нужны блокировки или предварительное клонирование.
Почему sort.SliceStable гарантирует стабильность, а sort.Slice нет?
Потому что использует другой алгоритм: stable merge sort, который сохраняет относительный порядок равных элементов.
Как работает crypto/tls?
Краткий ответ
crypto/tls реализует протоколы TLS 1.2 и 1.3, предоставляя tls.Config для настройки сертификатов, проверки пиров, сессионных билетов. И сервер (tls.Listen), и клиент (tls.Dial) используют net.Conn с установкой безопасного канала. Пакет управляет рукопожатием, шифрованием, аутентификацией и сессиями.
Развёрнутый ответ
// Сервер
cert, _ := tls.LoadX509KeyPair("cert.pem", "key.pem")
config := &tls.Config{Certificates: []tls.Certificate{cert}}
ln, _ := tls.Listen("tcp", ":443", config)
// Клиент
conn, _ := tls.Dial("tcp", "example.com:443", &tls.Config{InsecureSkipVerify: false})
Поддерживает взаимную TLS аутентификацию, ClientHello-фильтрацию, выбор кривых, ALPN (HTTP/2, gRPC). В Go 1.22 улучшена поддержка ECH (Encrypted Client Hello).
Под капотом (internals)
tls.Listenпринимает TCP-соединения, для каждого запускаетserverHandshake, обменивается сообщениями ClientHello/ServerHello, устанавливает сессионные ключи после обмена ключами (ECDHE, DHE).Connвнутри хранитnet.Conn, буферы записи/чтения, состояние записи и чтения с отдельными шифрами, MAC.- TLS 1.3 рукопожатие упрощено до 1-RTT, использует PSK и 0-RTT (ранние данные). Ключ согласовывается по ECDHE.
- Проверка сертификатов: на клиенте по умолчанию сверяет цепочку с системным пулом корневых сертификатов, проверяет срок действия и соответствие имени хоста (SAN). Можно кастомизировать
VerifyPeerCertificate. - Возобновление сессий: TLS 1.2 использует session tickets (шифруются серверным ключом), TLS 1.3 — PSK.
- Криптографические операции используют
crypto/aes,crypto/ecdsa,crypto/x509,crypto/elliptic. Ключи хранятся в памяти.
Типичные ошибки кандидатов
- Установка
InsecureSkipVerify: trueбез необходимости. Это отключает проверку сертификата, делая клиент уязвимым к MITM. Использовать только для тестов или с кастомной верификацией. - Забыть задать
MinVersionи оставить TLS 1.0 включённым. По умолчаниюMinVersion= TLS 1.2 с Go 1.18? В Go 1.18+ дефолт 1.2. Но лучше явно задатьtls.VersionTLS12илиtls.VersionTLS13. - Переиспользование
tls.ConfigбезClone().Configсодержит состояние (например, session ticket key), безопасно клонировать. - Не настроить
ServerNameв клиентском соединении, когда IP используется. При прямом IP-адресе проверка имени хоста не работает; нужно указатьServerNameили отключать проверку с осторожностью.
Возможные дополнительные вопросы интервьюера
Что такое ALPN и зачем он нужен?
Application-Layer Protocol Negotiation — позволяет согласовать протокол (h2, http/1.1, gRPC) во время рукопожатия без дополнительных round-trips. Задаётся в tls.Config.NextProtos.
Как защищена память ключей?
Стандартно ключи хранятся в Go-памяти, могут попасть в swap. Для защиты можно использовать аппаратные модули (HSM) через crypto.Signer, но не встроено.
Как работает ротация сессионных ключей в TLS 1.2?
Сервер периодически меняет ключ SessionTicketKey через tls.Config.SetSessionTicketKeys. Клиент не замечает смены, старые билеты становятся невалидны.
Как работает net/http/httptest для тестирования?
Краткий ответ
Пакет httptest предоставляет утилиты для тестирования HTTP-обработчиков: httptest.NewServer запускает реальный HTTP-сервер на локальном порту, а httptest.NewRequest и httptest.ResponseRecorder позволяют тестировать обработчики без сети, захватывая ответ.
Развёрнутый ответ
// Модульный тест обработчика
handler := http.HandlerFunc(myHandler)
req := httptest.NewRequest("GET", "/foo", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
resp := w.Result()
body, _ := io.ReadAll(resp.Body)
// Проверяем status, body, headers
Для интеграционных тестов:
ts := httptest.NewServer(mux)
defer ts.Close()
resp, _ := http.Get(ts.URL + "/path")
NewTLSServer для HTTPS.
Под капотом (internals)
ResponseRecorderреализуетhttp.ResponseWriter, сохраняя статус, заголовки, тело во внутреннийbytes.Buffer.Result()возвращает сконструированныйhttp.Response. Поддерживаетhttp.Flusher,http.Pusher.NewServerсоздаётhttptest.Server, который в фоне слушает на127.0.0.1:0, получая случайный порт.URLвозвращаетhttp://127.0.0.1:<port>. ПриCloseсервер останавливается.- Сервер использует
net.Listenи реальную сеть, что может быть медленно по сравнению с рекордером, но позволяет тестировать клиентскую логику.
Типичные ошибки кандидатов
- Забыть вызвать
resp.Body.Close()после чтения изw.Result(). Хотя тело в рекордере — простоbytes.Buffer,http.Responseтребует закрытия для освобождения ресурсов. - Проверять тело до вызова
w.Result(). Данные финализируются приResult(), после негоw.Bodyстановится недоступным для чтения? Нет,Bodyчитается из рекордера до вызоваResult()? Лучше получать тело изresp.Body. - Использовать
httptest.NewServerв тестах, где достаточноResponseRecorder, что замедляет тесты из-за реального сетевого стека. - Не устанавливать
Content-Typeв запросе при тестировании поведения разбора.NewRequestпо умолчаниюapplication/octet-stream. Для JSON нужноreq.Header.Set("Content-Type", "application/json").
Возможные дополнительные вопросы интервьюера
Как протестировать WebSocket с httptest?
Нужно использовать net/http/httptest.NewServer и подключаться к нему WebSocket-клиентом, потому что ResponseRecorder не поддерживает hijack.
Можно ли создать сервер с TLS без реальных сертификатов?
httptest.NewTLSServer генерирует самоподписанный сертификат, клиент должен использовать http.Client с InsecureSkipVerify или кастомным транспортом, доверяющим сертификату сервера.
Как симулировать медленное соединение?
Обернуть net.Listener сервера с помощью netutil.LimitListener или использовать slowConn обёртку.
Как работает text/template и html/template?
Краткий ответ
Оба пакета — системы шаблонизации на основе данных. text/template генерирует произвольный текстовый вывод, выполняя простые подстановки и управляющие конструкции. html/template оборачивает text/template с автоматическим контекстным экранированием для безопасной генерации HTML, предотвращая XSS.
Развёрнутый ответ
tmpl := template.Must(template.New("test").Parse("Hello, {{.Name}}"))
tmpl.Execute(os.Stdout, map[string]string{"Name": "World"})
Поддерживает действия: {{if}}, {{range}}, {{with}}, {{template}}, {{block}}, вызов функций. Контекст в html/template понимает, где выводится значение — в HTML-теле, атрибуте, JavaScript, CSS, URL — и применяет соответствующее экранирование (html.EscapeString, js.EscapeString, url.QueryEscape).
Под капотом (internals)
- Парсинг шаблона строит дерево узлов (
parse.Tree).Executeобходит дерево, рефлексивно извлекая данные из структуры. - Контекстное экранирование в
html/templateреализовано через анализ графа переходов состояний (HTML, JS, CSS, URL). При компиляции шаблона строится цепочка контекстов, вставляются необходимые функции экранирования. - Данные в шаблон передаются через
reflect.Value, поддерживаются каналы, вызовы методов. Mustпаникует при ошибке парсинга, удобно для глобальных переменных.
Типичные ошибки кандидатов
- Забыть про экранирование в
text/templateпри генерации HTML. Это приводит к XSS. Всё, что отдаётся в веб, должно использоватьhtml/template. - Использовать
html/templateсо сложной логикой, которая изменяет порядок вывода и ломает контекст. Несмотря на умность, пакет может ошибаться с определением контекста; лучше вынести логику из шаблона. - Вызывать функции, которые возвращают шаблонный размет (HTML) без маркировки типа
template.HTML. Вhtml/templateстроки экранируются, если явно не указан safe-type. Для безопасной вставки проверенного HTML используйтеtemplate.HTML. - Передавать
nilв Execute как данные. Если шаблон ожидает поля, получитсяnildereference; нужно передавать пустую структуру.
Возможные дополнительные вопросы интервьюера
Как html/template определяет, что значение является URL, и экранирует его?
По контексту: если значение выводится внутри атрибута href или src, оно подвергается URL-экранированию, гарантируя корректный синтаксис и отсутствие javascript:.
Чем {{.}} отличается от {{$.}}?
. — курсор; $ — корневой объект данных, переданный в Execute. Полезно при вложенных range.
Как реализовать пользовательские функции в шаблоне?
Через template.FuncMap и tmpl.Funcs(funcMap).
Как предотвратить XSS с помощью html/template?
Краткий ответ
html/template автоматически экранирует все подставляемые значения в соответствии с контекстом (HTML, атрибут, JS, CSS, URL). Достаточно всегда использовать html/template вместо text/template. Для явной вставки безопасного HTML использовать тип template.HTML, но только после тщательной санитизации контента.
Развёрнутый ответ
Правильный подход: все HTML-страницы рендерить через html/template. Пример:
var tmpl = template.Must(template.New("page").Parse(`<a href="/user?name={{.Name}}">Profile</a>`))
// Name = "John" → href="/user?name=John" (авто URL-экранирование)
Если злоумышленник передаст Name = "javascript:alert(1)", html/template заметит контекст URL и заменит на безопасный #ZgotmplZ, или экранирует пробелы, предотвращая выполнение JavaScript. В контексте JavaScript-строки автоматически применяется \xHH-кодирование.
Важно: если вы используете template.HTML(value) для вставки сырого HTML, это эквивалентно доверительному пометке, и никакого экранирования не будет. Использовать только после применения библиотек санитизации (например, bluemonday).
Под капотом (internals)
- Пакет строит «контекстную цепочку» при компиляции шаблона и вставляет экранирующие функции (
htmlEscaper,urlEscaper,jsStrEscaperи др.). - В случае потенциально опасных переходов (example: URL с
javascript:) возвращаетсяtemplate.HTML("")с кодом ошибки, но с Go 1.13+ это приводит к замене на#ZgotmplZ. - При использовании
template.HTMLэкранирование пропускается. Так же работаютtemplate.JS,template.CSS,template.URL.
Типичные ошибки кандидатов
- Использовать
text/templateдля HTML. Это неминуемо приводит к XSS, если данные не экранируются вручную. Всегдаhtml/template. - Помечать пользовательские данные как
template.HTMLбез санитизации. Это открывает XSS. Нельзя доверять вводу. - Не учитывать, что
html/templateможет изменить структуру документа при неверном контексте. Например,{{.}}в атрибуте без кавычек может привести к тому, что злоумышленник добавит атрибуты; защита: всегда заключать в кавычки.
Возможные дополнительные вопросы интервьюера
Может ли html/template полностью защитить от DOM XSS?
Нет, если JavaScript динамически вставляет данные в DOM без экранирования. Шаблонизатор защищает серверный рендеринг, но не клиентский код.
Как обрабатывать JSON, встроенный в <script>?
Использовать template.JS для внедрения, но предварительно сериализовать через json.Marshal и экранировать символы </ и ]]>, чтобы не сломать HTML/XML.
Что такое контекстная автоэкранировка и когда она может ошибаться?
Это способность пакета определять, в каком контексте выводится значение, и применять экранирование. Ошибки возможны при сложных запутанных шаблонах, где пакет неверно определяет экранирование, но это редко.
Как работает reflect?
Краткий ответ
Пакет reflect предоставляет возможность исследовать типы и значения во время выполнения: получать информацию о полях структуры, методах, тегах, а также динамически вызывать функции и изменять переменные. Ключевые типы — reflect.Type (описание типа) и reflect.Value (конкретное значение). Реализован через метаданные, сохраняемые компилятором для каждого типа.
Развёрнутый ответ
v := reflect.ValueOf(x)
t := v.Type()
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
val := v.Field(i).Interface()
}
Можно создавать новые значения: reflect.New, reflect.MakeSlice. Устанавливать значения через v.Set(reflect.ValueOf(42)) при условии, что значение адресуемо (указатель). reflect.Indirect разыменовывает указатели.
Под капотом (internals)
- Каждый Go-тип имеет дескриптор времени выполнения (
runtime._type).reflect.Type— обёртка над*rtype.reflect.Value— тройкаtyp *rtype,ptr unsafe.Pointer,flag(флаги). - Компилятор генерирует метаданные для всех типов, используемых в пакете (и тех, на которые есть ссылки через рефлексию). В них хранится размер, alignment, имена полей, offsets, карта методов.
- При
ValueOfпроисходит escape analysis: если значение может быть изменено или передано через интерфейс, оно аллоцируется на куче. Для маленьких значений может быть скопировано. Interface()превращаетValueобратно вinterface{}.- Рефлексия нарушает безопасность типов; некорректный вызов приводит к панике.
Типичные ошибки кандидатов
- Пытаться изменить неадресуемое значение.
v := reflect.ValueOf(x); v.SetInt(5)паникует. Нужноreflect.ValueOf(&x).Elem().SetInt(5). - Использовать
Interface()на нулевомValue. Паника. Проверятьv.IsValid(). - Полагать, что рефлексия быстра. Она на порядок медленнее прямого доступа. Не использовать в горячих путях.
- Не учитывать, что
reflect.DeepEqualсравнивает неэкспортированные поля, что может привести к неожиданностям при сравнении структур с мьютексами. Использовать с осторожностью.
Возможные дополнительные вопросы интервьюера
Как получить все методы типа через рефлексию?
t.NumMethod() и t.Method(i) дают reflect.Method, содержащий имя и Func.
Как рефлексия обрабатывает интерфейсы?
ValueOf принимает interface{}. Если передать конкретное значение, будет его конкретный тип. Чтобы получить информацию о интерфейсном типе, нужно использовать reflect.TypeOf((*io.Reader)(nil)).Elem().
Чем reflect.TypeOf отличается от reflect.ValueOf?
TypeOf возвращает только метаданные типа, ValueOf — конкретное значение и тип.
Когда стоит избегать reflect?
Краткий ответ
Следует избегать reflect, когда критичны производительность, безопасность типов на этапе компиляции и читаемость. Он лишает статической проверки, затрудняет отладку и рефакторинг. Используйте только при обоснованной необходимости: универсальные библиотеки (JSON, ORM), где невозможно обойтись кодогенерацией.
Развёрнутый ответ
Красные флаги:
- Циклы с высокой частотой вызовов, где
reflectвызывает значительные накладные расходы. - Логика, которую можно выразить через интерфейсы или дженерики (Go 1.18+).
- Потребность в строгой типизации для предотвращения ошибок.
Примеры альтернатив: дженерики для обобщённых алгоритмов, интерфейсы для полиморфизма, кодогенерация (например, go generate, stringer) для перечислений.
Под капотом (internals)
- Каждый вызов
reflect.Valueтребует escape анализа и потенциальной аллокации; доступ к полям черезIndex/Fieldне встраивается. - Сравнение производительности: прямой доступ к полю ~0.3 нс, через рефлексию ~50-100 нс.
- Рефлексия лишает компилятор возможности инлайнинга и оптимизаций dead code elimination.
Типичные ошибки кандидатов
- Использовать
reflectдля сортировки или простых операций, которые можно сделать через интерфейсы/дженерики. Результат: замедление в десятки раз. - Писать «фреймворк» на рефлексии с динамической диспетчеризацией, не имея на то веских причин. Это усложняет поддержку и дебаггинг.
- Злоупотреблять
reflect.DeepEqualв тестах без понимания его ограничений (неэкспортированные поля). - Пытаться обойти ограничения Go через
reflect(создание невалидных типов), что может привести к неопределённому поведению.
Возможные дополнительные вопросы интервьюера
В каких местах стандартной библиотеки оправдано применение рефлексии?
encoding/json, fmt, sort.Slice, text/template, database/sql (сканирование). Везде, где данные произвольного типа должны быть обработаны обобщённо.
Как дженерики снижают потребность в рефлексии?
Они позволяют писать типобезопасный обобщённый код без interface{} и рефлексии, компилятор генерирует специализированный код.
Можно ли обойтись без рефлексии для сериализации?
Да, через кодогенерацию (например, ffjson, easyjson), что даёт большую производительность, чем рефлексия.
Как работает unsafe?
Краткий ответ
Пакет unsafe даёт обход системы типов Go: указатели произвольного преобразования (unsafe.Pointer), арифметику указателей (через uintptr), доступ к полям по смещению (unsafe.Offsetof), и создание макетов памяти. Компилятор не проверяет безопасность; некорректное использование приводит к порче памяти, нарушению сборщика мусора и неопределённому поведению.
Развёрнутый ответ
Ключевые элементы:
unsafe.Pointer— обобщённый указатель, может быть конвертирован в любой указатель и обратно.uintptr— целое число для арифметики; не защищает от GC.unsafe.Sizeof,unsafe.Offsetof,unsafe.Alignof— получение информации о памяти.- Допустимые паттерны (по документации): (1) конвертация
*T1в*T2при одинаковом layout; (2) преобразование pointer в uintptr и обратно — только внутри одного выражения, иначе GC может переместить объект; (3) системные вызовы (черезsyscall.Syscall); (4) доступ к полям черезunsafe.Add,unsafe.Slice.
Под капотом (internals)
unsafe.Pointer— это любой Go-указатель, игнорирующий типизацию. В некоторых реализациях компилятора это просто*void.- Арифметика указателей через
unsafe.Add(ptr, len)(Go 1.17+), которая добавляет смещение в байтах. - GC рассматривает
unsafe.Pointerкак живой указатель (если он указывает в кучу). Когда он преобразован вuintptr, GC не отслеживает и объект может быть перемещён/собран. - Строгие правила:
uintptrне должен храниться в переменной дольше выражения, которое включает преобразование вunsafe.Pointer.
Типичные ошибки кандидатов
- Хранение
uintptrв переменной и последующее преобразование в указатель. Между этими событиями GC может переместить объект, указатель станет невалидным. Следует выполнять преобразование в одном выражении. - Преобразование произвольных указателей в
unsafe.Pointerи обратно без гарантий совместимости памяти. Это может нарушить alignment или strict aliasing. - Использование
unsafe.Pointerдля обхода экспорта полей структуры. Неэкспортированные поля могут менять смещение между версиями, ведёт к хрупкости. - Создание
unsafe.Sliceс неправильным размером/вместимостью, порождая повреждение памяти.
Возможные дополнительные вопросы интервьюера
Как безопасно преобразовать []byte в string без копирования?
Используя unsafe.SliceData и unsafe.StringData (Go 1.20+), например unsafe.String(&b[0], len(b)). Но модификация байтов после этого неопределена, так как строка immutable.
Почему не рекомендуется использовать unsafe в продакшене?
Потому что код становится зависимым от деталей реализации компилятора, может сломаться при обновлении версии Go, и обходит проверки безопасности памяти.
Какие новые функции появились в Go 1.17+ для уменьшения необходимости unsafe?
unsafe.Add, unsafe.Slice — стандартизированная арифметика, менее подверженная ошибкам, чем ручные uintptr манипуляции.
Как работает syscall?
Краткий ответ
Пакет syscall предоставляет низкоуровневый интерфейс к системным вызовам ОС. Экспортирует функции, такие как syscall.Open, syscall.Read, syscall.Write, syscall.Socket, syscall.Bind. Использует uintptr для указателей и номеров дескрипторов. Служит основой для os, net. В сборочных целях для Windows используется golang.org/x/sys/windows.
Развёрнутый ответ
fd, err := syscall.Open("file.txt", syscall.O_RDONLY, 0)
buf := make([]byte, 1024)
n, err := syscall.Read(fd, buf)
syscall.Close(fd)
Пакет платформозависим; под капотом на Linux вызывает Syscall6, Syscall, реализованные на ассемблере (или через runtime.syscall). Сигналы, epoll, poll, termios, ptrace — всё доступно.
Под капотом (internals)
- Каждая функция, например
Open, вызываетSyscallс номером syscall (SYS_openat), передавая аргументы через регистры. На amd64:syscall.SyscallделаетMOVQаргументов иSYSCALL. syscallтакже предоставляетRawSyscallдля минимального вызова без проверки сигналов.- Внутренности:
syscall.Syscallвызываетruntime.syscall_syscall, который обрабатывает прерывания и возвращает ошибку. syscall.Mmap,syscall.Munmapиспользуют прямые системные вызовы.
Типичные ошибки кандидатов
- Неправильное использование
uintptrдля указателей вSyscall. Передачаuintptr(buf)безопасна только еслиbufне перемещается во время вызова; syscall гарантирует, что GC не срабатывает во время блокирующего syscall? На самом деле syscall в Go может привести к зависанию GC, но нужно гарантировать, что срезbufдостижим на протяжении вызова. - Забыть закрыть файловый дескриптор после
syscall.Open. Утечка дескрипторов. - Использовать
syscallнапрямую вместо кроссплатформенных обёртокos, теряя переносимость.
Возможные дополнительные вопросы интервьюера
Почему syscall считается замороженным?
Из-за платформозависимости и сложности поддержки. Рекомендуется использовать golang.org/x/sys или стандартные os/net.
Как сделать неблокирующий accept?
Через syscall.Accept4(fd, syscall.SOCK_NONBLOCK). Но проще использовать net.
Как syscall взаимодействует с планировщиком?
При блокирующем системном вызове горутина отпускает P (процессор), и планировщик запускает другую горутину. Это позволяет переиспользовать потоки.
Как работает os/exec и как безопасно выполнять внешние команды?
Краткий ответ
Пакет os/exec запускает внешние команды, настраивая Cmd с аргументами, переменными окружения, stdin/stdout/stderr. Безопасность: избегать sh -c с пользовательским вводом, всегда передавать аргументы отдельно (не через строку команды), очищать окружение при необходимости, использовать контекст для таймаута и отмены.
Развёрнутый ответ
cmd := exec.CommandContext(ctx, "ls", "-l", "/tmp")
cmd.Stdout = os.Stdout
err := cmd.Run()
Команда запускается через cmd.Start(), ожидается cmd.Wait(). CombinedOutput захватывает stdout+stderr. Для ограничения времени: cmd.Cancel = func() error { return cmd.Process.Signal(os.Interrupt) }.
Безопасность:
- Не передавать пользовательский ввод напрямую в оболочку:
Command("sh", "-c", "echo " + userInput)— возможность инъекции. Всегда используйтеCommand(name, arg...)без оболочки. - Не запускать команды с
sudoбез крайней необходимости. - Устанавливать
Pathк бинарнику явно, если важно. - Очищать
Envи добавлять только необходимое:cmd.Env = append(os.Environ(), "FOO=bar").
Под капотом (internals)
Cmd.Startсоздаёт процесс черезos.StartProcess, который используетsyscall.ForkExec(Unix) илиCreateProcess(Windows). Файловые дескрипторы stdin/stdout/stderr дублируются черезsyscall.Dup2.- Контекстный вызов регистрирует горутину, ожидающую
ctx.Done(), и при отмене вызываетcmd.Cancel(по умолчаниюos.Process.Kill). Waitсобирает статус завершения, освобождает ресурсы ОС.
Типичные ошибки кандидатов
- Строить команду через строку с
sh -cи интерполяцией. Подвержено инъекциям. Исправление:Command("echo", userInput). - Забыть обработать
cmd.StdoutPipeдоStart, а потом повесить на чтение, приводя к дедлоку. - Не вызвать
WaitпослеStartс пайпами, оставляя зомби-процесс. - Игнорировать ошибку
cmd.Run(), когда она не nil. Ошибка может быть*exec.ExitError, содержащей код возврата.
Возможные дополнительные вопросы интервьюера
Как безопасно выполнить команду с ограничением доступа к файловой системе?
Использовать syscall.Chroot (привилегии root) или Linux namespaces, но exec сам не изолирует. Лучше контейнеризация (Docker).
Как перенаправить stdout программы в логгер с ограничением размера?
Подключить io.LimitReader к пайпу или использовать cmd.Stdout = limitedWriter.
Как обеспечить, чтобы дочерний процесс не пережил родительский?
Поддерживать связь через неблокирующий контроль; при cmd.Cancel убивать процесс. Но если родитель аварийно завершится, нужно syscall.Pdeathsig (Linux) или Setpgid.
Как работает log и log/slog (Go 1.21+)?
Краткий ответ
log — простой пакет для логирования в io.Writer с префиксом, флагами времени и файла. log/slog (с Go 1.21) предлагает структурированное логирование с уровнями (Debug, Info, Warn, Error), атрибутами (ключ-значение), поддержкой контекстов, пользовательскими обработчиками и эффективной записью без аллокаций через LogAttrs.
Развёрнутый ответ
Традиционный log:
log.SetFlags(log.LstdFlags | log.Lshortfile)
log.Println("Hello")
slog:
logger := slog.New(slog.NewJSONHandler(os.Stderr, nil))
logger.Info("user login", "user", userID, "ip", remoteAddr)
slog поддерживает structured logging, группировку, уровни, With, LogAttrs для минимизации аллокаций. Handler — интерфейс для произвольного бэкенда (JSON, текст, syslog, и т.д.).
Под капотом (internals)
logиспользуетsync.Mutexдля сериализации вывода вWriter; форматирует время.slogоснован наHandlerинтерфейсе. Встроенные:JSONHandler,TextHandler. При вызовеInfoсоздаётсяRecord, содержащий время, уровень, сообщение, слайсAttr.Handler.Handleобрабатывает.- Оптимизация
LogAttrs: передача атрибутов какslog.String, избегая аллокаций интерфейсов. slogможет выводить stack trace при вызовеWithStack.- Извлечение из контекста: можно внедрить
Handlerс контекстом черезslog.WithAttrs.
Типичные ошибки кандидатов
- Смешивать
logиslogв одном проекте, получая несогласованный формат. Лучше мигрировать полностью. - Использовать
log.Panicиlog.Fatalбездумно. Они вызываютos.Exitили панику, останавливая все горутины. Для серверов лучше возвращать ошибки. - При использовании
slogпередавать пары атрибутов нечётным количеством. Это паника:slog.Info("msg", "key")— не хватает значения. - Думать, что
slogпо умолчанию логирует в stderr. По умолчаниюslog.SetDefaultставит текстовый обработчик в stderr, но при создании нового логгера нужно передавать Writer.
Возможные дополнительные вопросы интервьюера
Как добавить трассировку в каждый лог через slog?
Использовать middleware-обработчик, который извлекает traceID из контекста и добавляет в атрибуты, или создать Handler-обёртку.
Как slog избегает аллокаций при LogAttrs?
LogAttrs принимает variadic slog.Attr, а не any, благодаря чему не происходит упаковка в интерфейсы и не аллоцируется память.
В чём разница между slog.Info и slog.LogAttrs?
Info принимает any пары (чередующиеся ключи-значения), вызывает упаковку и рефлексию. LogAttrs принимает готовые slog.Attr, что быстрее.
Что такое io/fs и зачем он нужен?
Краткий ответ
Пакет io/fs определяет абстрактную файловую систему как интерфейс fs.FS с методом Open(name string) (fs.File, error). Позволяет работать с файлами независимо от хранилища: OS, embed, zip, память. Сопутствующие интерфейсы: fs.ReadDirFS, fs.StatFS, fs.GlobFS, fs.SubFS. Унифицирует код для тестирования и абстракции.
Развёрнутый ответ
Основной интерфейс:
func readConfig(fsys fs.FS) ([]byte, error) {
f, err := fsys.Open("config.json")
if err != nil { return nil, err }
defer f.Close()
return io.ReadAll(f)
}
Можно передать os.DirFS("/etc") или embed.FS. fs.WalkDir рекурсивно обходит файлы. fs.ReadFile — удобная обёртка.
Под капотом (internals)
fs.FS— минимальный интерфейс.os.DirFSвозвращает*os.Fileкакfs.File.embed.FS— встроенная файловая система только для чтения, реализуетfs.ReadDirFS,fs.StatFS.fs.WalkDirиспользуетReadDirи рекурсию.- Пакет также предоставляет функции проверки (
fs.ValidPath), валидации пути без выхода за корень.
Типичные ошибки кандидатов
- Пытаться открыть путь с ведущим слэшем (
/config.json), что нарушает требованияfs.FS(пути не должны начинаться с/). Нужноfs.ValidPath. - Использовать
fs.FSтам, где нужна запись. Интерфейс только для чтения. Для записи нужны другие абстракции. - Забыть про закрытие
fs.File, особенно приfs.WalkDir, который возвращает файл в callback. - Полагать, что
fs.ReadDirвозвращает отсортированный список. Начиная с Go 1.17,ReadDirсортирует имена. Но собственные реализацииfs.FSмогут не соблюдать.
Возможные дополнительные вопросы интервьюера
Как протестировать код, использующий fs.FS?
Создать fstest.MapFS с виртуальными файлами: fstest.MapFS{"hello.txt": &fstest.MapFile{Data: []byte("world")}}.
Чем fs.File отличается от os.File?
fs.File — интерфейс (только чтение, Stat, Read, Close). os.File — конкретная реализация, дополнительно поддерживает запись, seek, fd и т.д.
Как embed использует io/fs?
embed.FS реализует fs.ReadDirFS, fs.StatFS, позволяя встроенным файлам быть доступными через стандартные абстракции.
Как работает embed для встраивания файлов?
Краткий ответ
Пакет embed (директива //go:embed) встраивает файлы и директории непосредственно в Go-бинарник во время компиляции, предоставляя доступ к ним через переменные типа embed.FS, string или []byte. Это read-only файловая система, удовлетворяющая io/fs.FS.
Развёрнутый ответ
import _ "embed"
//go:embed templates/*.html
var tmplFS embed.FS
//go:embed config.json
var configData []byte
data, _ := tmplFS.ReadFile("templates/index.html")
Поддерживаются шаблоны glob: *.txt, directory/*.png. Вложенные директории рекурсивно включаются.
Под капотом (internals)
- Компилятор на этапе сборки читает указанные файлы и упаковывает их в секцию данных бинарного файла (в
go:embedдобавляет их вruntime-таблицу встроенных файлов). - Переменная
embed.FSинициализируется указателем на внутреннийembedFileс таблицей файлов. embed.FSреализуетfs.ReadDirFS,fs.StatFS.Openвозвращаетio/fs.File, который читает из памяти.- Сжатие не применяется; при необходимости сжимают контент до
//go:embed(например,compress/gzip).
Типичные ошибки кандидатов
- Использовать абсолютные пути или
..в шаблонах.embedзапрещает пути, выходящие за корень модуля. - Попытка использовать
//go:embedс переменными, которые не являютсяstring,[]byteилиembed.FS. Ошибка компиляции. - Забыть, что встроенные файлы занимают место в бинарнике. Большие файлы могут значительно увеличить размер.
- Пытаться писать во встроенную ФС. Она read-only, вызовет панику.
Возможные дополнительные вопросы интервьюера
Можно ли встроить файлы по абсолютному пути вне модуля?
Нет, только относительно директории с исходным кодом.
Как embed работает с бинарным файлом, сжатым через upx?
upx может изменить структуру, embed не рассчитан на это и может сломаться. Лучше не сжимать.
Как проверить размер встроенных данных на этапе компиляции?
Использовать runtime-проверку: len(configData) > maxSize, но компиляционно нельзя. Можно написать тест.
Как работает testing пакет для бенчмарков?
Краткий ответ
Бенчмарки в Go — функции вида func BenchmarkXxx(b *testing.B), запускаемые go test -bench. Инфраструктура testing.B предоставляет таймер, управление циклом (b.N), остановку/запуск таймера, выделение памяти на итерацию (ReportAllocs). Бенчмарки многократно вызываются для получения устойчивых метрик, выводят время на операцию и аллокации.
Развёрнутый ответ
func BenchmarkConcat(b *testing.B) {
s := "hello"
for i := 0; i < b.N; i++ {
_ = s + " world"
}
}
Фреймворк сам определяет b.N, увеличивая его до стабилизации времени. b.ResetTimer сбрасывает таймер после ресурсоёмкой подготовки. b.StopTimer/StartTimer управляют измерением. b.ReportAllocs включает отчёт о выделениях памяти.
Под капотом (internals)
testing.B.Runвызывает бенчмарк в цикле, экспоненциально увеличиваяNот 1 до тех пор, пока измеренное время не станет стабильным (по умолчанию >= 1 секунда суммарного времени).- Внутренне используется
benchTimeпакетаtesting, функцияrunBenchmarkизмеряет время с помощьюtime.Nowвокруг вызова функции бенчмарка. - Отчёт об аллокациях получается через
runtime.ReadMemStatsдо и после, либо черезtesting.AllocsPerRun. - Параллельные бенчмарки (
b.RunParallel) запускают несколько горутин.
Типичные ошибки кандидатов
- Не учитывать, что
b.Nможет быть 0, если бенчмарк не может завершиться. Избегать бесконечных циклов. - Забыть
b.ResetTimerпосле дорогой настройки, что искажает результаты. - Использовать
b.Nдля выделения ресурсов внутри цикла, создавая непреднамеренные аллокации. Подготовить данные заранее. - Сравнивать бенчмарки на разных машинах без указания
-benchtimeи-count. Нестабильность окружения; использовать-benchtime=10xдля повторений.
Возможные дополнительные вопросы интервьюера
Как измерить количество аллокаций в бенчмарке?
b.ReportAllocs() включает вывод allocs/op. Или testing.AllocsPerRun(100, func(){...}).
Можно ли пропустить бенчмарк при определённых условиях?
b.Skip внутри функции бенчмарка.
Как работает b.RunParallel?
Создаёт pb *testing.PB с методом Next(), горутины запускают параллельно циклы; бенчмарк измеряет общую пропускную способность.
Как работает runtime/debug и debug/elf?
Краткий ответ
runtime/debug предоставляет доступ к информации о сборщике мусора (FreeOSMemory, SetGCPercent), стек-трейсам (PrintStack), управлению максимальными горутинами (SetMaxThreads), и сбору отладочной информации (build info через ReadBuildInfo). debug/elf реализует доступ к ELF-файлам (исполняемые файлы Linux) — чтение заголовков, секций, символов.
Развёрнутый ответ
// runtime/debug
debug.SetGCPercent(100)
debug.FreeOSMemory()
buildInfo, _ := debug.ReadBuildInfo()
fmt.Println(buildInfo.Main.Version)
// debug/elf
f, _ := elf.Open("binary")
defer f.Close()
fmt.Println(f.Machine)
debug/plan9obj, debug/macho, debug/pe — аналоги для других форматов.
Под капотом (internals)
debug.ReadBuildInfoчитает встроенный модульruntime.buildVersionи данные модулей из секции.go.buildinfoили таблицы символов.debug.Stackвозвращает стек-трейс всех горутин, похоже на профилировщик.debug/elfпарсит бинарный ELF напрямую, используя структуры изdebug/elf/elf.go. Поддерживает DWARF для отладки.
Типичные ошибки кандидатов
- Использовать
debug.SetMaxThreadsбез понимания последствий. Может вызвать панику при превышении лимита потоков. - Забыть закрыть
elf.File— утечка дескриптора. - Парсить ELF-файлы в горячем пути; это ресурсоёмко.
Возможные дополнительные вопросы интервьюера
Как получить версию Go, которой скомпилирован бинарник?
Через debug.ReadBuildInfo().GoVersion. Или strings.Contains(runtime.Version(), "go1.x").
Можно ли уменьшить потребление памяти через FreeOSMemory?
Да, принудительно возвращает операционной системе неиспользуемую память, но это может увеличить работу GC.
Как прочитать символы из ELF?
elf.File.Symbols() или динамические символы ImportedSymbols.
Что такое container/list и container/heap?
Краткий ответ
container/list — двусвязный список, реализованный через кольцевую структуру Element. container/heap — интерфейс для кучи (приоритетной очереди), который работает с любым срезом, реализующим heap.Interface. Обе структуры являются примитивами, не потокобезопасны.
Развёрнутый ответ
// list
l := list.New()
l.PushBack("a")
for e := l.Front(); e != nil; e = e.Next() {
fmt.Println(e.Value)
}
// heap
h := &IntHeap{2, 1, 5}
heap.Init(h)
heap.Push(h, 3)
min := heap.Pop(h)
Под капотом (internals)
listхранитrootэлемент как sentinel.PushBackсоздаётElement, связывает его, O(1).heapиспользует функцииupиdownдля восстановления инвариантов. Минимальный элемент всегдаh[0]. Сложность O(log n). Сортировка черезheap.Sortдаёт пирамидальную сортировку.
Типичные ошибки кандидатов
- Не инициализировать кучу перед использованием:
h := &IntHeap{}; heap.Push(h, 1)безheap.Init. Правильно: сначалаheap.Init. - Хранить в куче мутируемые элементы без перестроения. Изменение ключа приведёт к нарушению инварианта; нужно вызывать
heap.Fix. - Использовать
container/listвместо слайса, когда нужен частый доступ по индексу. У списка O(n) доступ по индексу.
Возможные дополнительные вопросы интервьюера
Как реализовать max-кучу с помощью container/heap?
Инвертировать функцию Less: return h[i] > h[j].
Когда оправдано использовать двусвязный список?
При частых вставках/удалениях в середине при больших объёмах данных и при отсутствии необходимости в индексации.
Как container/heap гарантирует O(log n)?
Через просеивание вверх/вниз, используя свойства полного бинарного дерева, представленного в срезе.
Как работает ring из container/ring?
Краткий ответ
container/ring — циклический двусвязный список фиксированного размера. Создаётся ring.New(n), элементы хранят Value (interface{}). Поддерживает перемещение (Next, Prev), связывание/развязывание (Link, Unlink), обход через Do. Полезен для кольцевых буферов, каруселей.
Развёрнутый ответ
r := ring.New(5)
for i := 0; i < r.Len(); i++ {
r.Value = i
r = r.Next()
}
r.Do(func(x interface{}) {
fmt.Println(x)
})
Под капотом (internals)
Ring— структура сnext,prevуказателями иValue. Циклический: next последнего указывает на первый, prev первого на последний.- Операции:
Linkсоединяет два кольца за O(1),Unlinkудаляет n элементов. Doитерируется, не вызывая рекурсии.
Типичные ошибки кандидатов
- Создать
ring.New(0)— допустимо, но nil; вызов методов паникует. Проверятьr != nil. - Забыть, что кольцо циклическое, и
Do/ итерация поNextмогут зациклиться, если условие выхода неr.Len(). - Использовать
ringкак заменуsliceдля случайного доступа — нет индексации.
Возможные дополнительные вопросы интервьюера
Можно ли использовать ring для кольцевого буфера?
Да, но фиксированный размер; для динамического нужен слайс с указателями.
Как клонировать кольцо?
ring.New(n); for ... r.Do(func(x){...}). Прямого метода нет.
Чем ring отличается от list?
У ring фиксированный размер (нет динамического добавления/удаления, хотя Link меняет размер). list динамический, без кольцевой связности по умолчанию.
Как работает math/big и когда его использовать?
Краткий ответ
math/big предоставляет целые числа произвольной точности (Int), рациональные (Rat) и числа с плавающей запятой произвольной точности (Float). Используется в криптографии, финансовых расчётах, где встроенные int64/float64 недостаточны по диапазону или точности. Реализован на чистых Go-алгоритмах (умножение Карацубы, деление с остатком, и т.д.).
Развёрнутый ответ
a := new(big.Int)
a.SetString("123456789012345678901234567890", 10)
b := new(big.Int)
b.SetString("987654321098765432109876543210", 10)
sum := new(big.Int).Add(a, b)
Float поддерживает режимы округления, точность в битах. Rat для точных дробей.
Под капотом (internals)
Intхранитnat— срез слов ([]Word, гдеWord=uint). Операции реализованы ассемблерными вставками (addVW,subVW) для производительности.- Умножение использует Карацубу для больших чисел, возведение в степень — бинарный метод.
- GC-friendly:
Intсодержит указатель наnat; приSet/Addвозвращает новый объект, но может писать в существующий для избежания аллокаций. Float— знак, мантиса (nat), экспонента, точность. Округление согласно IEEE 754.
Типичные ошибки кандидатов
- Использовать
big.Intдля маленьких чисел, создавая излишние аллокации. Для 64-битного диапазона хватаетint64. - Забыть про иммутабельность:
a.Add(b, c)не изменяетb,c, но может записать вa, что ок. Однакоa.SetStringперезаписываетa. - Создавать
big.Ratчерезnew(big.Rat).SetFracс нулевым знаменателем — паника. - Не проверять успешность
SetString— если ошибка, значение не определено.
Возможные дополнительные вопросы интервьюера
Как быстро возвести в степень по модулю?
big.Int.Exp(x, y, m) использует бинарное возведение с модулем.
Как конвертировать big.Int в []byte?
i.Bytes() или i.FillBytes(buf).
Почему math/big не использует GMP?
Чтобы избежать cgo и лицензионных ограничений, обеспечить портабельность.
Что такое net/url и как правильно парсить URL?
Краткий ответ
Пакет net/url разбирает URL-адреса на компоненты (Scheme, Host, Path, Query и т.д.) через url.Parse. Предоставляет функции экранирования (QueryEscape, PathEscape). Ключевые типы: url.URL и url.Values (для query-параметров). Безопасное конструирование: избегать ручной сборки строки, использовать методы url.URL и url.Values.
Развёрнутый ответ
u, err := url.Parse("https://example.com/path?q=hello%20world#frag")
fmt.Println(u.Scheme, u.Host, u.Path, u.Query().Get("q"), u.Fragment)
// Сборка
v := url.Values{}
v.Set("key", "value")
u := &url.URL{
Scheme: "https",
Host: "example.com",
Path: "/path",
RawQuery: v.Encode(),
}
fmt.Println(u.String())
Экранирование: url.PathEscape для сегментов пути, url.QueryEscape для ключей и значений.
Под капотом (internals)
Parseиспользует внутренний сканер, разбивая строку по символам:,//,?,#. Декодирует percent-encoded последовательности.String()собирает URL обратно, применяя соответствующее экранирование.url.Valuesбазируется наmap[string][]string.Encodeсортирует ключи для стабильности.
Типичные ошибки кандидатов
- Строить URL конкатенацией строк без экранирования, что приводит к некорректным URL или уязвимостям.
- Неправильно экранировать параметры: использовать
PathEscapeдля query, что не экранирует&. Нужно применятьQueryEscape. - Парсить URL без проверки ошибок:
url.Parseможет вернуть nil URL и ошибку, при непроверке паника. - Забыть вызвать
u.Query()для доступа к параметрам, а парситьRawQueryвручную. Используйтеu.Query().Get.
Возможные дополнительные вопросы интервьюера
Как безопасно добавить путь к базовому URL?
u.ResolveReference(ref) или через u.JoinPath. Вручную не рекомендуется.
Чем отличается url.Parse от url.ParseRequestURI?
Второй для HTTP-запросов, не принимает фрагменты и предполагает отсутствие схемы/хоста.
Как обработать повторяющиеся параметры?
v := u.Query(); v["key"] даёт слайс всех значений.
Как работает sync/atomic.Value?
Краткий ответ
atomic.Value — конкурентная переменная, позволяющая атомарно загружать и сохранять значения произвольного типа без блокировок. Гарантирует, что Load после Store увидит сохранённое значение. Может хранить только значения одного конкретного типа в течение жизни.
Развёрнутый ответ
var config atomic.Value
config.Store(Config{Port: 8080})
// позже
if val, ok := config.Load().(Config); ok {
fmt.Println(val.Port)
}
Тип фиксируется первым Store. Нарушение типа вызывает панику. Используется для кеша конфигурации без мьютексов.
Под капотом (internals)
Valueсодержит указатель наifaceWords(два слова: тип и значение). ПриStoreсравнивается тип, если отличается — паника.- Реализация использует атомарные операции над указателем (на некоторых платформах — atomic.LoadPointer и atomic.StorePointer) и барьеры памяти для гарантии видимости.
- До Go 1.17 использовался
unsafe.Pointerнапрямую; сейчас используется type-based механизм.
Типичные ошибки кандидатов
- Сохранять nil через
Store(nil). value не поддерживает nil для первого store; нужно обернуть в указатель. - Первый Store делать из горутин с разными типами. Тип фиксируется первым успешным сохранением; последующие с другим типом паникуют.
- Использовать
atomic.Valueдля обновления «in-place» большого объекта. Обновление всего значения — копирование всей структуры; для частичного обновления лучшеsync.Mutexилиatomic.Valueс указателем на структуру. - Думать, что
atomic.Valueобеспечивает синхронизацию сложных инвариантов нескольких полей. Он атомарно только обновляет ссылку; внутренние данные должны быть неизменяемы после публикации.
Возможные дополнительные вопросы интервьюера
Что быстрее: atomic.Value или мьютекс для частых чтений?
atomic.Value даёт быстрые lock-free чтения, поэтому лучше для read-heavy нагрузки.
Как хранить nil?
Использовать указатель на значение, сохранять (*Config)(nil), при Load проверять указатель.
Можно ли использовать atomic.Value с интерфейсами?
Да, тип фиксируется первым сохранённым типом.
Как работает runtime/metrics?
Краткий ответ
Пакет runtime/metrics (Go 1.16+) предоставляет стабильный API для чтения ключевых метрик Go-рантайма: использование памяти, горутины, GC, размер кеша и т.д. В отличие от runtime.ReadMemStats, он не вызывает STW и работает эффективно. Метрики описаны структурами Description, Value, предоставляются по ключам.
Развёрнутый ответ
sample := []metrics.Sample{
{Name: "/gc/heap/allocs-by-size:bytes"},
{Name: "/sched/goroutines:goroutines"},
}
metrics.Read(sample)
fmt.Println(sample[0].Value)
Метрики доступны по URI-путям, например /memory/classes/heap/free:bytes. Всегда возвращают актуальные значения без существенного влияния на производительность.
Под капотом (internals)
metricsсобирает данные из внутренней структурыruntime.memstatsи других счётчиков без остановки мира, используя atomic чтения либо кеши.metrics.Readвызываетruntime.readMetrics, которая заполняет буфер из соответствующих полей.- Метрики делятся на кумулятивные (счётчики) и мгновенные (gauge). Определены в
runtime/metrics/description.go.
Типичные ошибки кандидатов
- Парсить имена метрик жёстко закодированными строками, не проверяя их существование. Имена стабильны, но всё же лучше использовать константы
metrics.Kindиmetrics.Descriptionдля динамического подхода. - Вызывать
metrics.Readслишком часто. Хотя вызов дешёвый, стоит избегать тысяч раз в секунду; подходит для периодического сбора. - Думать, что это замена профилировщику pprof. metrics даёт текущие значения, а не профиль времени/аллокаций.
Возможные дополнительные вопросы интервьюера
Как получить количество горутин?
metrics.Read с ключом /sched/goroutines:goroutines. Альтернативно runtime.NumGoroutine.
Чем runtime/metrics лучше runtime.ReadMemStats?
ReadMemStats вызывает stop-the-world для гарантии консистентности; metrics избегает этого.
Можно ли экспортировать эти метрики в Prometheus?
Да, парсить metrics.All() и передавать в свою экспозицию.
Что такое mime/multipart и как обрабатывать загрузку файлов?
Краткий ответ
Пакет mime/multipart реализует MIME multipart streams, используется для загрузки файлов через HTTP-формы (Content-Type: multipart/form-data). multipart.Reader парсит поток частей, каждая часть содержит заголовки и тело. На сервере r.ParseMultipartForm собирает их в память или на диск.
Развёрнутый ответ
Серверная сторона:
r.ParseMultipartForm(10 << 20) // 10 MB max memory
file, header, err := r.FormFile("file")
defer file.Close()
// file — multipart.File, header содержит имя и размер
Низкоуровневый парсинг:
mr := multipart.NewReader(r.Body, boundary)
for {
part, err := mr.NextPart()
if err == io.EOF { break }
// чтение part
}
multipart.Writer для создания multipart-запросов.
Под капотом (internals)
ParseMultipartFormпарситContent-Type, извлекаетboundary, затем используетmultipart.Readerдля чтения.- Части, превышающие лимит памяти, сохраняются во временные файлы через
os.CreateTemp; при удалении формы нужно удалить временные файлы (автоматически приRemoveAllпосле запроса). - Буферизованное чтение с поиском boundary.
Типичные ошибки кандидатов
- Не закрывать
multipart.Fileи не удалять временные файлы. Утечка дескрипторов и места на диске. - Забыть вызвать
ParseMultipartFormпередFormFile. Без этогоFormFileвернёт ошибку. - Устанавливать слишком маленький
maxMemory, тогда даже маленькие файлы пишутся на диск, замедляя обработку. Настраивать соответственно ожидаемому размеру. - Не проверять размер файла, что ведёт к exhaustion атакам. Ограничивать размер через
http.MaxBytesReaderперед парсингом.
Возможные дополнительные вопросы интервьюера
Как протестировать загрузку файла?
Создать multipart.Writer, записать части в bytes.Buffer, установить Content-Type с boundary, и отправить запрос через httptest.NewRequest.
Как отправить файл клиентом?
Использовать mime/multipart.Writer с http.Post, либо более удобные net/http.NewRequest с multipart.Writer и FormFile.
Что такое multipart.Reader.NextRawPart?
Возвращает часть без буферизации, позволяя более эффективно обрабатывать большие тела.
Как работает net/rpc и net/rpc/jsonrpc?
Краткий ответ
net/rpc реализует Remote Procedure Call (RPC) через Go-свой протокол (gob). Экспортированные методы объекта могут быть вызваны удалённо. net/rpc/jsonrpc использует JSON как кодирование, совместимое с другими языками. Оба устарели в пользу gRPC.
Развёрнутый ответ
type Args struct { A, B int }
type Arith int
func (t *Arith) Multiply(args *Args, reply *int) error {
*reply = args.A * args.B
return nil
}
// Сервер
arith := new(Arith)
rpc.Register(arith)
rpc.HandleHTTP()
http.ListenAndServe(":1234", nil)
// Клиент
client, _ := rpc.DialHTTP("tcp", "localhost:1234")
var result int
client.Call("Arith.Multiply", Args{3, 4}, &result)
Под капотом (internals)
rpc.Registerсохраняет тип и методы в карту. Клиентский вызов отправляет имя метода и аргументы через gob-кодер/декодер.- Транспорт может быть любым
io.ReadWriteCloser. jsonrpcреализуетrpc.ClientCodecиrpc.ServerCodecчерез JSON.
Типичные ошибки кандидатов
- Использовать неэкспортированные методы — не будут доступны.
- Ошибка передачи не того типа (reply must be pointer type). Паника или ошибка.
- Думать, что
net/rpcобеспечивает безопасность. Нет аутентификации/шифрования; в продакшене нужен gRPC или оборачивать в TLS.
Возможные дополнительные вопросы интервьюера
Почему net/rpc считается устаревшим?
Ограничен только Go-клиентами, нет стриминга, слабая расширяемость. gRPC имеет межъязыковую поддержку и потоковые запросы.
Как реализовать асинхронный вызов?
client.Go(serviceMethod, args, reply, done).
Можно ли использовать JSON-RPC с HTTP?
Да, через jsonrpc.DialHTTP.
Как работает archive/zip и archive/tar?
Краткий ответ
Пакеты предоставляют чтение и запись ZIP- и TAR-архивов. archive/zip работает с zip.Reader/Writer, сжатием (deflate, store), шифрованием (AES, дефолтное ZipCrypto небезопасно). archive/tar — с tar.Reader/Writer, поддерживает различные форматы записей (USTAR, PAX, GNU). Оба работают поверх io.Reader/io.Writer.
Развёрнутый ответ
Чтение ZIP:
r, _ := zip.OpenReader("archive.zip")
defer r.Close()
for _, f := range r.File {
rc, _ := f.Open()
// читать rc
rc.Close()
}
Создание TAR:
tw := tar.NewWriter(w)
hdr := &tar.Header{Name: "file.txt", Size: int64(len(data)), Mode: 0644}
tw.WriteHeader(hdr)
tw.Write(data)
tw.Close()
Под капотом (internals)
- ZIP:
FileHeaderсодержит метаданные, метод сжатия, смещение данных.Writerиспользует буферизацию и потоковое сжатие (flate). - TAR: записи по 512 байт, заголовки заполняются структурами. Пакет автоматически определяет формат (USTAR, PAX для больших файлов).
Типичные ошибки кандидатов
- Не закрывать
zip.ReadCloser/Writer, теряя данные. - Игнорировать ошибки при вызове
tw.WriteHeaderиtw.Write, что приводит к повреждению архива. - Использовать стандартное ZipCrypto шифрование для конфиденциальных данных (небезопасно). Лучше шифровать отдельно.
- Не проверять path traversal уязвимости при распаковке: имена файлов в архиве могут содержать
../. Использоватьfilepath.SecureJoinилиfilepath.IsLocal.
Возможные дополнительные вопросы интервьюера
Как создать ZIP с сжатием Deflate?
zip.NewWriter(w) по умолчанию deflate. Можно указать метод через f.CreateHeader.
Как обработать распаковку TAR-архива с длинными именами?
tar.Reader сам декодирует PAX расширения.
Почему archive/tar не поддерживает сжатие?
Разделение ответственности: tar — формат архивации, сжатие накладывается отдельно (gzip, bzip2).
Как работает compress/gzip?
Краткий ответ
compress/gzip реализует чтение и запись gzip-формата (RFC 1952). gzip.Writer оборачивает io.Writer, сжимая данные; gzip.Reader оборачивает io.Reader, распаковывая. Поддерживает уровни сжатия gzip.BestCompression, gzip.BestSpeed. Внутри использует библиотеку compress/flate (DEFLATE).
Развёрнутый ответ
// Сжатие
var buf bytes.Buffer
w := gzip.NewWriter(&buf)
w.Write([]byte("data"))
w.Close() // обязательно закрыть, чтобы записать финальные блоки
// Распаковка
r, _ := gzip.NewReader(&buf)
defer r.Close()
data, _ := io.ReadAll(r)
gzip.Writer позволяет задать уровень и комментарий. При закрытии записывает контрольную сумму CRC32.
Под капотом (internals)
compress/flateреализует DEFLATE (LZ77 + Хаффман). Пакетgzipдобавляет заголовок и трейлер.Writerинициализируетflate.Writerс выбранным уровнем.Readerоборачиваетflate.Reader, также проверяет gzip-заголовок (магические байты, метод).- Потоковый: можно читать/писать гигабайтные данные без загрузки всего в память.
Типичные ошибки кандидатов
- Не закрыть
gzip.Writer, из-за чего данные теряются, т.к. финальные блоки не записаны. Использоватьdefer w.Close()с проверкой ошибки. - Забыть закрыть
gzip.Reader, оставляя висящий внутренний буфер. - Не обрабатывать ошибку
Close— она может вернуть ошибку записи в нижележащий writer. - Полагать, что
gzip— единственный способ сжатия для HTTP. Для веба лучше использоватьContent-Encoding: gzipс автоматической обработкой через middleware.
Возможные дополнительные вопросы интервьюера
Как gzip работает с многопоточным сжатием?
Стандартный пакет не поддерживает; есть сторонние обёртки (pgzip) с параллельным flate.
Как сжать данные в памяти?
var buf bytes.Buffer; w := gzip.NewWriter(&buf); ...; w.Close(); compressed := buf.Bytes().
Чем compress/gzip отличается от compress/zlib?
Формат контейнера: gzip имеет более богатые заголовки (имя файла, комментарий), zlib — облегчённый формат (RFC 1950). Оба используют DEFLATE.
Что такое encoding/json/v2 в Go 1.25?
Краткий ответ (30–60 секунд)
Пакет encoding/json/v2 (Go 1.25+) — это долгожданная фундаментальная переработка стандартного JSON-сериализатора, устранившая критические недостатки v1:
- Производительность и потоковая обработка: модуль
jsontextреализует потоковый zero-allocation токенизатор (SAX-модель), в 2–4 раза быстрееv1. - Строгая обработка дубликатов: наличие дублирующихся ключей в объекте теперь по умолчанию вызывает ошибку (в
v1последнее значение молча затирало предыдущие, что приводило к уязвимостям безопасности). - Честный
omitzero: новый тегomitzeroпропускает любые нулевые значения (включая пустые структурыtime.Time{}и кастомные типы с методомIsZero() bool), устраняя кривую семантику старогоomitempty.
Развёрнутый ответ
Сравнительная таблица семантики v1 и v2:
| Возможность | encoding/json (v1) | encoding/json/v2 (Go 1.25+) |
|---|---|---|
| Дубликаты ключей | Тихо перезаписывает (риск атак) | Возвращает ошибку DuplicateNameError |
| Пропуск нулевых структур | omitempty не скрывает time.Time{} | omitzero честно вызывает IsZero() |
| Кастомные маршаллеры | MarshalJSON() ([]byte, error) | MarshalEncode(*jsontext.Encoder) error |
| Форматирование типов | Фиксированное глобальное поведение | Тонкие опции через json.WithMarshalers() |
Пример кода на v2:
import "encoding/json/v2"
type Order struct {
ID string `json:"id"`
CreatedAt time.Time `json:"created_at,omitzero"` // Скрывается, если IsZero() == true!
Comment string `json:"comment,omitzero"`
}
func encodeOrder(o Order) ([]byte, error) {
// В 2-3 раза быстрее и без лишних аллокаций
return json.Marshal(o, json.Deterministic(true))
}
Под капотом (internals)
В v2 компилятор и рантайм кэшируют таблицы дескрипторов полей без многократного обращения к медленной рефлексии reflect.TypeOf. Маршаллинг происходит напрямую в буфер потока через метод MarshalEncode, минуя создание промежуточных срезов байт.
Типичные ошибки кандидатов
- Ожидание 100% совместимости с v1: в
v2намеренно изменены дефолты (строгость к дубликатам, валидация UTF-8). - Использование
omitemptyвместоomitzero: в коде на v2 рекомендуется использовать именноomitzero.
Что такое пакет uuid в Go 1.27?
Краткий ответ (30–60 секунд)
Начиная с Go 1.27, в стандартную библиотеку добавлен официальный пакет uuid, положив конец многолетней фрагментации сторонних зависимостей (google/uuid, gofrs/uuid). Пакет нативно поддерживает спецификацию RFC 9562 (включая UUIDv4 — криптографически стойкий случайный идентификатор, и UUIDv7 — монотонно возрастающий во времени идентификатор, оптимизированный для первичных ключей в базах данных и B-tree индексов).
Развёрнутый ответ
import (
"fmt"
"uuid"
)
func ExampleUUID() {
// 1. UUIDv7 (Timestamp-ordered: миллисекунды UNIX + случайные биты)
// Идеально подходит для primary key в PostgreSQL / MySQL / CockroachDB!
idV7, err := uuid.NewV7()
if err != nil {
panic(err)
}
fmt.Println("UUIDv7:", idV7.String())
// 2. UUIDv4 (Классический случайный)
idV4, _ := uuid.NewV4()
// 3. Парсинг и сравнение за O(1) через 16-байтовый массив [16]byte
parsed, _ := uuid.Parse("018d3b2f-8a0a-7000-8000-000000000000")
if idV7 == parsed {
fmt.Println("IDs match!")
}
}
Под капотом (internals)
- Тип
uuid.UUIDопределен как компактный фиксированный массив[16]byte. Он не аллоцирует память в куче при создании и передаче. - Поддерживает оператор
==напрямую в регистрах процессора (через SSE/AVX инструкции на amd64 и NEON на arm64). uuid.NewV7()упаковывает 48-битный timestamp Unix epoch в старшие байты, что гарантирует монотонный рост значений. При вставке записей в B-tree индекс базы данных новые узлы всегда добавляются в конец дерева, предотвращая дорогостоящие page splits и фрагментацию страниц диска.
Типичные ошибки кандидатов
- Продолжение генерации UUIDv4 для первичных ключей в БД: UUIDv4 полностью случаен, что разрушает кластеризованные индексы в БД при больших объемах (Random I/O). Кандидат должен уверенно предлагать UUIDv7.
- Ненужные аллокации строк: хранение UUID в структурах в виде
stringвместоuuid.UUID([16]byte) увеличивает расход памяти в 3 раза.