Помню, когда я только начинал программировать на Go, вывод переменных был первой путаницей: где-то fmt.Println, где-то fmt.Printf, а где-то и вовсе fmt.Sprintf. Вариантов казалось много, и закономерность в голове долго не складывалась. Поэтому в этой статье я разложил весь пакет fmt по полочкам и делюсь результатом, чтобы у вас всё стало на свои места быстрее, чем у меня.
- Три вопроса, которые снимают всю путаницу
- Куда пишем результат?
- Как форматируем аргументы?
- Таблица всех девяти комбинаций
- Print, Println, Printf — вывод в консоль
- Sprint, Sprintln, Sprintf — вывод в строку
- Fprint, Fprintln, Fprintf — вывод в io.Writer
- Пробелы между аргументами: где логика, а где сюрприз
- Шпаргалка по глаголам форматирования
- Errorf и вывод ошибок
- Когда что выбирать на практике
- Ключевой вывод
Три вопроса, которые снимают всю путаницу
Весь пакет fmt для вывода строится на пересечении двух осей.
Куда пишем результат?
- в стандартный вывод (
os.Stdout) — функции без префикса,Print... - в строку, которая вернёт функция — префикс
SотString,Sprint... - в произвольный
io.Writer(файл, буфер, сетевое соединение) — префиксFот словаFile,Fprint...
Как форматируем аргументы?
- через пробел, без переноса строки — суффикса нет, просто
Print - через пробел, с переносом строки в конце — суффикс
ln - по шаблону с плейсхолдерами
%v,%d,%s— суффиксf
Если перемножить 3 варианта назначения на 3 варианта форматирования, получится 9 функций. Больше в базовом наборе fmt для вывода ничего нет, и как только эта матрица укладывается в голове, весь пакет перестаёт быть загадкой.
Таблица всех девяти комбинаций
| Без переноса строки | С переносом строки | По шаблону | |
| В консоль | fmt.Print |
fmt.Println |
fmt.Printf |
| В строку | fmt.Sprint |
fmt.Sprintln |
fmt.Sprintf |
| В io.Writer | fmt.Fprint |
fmt.Fprintln |
fmt.Fprintf |
Дальше — подробный разбор каждой строки, с примерами и подводными камнями.
Print, Println, Printf — вывод в консоль
Это самая привычная тройка, с неё начинается почти любой учебник.
fmt.Print("Привет", "мир") // Приветмир
fmt.Print("Привет", 35) // Привет35
fmt.Println("Привет", "мир") // Привет мир
// (с переносом строки в конце)
fmt.Printf("Привет, %s! Тебе %d лет.\n", "мир", 35)
// Привет, мир! Тебе 35 лет.
Все три пишут пишут в os.Stdout и возвращают (n int, err error) — количество записанных байт и ошибку. На практике эту ошибку почти никто не проверяет: os.Stdout почти никогда не отбрасывает данные при обычной работе.
Но есть случай, где эта ошибка важна. В терминале можно соединить вывод одной программы со входом другой через символ | — это и называется (pipe).
Например, команда your-tool | head -1 берёт весь вывод your-tool и передаёт его в head -1, которая печатает только первую строку и сразу завершается — читать дальше она не станет.
Если ваша утилита продолжает писать в os.Stdout после того, как head уже закрылась, система вернёт ошибку «broken pipe»: писать больше некуда, получатель отключился. Без проверки err в этот момент программа просто не узнает, что часть вывода потерялась.
Sprint, Sprintln, Sprintf — вывод в строку
Эта тройка ничего никуда не печатает. Она просто собирает результат и отдаёт его как string.
s1 := fmt.Sprint("Привет", "мир") // "Приветмир"
s2 := fmt.Sprintln("Привет", "мир") // "Привет мир\n"
s3 := fmt.Sprintf("id=%d, name=%s", 34, "Стас") // "id=34, name=Стас"
Очень часто использую Sprintf при написании кода. Например, ключ для кэша: у меня есть ID пользователя и название региона, и я хочу собрать из них строку вида user:1:region:vrn.
key := fmt.Sprintf("user:%d:region:%s", userID, region)
Sprint и Sprintln в такой задаче не годятся: они просто склеивают аргументы через пробел, и на выходе получилось бы 1 vrn без структуры и разделителей. Шаблон в Sprintf даёт полный контроль над тем, как выглядит итоговая строка, поэтому в реальном коде он почти всегда обходит Sprint по частоте использования.
Fprint, Fprintln, Fprintf — вывод в io.Writer
Это самая гибкая тройка, и часто самая недооценённая новичками. io.Writer — это интерфейс с одним методом Write, и под него подходит буквально всё: файл, bytes.Buffer, сетевое соединение, http.ResponseWriter, os.Stderr.
file, _ := os.Create("log.txt")
defer file.Close()
fmt.Fprintln(file, "Сервис запущен")
var buf bytes.Buffer
fmt.Fprintf(%buf, "user=%s, ip=%s", "admin", "192.168.1.1")
fmt.Fprintln(os.Stderr, "предупреждение: конфиг не найден")
Кстати, если вы когда-нибудь задумывались, что происходит по капотом у fmt.Println — там просто вызывается Fprintln(os.Stdout, ...). Вся консольная тройка — это тонкая обёртка над записью в os.Stdout, который тоже реализует io.Writer. Матрица из девяти функций на деле держится всего трёх реализациях, остальное — синтаксический сахар.
На практике это выглядит так: когда пишите консольную утилиту, os.Stderr для ошибок и предупреждений, os.Stdout для основного результата — это не просто вкусовщина, а конвенция Unix, которая позволяет делать your-tool 2>errors.log >result.txt и разводить потоки.
Пробелы между аргументами: где логика, а где сюрприз
Вот тот самый момент, на котором я застрял в начале. Правило звучит так: Print и Sprint ставят пробел между двумя соседними аргументами только в одном случае — когда оба этих аргумента не строки. Если хотя бы один из пары — строка, пробела не будет.
fmt.Print("a", "b") // ab - оба строки, пробела нет
fmt.Print("a", 1) // a1 - рядом строка, пробела нет
fmt.Print(1, 2) // 1 2 - оба не строки, пробела есть
fmt.Print(1, "b") // 1b - рядом строка, пробела нет
Println и Sprintln под это правило не попадают — они ставят пробел между всеми аргументами всегда, независимо от типов, и добавляют перенос строки в конце.
Тут кроется подвох из примера в начале статьи. fmt.Sprint("a", 1, "b") даёт a1b — без единого пробела. Между "a" и 1 пробела нет, потому что рядом строка. Между 1 и "b" пробела тоже нет — по той же причине. Пробел появляется только там, где два не-строковых значения стоят рядом друг с другом. Если результат конкатенации отличается от ожидаемого — первым делом проверьте, нет ли рядом двух не-строковых аргументов.
Шпаргалка по глаголам форматирования
Раз уж заговорили про Printf-семейство, вот основные плейсхолдеры, которые реально нужны в интервью и в жизни:
| Глагол | Что делает | Пример |
| %v | значение в стандартном виде | Printf(«%v», user) |
| %+v | значение с именами полей структуры | Printf(«%+v», user) |
| %#v | Go-синтаксис значения | Printf(«%#v», user) |
| %T | тип значения | Printf(«%T», user) |
| %d | целое число | Printf(«%d», 35) |
| %s | строка | Printf(«%s», name) |
| %f | float с фиксированной точностью | Printf(«%.2f», 3.14159) |
| %t | булево значение | Printf(«%t», true) |
| %p | указатель (адрес) | Printf(«%p», %x) |
| %w | обёртывание ошибки (только в Errorf) | Printf(«контекст: %w», err) |
%+v — мой любимый глагол для дебага структур: выводит все поля с именами, и не нужно писать String() метод ради разового принта в консоль.
Errorf и вывод ошибок
Формально Errorf не входит в матрицу «вывода», потому что он ничего не печатает и не возвращает строку — он возвращает error. Но раз мы уже разбираем семейство fmt, обойти его нельзя, тем более что синтаксис у него общий с Printf.
if err != nil {
return fmt.Errorf("не удалось прочитать конфиг: %w", err)
}
Глагол %w появился в Go 1.13 и умеет заворачивать исходную ошибку так, что её потом можно достать через erros.Unwrap, errors.Is или erros.As. Если использовать %v вместо %w, ошибка превратится в обычную строку и потеряет связь с оригиналом — это частый вопрос на собеседованиях по Go, и хорошая тема для отдельной статьи.
Когда что выбирать на практике
- Нужно быстро что-то напечатать при отладке —
fmt.Println, самый низкий порог входа. - Нужен предсказуемый формат вывода —
fmt.Printf, явный шаблон читается лучше неявной склейки через пробел. - Собираете строку для дальнейшего использования (лог, ключ кэша, сообщение об ошибке) —
fmt.Sprintf. - Пишете в файл, буфер или любой другой
io.Writer— вся тройкаFprint*, особенноFprintfдля форматирования записи. - Пишите библиотеку и не хотите завязываться на
os.Stdout— всегда принимайтеio.Writerпараметром и используйтеFprintf, это даёт вызывающему коду выбор, куда слать вывод.
Последний пункт — не теоретическая рекомендация. Любая функция, которая жёстко вызывает fmt.Println внутри библиотеки, потом мешает тестировать код: тесты ловят вывод через захват os.Stdout, что неудобно и хрупко. Если вместо этого функция принимает io.Writer, в тесте достаточно подставить bytes.Buffer и сравнить содержимое.
Ключевой вывод
Девять функций fmt для вывода укладываются в простую матрицу: три места назначения (консоль, строка, io.Writer) на три способа форматирования (через пробел, через пробел с переносом на новую строку, по шаблону). Как только эта матрица закрепляется в голове, выбор нужной функции перестаёт требовать подглядывания в документацию.
А как у вас — есть свой выбор привычек по выбору между
PrintfиSprintf? Или свой способ ловить баги на пробелах между аргументами? Делитесь в комментариях, интересно сравнить подходы.