Форматирование вывода в Go: разбираем на живом примере
Пакет fmt в Go — это не только Println. Разбираем глаголы форматирования, ширину полей, выравнивание, работу со структурами и типичные ошибки — на фрагментах, которые можно запустить прямо сейчас.

Почему привычный Println быстро упирается в стену
Большинство Go-программ начинается с fmt.Println. Пока вы выводите строку или число, всё работает. Но стоит собрать отчёт в консоли, выровнять колонки таблицы, вывести дамп структуры для отладки или записать в лог значение с фиксированной точностью — Println заканчивается. Начинается fmt.Sprintf, fmt.Fprintf и набор глаголов форматирования, которые выглядят как шифровка: %+v, %#v, %-20s, %08.2f. Ниже разберём, что стоит за каждым символом, чем один подход отличается от другого и где форматирование ведёт себя не так, как ожидаешь.
Три функции, три задачи
В пакете fmt (Go 1.22+) для форматированного вывода используются три семейства функций. Разница не в синтаксисе шаблона — он одинаковый — а в том, куда идёт результат.
| Функция | Куда пишет | Когда использовать |
|---|---|---|
fmt.Sprintf | Возвращает строку | Формирование сообщений, логов, ответов API |
fmt.Printf | Стандартный вывод (os.Stdout) | Консольные утилиты, отладка |
fmt.Fprintf | Любой io.Writer | Запись в файл, сокет, HTTP-ответ, буфер |
Sprintf — самый частый выбор в продакшн-коде: результат можно передать дальше, записать в лог через slog или вернуть из функции. Printf удобен для CLI. Fprintf пригодится, когда вы пишете напрямую в http.ResponseWriter или файл, минуя промежуточную строку.
Глаголы: что стоит за символом после %
Глагол — буква (или сочетание) после знака %. Она говорит Go, как интерпретировать аргумент. Вот основные группы.
Строки и общие типы
%s— строка как есть, или результат методаString()у типа.%q— строка в двойных кавычках с экранированием спецсимволов. Незаменим для логов, где значение может содержать пробелы или переносы строк.%v— значение в формате по умолчанию. Для структуры выведет{поле1 поле2}.%+v— то же, но с именами полей:{Name:Alice Age:30}.%#v— Go-синтаксис:main.User{Name:"Alice", Age:30}. Удобно для отладки, когда нужно скопировать значение обратно в код.%T— тип аргумента:main.User,[]int,*os.File.
Числа
%d— целое в десятичной системе.%b,%o,%x— двоичная, восьмеричная, шестнадцатеричная.%#xдобавит префикс0x.%f— число с плавающей точкой, по умолчанию 6 знаков после запятой.%e— экспоненциальная запись:3.141593e+00.%g— компактная форма: убирает лишние нули, переключается на экспоненту для больших чисел.
Прочее
%p— адрес указателя.%c— символ Unicode по его коду.%%— литеральный знак процента.
Сноска. Глагол (verb) — термин из документации пакета fmt. Он не имеет отношения к грамматике: это просто буква-спецификатор, аналог %d и %s из C-языков.
Ширина, точность и выравнивание
Между % и глаголом можно вставить модификаторы. Именно они превращают сырой вывод в читаемую таблицу.
// Go 1.22+
package main
import "fmt"
type Product struct {
Name string
Price float64
Qty int
}
func main() {
items := []Product{
{"Кабель USB-C", 490.00, 120},
{"Адаптер питания 65W", 2790.50, 34},
{"Док-станция Thunderbolt", 14990.00, 7},
}
// Заголовок: минус — выравнивание влево, число — ширина поля
fmt.Printf("%-30s %10s %6s\n", "Товар", "Цена", "Кол-во")
fmt.Println("------------------------------------------------")
for _, p := range items {
// 08.2f: ширина 10, 2 знака после точки, ведущие пробелы
fmt.Printf("%-30s %10.2f %6d\n", p.Name, p.Price, p.Qty)
}
}
Вывод:
Товар Цена Кол-во
------------------------------------------------
Кабель USB-C 490.00 120
Адаптер питания 65W 2790.50 34
Док-станция Thunderbolt 14990.00 7
Разберём %-30s по частям:
-— выравнивание по левому краю (по умолчанию правое).30— минимальная ширина поля в символах. Если строка короче, остаток заполняется пробелами.s— глагол: строка.
А %10.2f: ширина 10 символов (включая точку и знаки после неё), точность 2 знака. Если число уже, поле дополняется пробелами слева.
Ширина в fmt считается в рунах, а не в байтах. Кириллическая строка из 10 букв займёт 10 позиций ширины, хотя в UTF-8 это 20 байт. Но символы разной визуальной ширины (иероглифы, эмодзи) всё равно сломают колонки — fmt не знает про ширину отображения в терминале.
Stringer и GoStringer: форматирование собственных типов
Когда %v или %s встречают тип, реализующий интерфейс fmt.Stringer (метод String() string), пакет вызывает этот метод вместо стандартного вывода полей. Аналогично %#v вызывает GoString() string из интерфейса fmt.GoStringer.
// Go 1.22+
package main
import "fmt"
type Status int
const (
Pending Status = iota
Active
Blocked
)
// Реализуем fmt.Stringer
func (s Status) String() string {
switch s {
case Pending:
return "ожидание"
case Active:
return "активен"
case Blocked:
return "заблокирован"
default:
return fmt.Sprintf("Status(%d)", int(s))
}
}
func main() {
st := Blocked
fmt.Printf("Статус: %s\n", st) // Статус: заблокирован
fmt.Printf("Отладка: %#v\n", st) // Отладка: 2 (GoStringer не реализован)
fmt.Printf("Тип: %T, значение: %v\n", st, st) // Тип: main.Status, значение: заблокирован
}
Это единственный штатный способ сделать так, чтобы в логах вместо цифры появлялось человеческое название. Без Stringer статус Blocked выведется как 2 — и через месяц вы не вспомните, что это значило.
Типичные ошибки и подводные камни
Несоответствие глагола и типа
Go не падает, если передать строку в %d. Он выведет %!d(string=hello) — уродливую диагностику прямо в выходную строку. В тесте это заметно, в логе на проде — нет, пока кто-то не начнёт разбирать инцидент.
Линтер go vet (встроен в go build с Go 1.21) ловит такие несоответствия на этапе компиляции. Не игнорируйте его предупреждения.
Лишний или недостающий аргумент
Количество глаголов и аргументов должно совпадать. Лишний аргумент выведет %!(EXTRA ...), недостающий — %!s(MISSING). Обе ситуации ловятся go vet.
Sprintf в цикле и аллокации
fmt.Sprintf каждый раз аллоцирует новую строку. В горячем цикле на тысячах итераций это заметная нагрузка на GC. Альтернатива — strings.Builder или strconv.AppendFloat / strconv.AppendInt, которые пишут в существующий слайс байт без аллокаций.
Форматирование времени
Пакет fmt не знает о типе time.Time ничего особенного — он просто вызовет String(). Если нужен конкретный формат даты, используйте time.Time.Format с шаблоном-образцом (Mon Jan 2 15:04:05 MST 2006), а не конструируйте дату через Sprintf.
Сравнение подходов: fmt, strconv, strings.Builder
| Критерий | fmt.Sprintf | strconv + Builder |
|---|---|---|
| Читаемость шаблона | Высокая: один вызов, вся структура строки видна | Ниже: цепочка вызовов Append |
| Аллокации на вызов | 1–3 (зависит от числа аргументов и их типов) | 0, если Builder переиспользуется |
| Безопасность типов | Проверка только линтером, в рантайме — нет | Ошибка компиляции при неверном типе |
| Поддержка Stringer | Да, автоматически | Нет, вызывайте .String() вручную |
| Когда выбирать | Логи, сообщения, не горячий путь | Горячие циклы, сериализация больших объёмов |
Когда что выбирать
Если строка собирается один раз для лога или ответа пользователю — fmt.Sprintf. Читаемость шаблона окупает одну-две аллокации. Если вы собираете CSV на 100 000 строк или генерируете тело ответа в горячем эндпоинте — strings.Builder с strconv. Разница в бенчмарках может составлять порядок, но измерять стоит на вашем профиле: go test -bench=. -benchmem покажет и аллокации, и наносекунды.
Что дальше
После знакомства с глаголами и шириной полей логичный следующий шаг — шаблоны из пакета text/template (и html/template для веба). Они решают задачу посложнее: генерация текста с условиями и циклами, где Sprintf уже превращается в нечитаемую кашу. Для структурированного логирования стоит посмотреть на log/slog (Go 1.21+), где форматирование значений происходит лениво и не тратит ресурсы, если уровень лога отключён.
Создатель презентаций: Структура и текст каждого слайда: заголовки-утверждения, одна мысль на слайд, заметки докладчику.
Спросить за 150 ₽Источники: Документация пакета fmt — Go standard library, Документация пакета strconv — Go standard library
Частые вопросы
Чем %v отличается от %+v и %#v?
Можно ли форматировать дату через fmt.Sprintf?
Что будет, если передать неправильный тип аргумента?
Sprintf в цикле — это правда медленно?
Работает ли ширина полей корректно с кириллицей?
А если у меня другая версия Go — всё будет работать?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.