Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 8 из 25 ~16 мин +50 XP

Логирование и recovery

Строку [GIN] и спасатель Recovery мы разобрали в уроке про встроенные middleware — теперь хозяйственные вопросы: gin логирование позволяет направить вывод в файл, буфер теста или структурный логгер, подменив пакетные DefaultWriter и DefaultErrorWriter. Разберём LoggerWithFormatter и тип LogFormatter, копилку ошибок c.Errors, из которой в строку лога попадает ErrorMessage, и CustomRecovery — свой обработчик паники, который отдаёт клиенту честный JSON.

Содержание урока · 13 разделов

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-ов: файл вместо консоли

GO
func 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, секреты, — их место в логе; публичные читает клиент.

Копилка в действии: публичное клиенту, приватное в лог

GO
r.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 и CustomRecoveryDefaultErrorWriter (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 без Recoverystderr сервера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-а, корректный ответ хендлера.

func main() { gin.(gin.ReleaseMode) // без [GIN-debug] строк в журнале f, err := os.Create("gin.log") if err != nil { panic(err) } // Подмена до gin.Default(): звенья захватят writer при регистрации. gin. = f gin.DefaultErrorWriter = f r := gin.Default() r.GET("/ping", func(c *gin.Context) { c.(http.StatusOK, "pong") }) r.Run(":8080") }

Соберите запрос с паникой: от 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

Термин

DefaultWriter и DefaultErrorWriter

нажмите, чтобы перевернуть
Определение

Пакетные io.Writer gin: os.Stdout и os.Stderr. Обслуживают Logger, Recovery и [GIN-debug]. Захватываются звеньями при регистрации — подменять до gin.Default().

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +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 · Логирование и recovery)

Вопросы и ответы по уроку

Как полностью отключить логи 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'ать по плоскому тексту.

Термины урока

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

Сравните с альтернативами

Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.

Следующая ступень учебного пути

Echo25 уроков~13 чпрактика25 уроков уже доступны

Echo: минималистичный быстрый фреймворк

Курс Echo для Go: маршруты, middleware, валидация, JWT, SSE, тестирование и деплой. 25 уроков с примерами и итоговым API-проектом.

  • Строить REST API на Echo
  • Использовать типизированные запросы/ответы (Request/Response)
  • Управлять ошибками через echo.HTTPError
  • Писать middleware, включая JWT-аутентификацию