Содержание урока · 13 разделов
- gin логирование: кто куда пишет и как это перенастроить
- Два writer-а на весь пакет: DefaultWriter и DefaultErrorWriter
- Цвета под контролем: isatty, isTerm и consoleColorMode
- LoggerWithFormatter и тип LogFormatter: своя строка лога
- c.Errors: копилка ошибок запроса
- ErrorMessage: приватные ошибки в строке лога
- Куда что пишет gin: сводная таблица
- CustomRecovery и RecoveryWithWriter: паника под контролем
- slog как writer-адаптер: практика
- Шесть правил вывода gin
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
gin логирование: кто куда пишет и как это перенастроить#
В уроке про встроенные middleware мы разобрали анатомию строки [GIN] и поведение Recovery при панике; в прошлом уроке — фабрику-замыкание, по которой эти звенья написаны. Тема этого урока — управление gin логированием: три письменных стола, на которые фреймворк кладёт свои записи, и все три можно перенести. Первый — gin.DefaultWriter: туда пишут и строка лога Logger, и отладочные [GIN-debug] строки движка. Второй — gin.DefaultErrorWriter: туда уходит трассировка паники от Recovery. Третий — stderr самого net/http, куда без Recovery падает сообщение http: panic serving — его мы трогать не будем, просто помним, что он существует. Вокруг этих столов соберём четыре сюжета: подмена вывода и её главный подводный камень — момент захвата writer-а; свои форматтеры через LoggerWithFormatter; копилка ошибок c.Errors и поле ErrorMessage, которое связывает хендлер с логом; и, наконец, CustomRecovery — паника, которая превращается не в пустой 500, а в JSON-конверт для клиента и структурированную запись для вашего логгера.
Два writer-а на весь пакет: DefaultWriter и DefaultErrorWriter#
Откройте mode.go в исходниках gin — там всего две строчки, которые объясняют всё gin логирование целиком. var DefaultWriter io.Writer = os.Stdout — так объявлен первый стол; док-комментарий в исходнике честно перечисляет, кто им пользуется: «debug output and middleware output like Logger() or Recovery()», то есть и отладка движка, и строки логов. var DefaultErrorWriter io.Writer = os.Stderr — второй стол, «для отладочных ошибок»: именно туда Recovery пишет [Recovery] panic recovered со стеком, а движок — [GIN-debug] [ERROR] строки. Обе переменные пакетные, то есть присваиваются из вашего кода: gin.DefaultWriter = файл — и весь вывод уезжает с консоли в журнал. Отсюда три практических вывода. Первый: подменять выгодно до создания движка, потому что звенья захватывают writer в момент регистрации — LoggerWithConfig делает out := conf.Output, а при nil подставляет текущее значение DefaultWriter ровно один раз, и замыкание дальше живёт с этим значением; Recovery() устроен так же — он равен RecoveryWithWriter(DefaultErrorWriter). Второй: DefaultWriter обслуживает не только [GIN]-строки, но и [GIN-debug] — сообщения о маршрутах печатаются при регистрации; SetMode(gin.ReleaseMode) или переменная окружения GIN_MODE=release глушат их разом. Третий: подмена глобальна на весь процесс — в параллельных тестах за один writer начнут драться, поэтому там чаще подставляют локальный буфер в конкретный LoggerWithConfig, а не пакетную переменную.
Подмена writer-ов: файл вместо консоли
GOfunc main() {
// Режим до всего: Release глушит [GIN-debug] строки движка.
gin.SetMode(gin.ReleaseMode)
// Открываем файл ДО gin.Default(): звенья запомнят writer
// в момент регистрации, поздние подмены на них не подействуют.
f, err := os.OpenFile("gin.log",
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
panic(err)
}
gin.DefaultWriter = f // [GIN]-строки Logger и [GIN-debug]
gin.DefaultErrorWriter = f // [Recovery]-трассировки при паниках
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.String(http.StatusOK, "pong")
})
r.Run(":8080")
}Порядок строк здесь и есть главный сюжет. SetMode перед созданием движка — иначе [GIN-debug] предупреждения успеют уйти в старый stdout: debugPrint читает DefaultWriter в момент вызова, а не хранения. Присваивание writer-ов до gin.Default() — иначе Logger и Recovery внутри Default захватят os.Stdout и os.Stderr, и подмена останется пустым жестом. В Release-режиме бонус: Recovery напечатает трассировку короче — без дампа запроса, который он добавляет только в debug. Для тестов вместо файла берите bytes.Buffer: buf := &bytes.Buffer{}; gin.DefaultWriter = buf — и в конце проверяете buf.String().
Проверка: момент захвата writer-а
В тесте сделали gin.DefaultWriter = buf, но ПОСЛЕ r := gin.Default(). Куда уйдут строки [GIN]?
Цвета под контролем: isatty, isTerm и consoleColorMode#
В уроке 06 мы сказали: в файле цвета исчезают, потому что isatty. Теперь заглянем глубже — тем более что от этого зависит, как будут выглядеть ваши подменённые логи. Пакетная переменная consoleColorMode хранит один из трёх режимов: autoColor по умолчанию, disableColor после gin.DisableConsoleColor() и forceColor после gin.ForceConsoleColor(). Решение о цвете принимает метод IsOutputColor(): force включён — красим всегда; иначе нужно auto И флаг isTerm. Сам isTerm вычисляется в LoggerWithConfig ровно один раз, при регистрации, цепочкой из трёх условий: если вывод не *os.File — цветы нет (буфер и пайп автоматически остаются плоским текстом); если TERM=dumb — тоже; если isatty.IsTerminal и IsCygwinTerminal оба сказали «не терминал» — снова нет. Отсюда приятное следствие для тестов: буфер не требует никакого отключения цветов руками, ANSI-коды в него не попадут. И ещё одна деталь, мимо которой мы прошли в прошлый раз: у латентности есть собственная палитра LatencyColor — до 100 мс белый, до 200 зелёный, до 300 бирюзовый, до 500 синий, до секунды жёлтый, до двух пурпурный, дольше — красный. Медленные запросы видно краем глаза ещё до чтения цифр.
LoggerWithFormatter и тип LogFormatter: своя строка лога#
Свой формат логов в gin подключается одной строкой, но под ней стоит интересная конструкция. В logger.go объявлен тип LogFormatter — и здесь вас ждёт маленькое разочарование, честно вынесенное из исходников: это не интерфейс, а именованный тип-функция, type LogFormatter func(params LogFormatterParams) string. LoggerWithFormatter(f) — тонкая обёртка: она возвращает LoggerWithConfig(LoggerConfig{Formatter: f}), вся остальная механика общая. Если форматтер не задан, конфиг берёт defaultLogFormatter — непакетированную переменную с шаблоном [GIN] %v |%s %3d %s|…, который мы читали в уроке 06; его не заменить точечно, но можно написать свой за десять минут. Контракта два. Первый: форматтер возвращает ГОТОВУЮ строку — fmt.Fprint не дописывает перевод строки, поэтому в конце ставите вы (а его отсутствие склеит все записи в одну строку). Второй: вернуть пустую строку — легально: writer ничего не получит, и это не баг, а дверь для структурных логгеров, о которых дальше. Наконец, LogFormatterParams несёт не только поля из урока 06, но и Keys — слепок c.Keys: всё, что звенья положили в контекст через c.Set, доступно форматтеру прямо в строке лога.
Форматтер с ключами контекста и ошибками
GO// requestID из прошлого урока пишет метку в контекст: c.Set("requestID", id).
// LogFormatterParams приносит Keys целиком — метка доезжает до лога.
r.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
// ErrorMessage приходит готовой строкой: "Error #01: ...\n"
// на каждую ошибку или пустая строка, если ошибок не было.
return fmt.Sprintf("%s %s -> %d за %v req=%v\n%s",
param.Method, param.Path,
param.StatusCode, param.Latency,
param.Keys["requestID"], param.ErrorMessage)
}))Форматтер вызывается после c.Next() — все ошибки хендлера к этому моменту уже в копилке, а статус уже финальный, включая Abort-ответы. Ключи контекста приходят картой any: обращение param.Keys["requestID"] вернёт nil-интерфейс на запросах без метки — для строгих типов заводите хелпер чтения, как мы делали в прошлом уроке. Обратите внимание: ErrorMessage в нашем шаблоне стоит после \n — каждая ошибка уедет на отдельную строку под строкой запроса, потому что String() копилки завершает каждую запись переводом строки.
Какая ошибка попадёт в строку лога
Прочитайте код и выберите, что он выведет:
r := gin.Default() // Logger из Default, форматтер дефолтный
r.GET("/flaky", func(c *gin.Context) {
c.Error(errors.New("кэш недоступен")).
SetType(gin.ErrorTypePublic)
c.Error(errors.New("timeout после 2s"))
c.JSON(http.StatusServiceUnavailable,
gin.H{"error": "unavailable"})
})
// curl localhost:8080/flaky
// Что напечатает Logger в поле ErrorMessage?c.Errors: копилка ошибок запроса#
Раз мы заговорили про публичное и приватное — время разобрать саму копилку. В errors.go определён type Error struct { Err error; Type ErrorType; Meta any }: обёртка над любой ошибкой с битовой меткой и опциональными метаданными. Типы — битовая маска uint64: ErrorTypePrivate (1<<0) и ErrorTypePublic (1<<1) — наше приватное/публичное разделение; ErrorTypeBind (1<<63) и ErrorTypeRender (1<<62) — служебные; ErrorTypeAny — все биты сразу, «фильтр не работает». Метод c.Error(err) — вход в копилку: он паникует на nil («err is nil» — проверено по исходникам), при необходимости оборачивает ошибку в Error с типом ErrorTypePrivate, дописывает в срез c.Errors и — важно — возвращает *Error. Возврат сделан под чейнинг: SetType и SetMeta возвращают ту же запись, поэтому c.Error(err).SetType(...).SetMeta(...) — одна строка. Над срезом живёт тип errorMsgs с помощниками: ByType возвращает отфильтрованную копию, Last — последнюю ошибку, Errors — срез строк, String — ту самую нумерованную распечатку Error #01: …, по одной строке на запись, с Meta отдельной строкой, если она есть. Договорённость о приватности простая: приватные ошибки могут содержать внутренности — адреса, SQL, секреты, — их место в логе; публичные читает клиент.
Копилка в действии: публичное клиенту, приватное в лог
GOr.GET("/flaky", func(c *gin.Context) {
// c.Error только копит ошибку — ответ клиенту она НЕ пишет!
c.Error(errors.New("кэш недоступен")).
SetType(gin.ErrorTypePublic) // читать сможет клиент
c.Error(errors.New("timeout после 2s")) // приватная: в лог
// Публичный срез строк — в тело ответа.
c.JSON(http.StatusServiceUnavailable, gin.H{
"errors": c.Errors.ByType(gin.ErrorTypePublic).Errors(),
})
})
// Звено-помощник из logger.go: после цепочки отдаёт все ошибки
// ответом. r.Use(gin.ErrorLogger()) — удобно в тестах и отладке.Клиент получит 503 и {"errors":["кэш недоступен"]}, Logger в ErrorMessage напечатает только приватный timeout. c.Errors.String() при этом содержит обе записи. ErrorLogger внутри — это ErrorLoggerT(ErrorTypeAny): после c.Next() при непустой копилке вызывается c.JSON(-1, errors); спец-статус -1 означает «не менять уже записанный» — responseWriter игнорирует коды не больше нуля. Отдельная тонкость, о которой редко пишут: SetType ЗАМЕНЯЕТ биты, а не добавляет. Bind-семейство из урока 04 ставит ошибкам тип ErrorTypeBind — ровно его, без бита приватности, — поэтому ошибки binding в ErrorMessage дефолтного лога не видны.
Проверка: звено ErrorLogger
Что делает звено gin.ErrorLogger(), поставленное в цепочку?
ErrorMessage: приватные ошибки в строке лога#
Теперь весь путь ошибки собран. Хендлер вызывает c.Error — запись ложится в копилку приватной; кто-то переопределил тип через SetType — канал сменился. Ниже по цепочке отрабатывает abort-звено: AbortWithError из прошлого урока — это AbortWithStatus плюс c.Error, то есть отказ клиенту и отметка в копилке одной строкой. А затем Logger, стоящий выше, печатает строку лога: ErrorMessage он собирает выражением c.Errors.ByType(ErrorTypePrivate).String() ПОСЛЕ c.Next() — механика «лога после Abort», которую мы проследили на тайминг-звене в прошлом уроке, здесь работает на поле ошибки: даже отклонённый запрос оставит и строку лога, и свои ошибки в ней. Полный набор фактов на выходе: [GIN] строка со статусом 401, а следом — Error #01 с текстом отказа. Практическое правило вытекает само: хотите видеть ошибку в логе — держите её приватной; хотите показать клиенту — публичной; транспорт для публичных — Envelope-конверт из урока про JSON, а не ErrorMessage. И ещё одно следствие: ошибки валидации binding не видны в дефолтном логе — их тип после SetType стал ErrorTypeBind, поэтому для форм бинд-ошибки логируйте сами, руками.
Куда что пишет gin: сводная таблица#
| Источник записи | Writer по умолчанию | Что именно пишет |
|---|---|---|
| gin.Logger и свой форматтер | DefaultWriter (os.Stdout) | строка [GIN] после ответа: статус, латентность, IP, путь, ErrorMessage |
| gin.Recovery и CustomRecovery | DefaultErrorWriter (os.Stderr) | [Recovery] panic recovered: время, значение паники, стек; в debug — ещё дамп запроса |
| отладка движка [GIN-debug] | DefaultWriter (os.Stdout) | маршруты при регистрации, предупреждения New/Default, [ERROR] — только в debug-режиме |
| debugPrintError движка | DefaultErrorWriter (os.Stderr) | [GIN-debug] [ERROR] строки, например ошибка Run |
| net/http без Recovery | stderr сервера | http: panic serving — соединение обрывается без статуса (разбор в уроке 06) |
| подмена на файл / буфер / slog | назначаете вы | присваивайте до gin.Default() и Use — звенья захватывают writer при регистрации |
CustomRecovery и RecoveryWithWriter: паника под контролем#
Второй стол — ошибки — не менее настраиваем, чем первый. Собственно gin recovery — это Recovery(), и в исходниках он однострочный: return RecoveryWithWriter(DefaultErrorWriter). RecoveryWithWriter(out io.Writer, recovery ...RecoveryFunc) — переходник с variadic-хвостом: передали обработчик — он вызовет CustomRecoveryWithWriter(out, handler), не передали — с defaultHandleRecovery, который мы видели в уроке 06: приведение значения паники к error (если не error — через fmt.Errorf("%v", err)), c.Error в копилку, AbortWithStatus(500) с пустым телом. CustomRecovery(handle) — тот же RecoveryWithWriter, но с дефолтным stderr: паника всё равно попадает в лог, а клиент получает то, что решил ваш обработчик. Внутри CustomRecoveryWithWriter создаётся log.New(out, "\n\n\x1b[31m", log.LstdFlags) — два пустых перевода строки и красный ANSI-префикс, чтобы трассировка не слипалась с [GIN]-строками. Два факта, которых в уроке 06 не было. Первый: дамп запроса в трассировке — привилегия debug-режима; в release печатаются только время, значение паники и стек (IsDebugging() решает, какую из двух печатных форм использовать). Второй: стек собирает функция stack — она идёт по кадрам runtime.Caller и для каждого читает ИСХОДНЫЙ ФАЙЛ С ДИСКА, чтобы показать строку кода, где случилась паника; поэтому [Recovery]-трассировка такая человекочитаемая, но и требует, чтобы исходники лежали рядом.
CustomRecovery: JSON-конверт вместо пустого 500
GO// panicToEnvelope — наш RecoveryFunc: лог и честный ответ клиенту.
// Сигнатура задана типом RecoveryFunc: func(c *Context, err any).
func panicToEnvelope(c *gin.Context, err any) {
// err — any: паниковать можно любым значением, не только error.
slog.Error("паника в хендлере", "panic", fmt.Sprintf("%v", err))
// Envelope-конверт из урока про JSON: ошибка со схемой, не стектрейс.
c.AbortWithStatusJSON(http.StatusInternalServerError, Envelope{
Error: &APIError{
Code: "INTERNAL",
Message: "внутренняя ошибка сервера",
},
})
}
func main() {
r := gin.New()
r.Use(gin.Logger())
// CustomRecovery = RecoveryWithWriter(DefaultErrorWriter, panicToEnvelope):
// стек уйдёт в stderr, клиенту — наш JSON.
r.Use(gin.CustomRecovery(panicToEnvelope))
r.GET("/boom", func(c *gin.Context) {
var m map[string]int
m["x"] = 1 // паника: assignment to entry in nil map
})
r.Run(":8080")
}В отличие от дефолтного defaultHandleRecovery, наш обработчик сам решает и лог, и тело ответа: в логе — slog с полем panic, клиенту — 500 с Envelope, где нет ни стектрейса, ни текста паники. Заметьте: запись в nil-карту — ещё одна классическая паника рантайма рядом с делением на ноль из урока 06. Если стек нужен не в stderr, а в файл — берите RecoveryWithWriter(f, panicToEnvelope): writer и обработчик настраиваются независимо.
Проверка: сигнатура обработчика паники
Какую сигнатуру требует gin.CustomRecovery от функции-обработчика?
slog как writer-адаптер: практика#
Теперь практика, ради которой затевались подмены. Стандартная библиотека Go с версии 1.21 даёт структурный логгер slog, и два моста между gin логированием и slog строятся за десять минут. Мост первый — адаптер writer-а: любой io.Writer годится для LoggerWithWriter и RecoveryWithWriter, а значит, достаточно типа с методом Write(p []byte) (int, error), который пересылает кусок в slog.Info. Контракт io.Writer строгий: вернуть длину принятых байт и nil — именно по возвращаемой длине fmt и log решают, что запись состоялась. Нюанс: gin пишет [GIN]-строку целиком одним вызовом fmt.Fprint, а Recovery — через log.Logger, который добавляет дату и свой префикс, поэтому адаптер получает готовые строки с \n на хвосте — TrimSpace перед отдачей slog. Мост второй, более аккуратный для access-лога: LoggerWithFormatter, который ничего не печатает — форматтер забирает поля LogFormatterParams и отдаёт их slog как атрибуты, возвращая пустую строку. Получаете настоящие структурные записи: method, path, status, latency как отдельные поля, по которым ищет любой сборщик логов. zap и zerolog, упомянутые ещё в уроке про первый сервер, подключаются той же дорогой — направление показано, пакет выберете сами.
Два моста: адаптер для Recovery, форматтер для access-лога
GO// slogWriter — адаптер io.Writer поверх структурного логгера.
type slogWriter struct {
logger *slog.Logger
}
// Write пересылает запись gin в slog одной строкой.
func (w slogWriter) Write(p []byte) (int, error) {
w.logger.Info(strings.TrimSpace(string(p)))
return len(p), nil // io.Writer: вернуть длину принятого
}
// slogAccessLog — access-лог gin средствами slog: без [GIN]-строк.
func slogAccessLog(logger *slog.Logger) gin.HandlerFunc {
return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
logger.Info("http",
"method", param.Method,
"path", param.Path,
"status", param.StatusCode,
"latency", param.Latency.String(),
"errors", param.ErrorMessage)
return "" // в writer ничего не уходит
})
}
func main() {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
r := gin.New()
r.Use(slogAccessLog(logger))
r.Use(gin.RecoveryWithWriter(slogWriter{logger})) // стек паники — тоже в slog
r.Run(":8080")
}Тип и функция демонстрируют оба моста. slogAccessLog превращает каждый запрос в структурную запись с полями — поиск по status=500 в журнале становится тривиальным, а строка [GIN] не печатается вовсе. RecoveryWithWriter(slogWriter{logger}) уводит трассировку паники в тот же логгер: запись придёт многострочной — со стеком целиком, потому что log.Logger внутри Recovery склеивает её в один Write. Импорты: log/slog, strings, os, net/http, time — при желании.
Куда уйдёт трассировка паники
Прочитайте код и выберите, что он выведет:
r := gin.Default() // Recovery() уже в цепочке
buf := &bytes.Buffer{}
gin.DefaultErrorWriter = buf
r.GET("/boom", func(c *gin.Context) {
panic("взрыв")
})
// curl localhost:8080/boom
// Куда напечатается [Recovery] panic recovered?Допишите подмену вывода на файл
Соберите каркас: тихий режим, файл в качестве writer-а, корректный ответ хендлера.
Соберите запрос с паникой: от nil-карты до строки лога
Цепочка: Logger, затем CustomRecovery(panicToEnvelope). Хендлер пишет в nil-карту. Разложите события в порядке исполнения.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Шесть правил вывода gin#
- подменяйте DefaultWriter и DefaultErrorWriter до gin.Default() и Use: звенья захватывают writer в момент регистрации
- DefaultWriter обслуживает и [GIN]-логи, и [GIN-debug]: SetMode(gin.ReleaseMode) или GIN_MODE=release глушат отладку
- свой формат — LoggerWithFormatter: функция от LogFormatterParams, \n в конце ставите сами, return "" отдаёт вывод slog
- c.Error только копит ошибки: приватные едут в ErrorMessage, публичные — клиенту; сама по себе c.Error ответа не пишет
- CustomRecovery — когда клиент должен получить JSON при панике; RecoveryFunc получает err any и несёт ответ сама
- RecoveryWithWriter(file) отправляет стек паники в файл; в Release-режиме трассировка короче — без дампа запроса
Карточки: логирование и recovery
Соберите main с подменёнными writer-ами и двумя маршрутами: /ping и /boom с записью в nil-карту. curl -i localhost:8080/ping — и в gin.log появится строка запроса; curl -i localhost:8080/boom — 500 с JSON-конвертом, а в журнале — структурная запись от CustomRecovery и трассировка со стеком. В браузерной песочнице сайта сетевой код не исполняется, поэтому runner-ов в курсе нет — интерактивы здесь quiz, predict, fill, order и флешкарты, а примеры живут в локальном go run, как и весь курс gin. Дальше по курсу — урок 09-validaciya: валидация входных данных с библиотекой validator, теги validate и свои сообщения об ошибках.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Весь вывод gin обслуживают две пакетные переменные: DefaultWriter (os.Stdout — [GIN]-строки и [GIN-debug]) и DefaultErrorWriter (os.Stderr — трассировки Recovery).
- Звенья захватывают writer в момент регистрации: LoggerWithConfig подставляет DefaultWriter один раз, Recovery() равен RecoveryWithWriter(DefaultErrorWriter) — подменяйте до gin.Default().
- Цвета решаются при регистрации: consoleColorMode (auto/disable/force) и одноразовый замер isTerm — буфер и файл остаются плоским текстом без всяких телодвижений.
- LogFormatter — тип-функция func(LogFormatterParams) string: LoggerWithFormatter подменяет формат строки, в Keys приходит слепок c.Keys из звеньев.
- c.Errors — копилка ошибок с типами private/public/bind: c.Error возвращает *Error для чейнинга SetType и SetMeta; ErrorMessage лога — только приватные, и собирается он после c.Next().
- CustomRecovery заменяет пустой 500 на ваш ответ: RecoveryFunc получает err any, стек паники всё равно уходит в writer; RecoveryWithWriter направляет его в любой io.Writer.
- slog подключается двумя мостами: адаптер io.Writer для трассировок и LoggerWithFormatter с slog-атрибутами и return "" для access-лога.
Домашнее задание
Возьмите main из урока и доведите до продакшен-вида: подмените DefaultWriter и DefaultErrorWriter на файл, добавьте slogAccessLog со структурными полями и панику /boom с CustomRecovery, отдающей Envelope с кодом INTERNAL. Затем добавьте звено requestID из прошлого урока и добейтесь, чтобы идентификатор запроса попал в access-лог через param.Keys. Проверьте curl'ом: строка /ping — с req=..., /boom — 500, конверт и две записи в журнале (структурная от форматтера и трассировка со стеком).
Личные заметкиПусто
Вопросы и ответы по уроку
Как полностью отключить логи gin?
Независимо управляются два потока. [GIN-debug] строки глушатся SetMode(gin.TestMode) или gin.ReleaseMode (или GIN_MODE=release). Access-лог — просто не добавляйте gin.Logger, либо направьте его в никуда: gin.LoggerWithConfig(gin.LoggerConfig{Output: io.Discard}). Трассировки Recovery отключать не стоит: замените их CustomRecovery со своим компактным обработчиком, если шумят.
Чем c.Error отличается от AbortWithError?
c.Error(err) лишь дописывает ошибку в копилку c.Errors — статус и тело она не трогает, и без последующего ответа клиент получит пустой 200. AbortWithError(code, err) — это AbortWithStatus плюс c.Error: refusal со статусом и отметка в копилке разом. Обе ошибки по умолчанию приватные и попадут в ErrorMessage лога.
Куда направлять логи gin в продакшене?
В структурный логгер: LoggerWithFormatter со slog/zap-атрибутами и return "" для access-лога, RecoveryWithWriter с адаптером io.Writer для трассировок. Писать [GIN]-строки в файл тоже можно — подменой DefaultWriter до gin.Default(), — но искать по полю status в структурной записи сильно удобнее, чем grep'ать по плоскому тексту.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
Echo: минималистичный быстрый фреймворк
Курс Echo для Go: маршруты, middleware, валидация, JWT, SSE, тестирование и деплой. 25 уроков с примерами и итоговым API-проектом.
- Строить REST API на Echo
- Использовать типизированные запросы/ответы (Request/Response)
- Управлять ошибками через echo.HTTPError
- Писать middleware, включая JWT-аутентификацию