Пакет fmt в Go помогает вывести значения в терминал или собрать из них строку. Printf печатает по шаблону, а Sprintf возвращает результат для дальнейшей работы. Разберём, какой спецификатор выбрать для числа, строки или структуры, как задать точность и что проверить, если в выводе появилось %!.
Примеры можно запускать без сторонних библиотек. Начнём с полной программы, а в следующих фрагментах будем опускать package main, импорт fmt и функцию main, если они не нужны для объяснения.
Printf печатает, Sprintf возвращает строку
Допустим, нужно вывести номер заказа и его сумму:
package main
import "fmt"
func main() {
id := 42
total := 1250.5
fmt.Printf("Заказ %d: %.2f руб.\n", id, total)
message := fmt.Sprintf("Заказ %d: %.2f руб.", id, total)
fmt.Println(message)
}
Оба сообщения выглядят одинаково: Заказ 42: 1250.50 руб. Но первый вызов сразу пишет в стандартный вывод, а второй создаёт строку message. Её можно вернуть из функции или передать другому коду. Сам Sprintf ничего не печатает.
У функций разная сигнатура:
func Printf(format string, a ...any) (n int, err error)
func Sprintf(format string, a ...any) string
Здесь приведены объявления из API, а не код для вставки в main. Первый аргумент содержит формат. Дальше идут значения, которые подставляются вместо спецификаторов. Запись ...any позволяет передать несколько аргументов разных типов.
Printf возвращает число записанных байтов и ошибку записи. В учебных примерах эти результаты часто пропускают. Если от вывода зависит работа программы, ошибку нужно обработать. При этом несовпадение типа со спецификатором отражается в самом тексте, а не служит ошибкой записи. К этому вернёмся ниже.
Вызов Sprintf удобен, когда строка нужна как значение:
name := "report"
version := 3
filename := fmt.Sprintf("%s-v%02d.csv", name, version)
fmt.Println(filename) // report-v03.csv
Здесь %s подставляет имя, а %02d выводит номер версии минимум в две позиции с нулём слева. Если версия станет равна 123, результатом будет report-v123.csv: ширина не обрезает число.
Как выбрать между Print, Println и Printf
Суффикс функции определяет, как fmt собирает текст:
| Функция | Как работает | Когда использовать |
|---|---|---|
Print | Выводит значения в стандартном формате | Части текста уже подготовлены |
Println | Разделяет аргументы пробелами, добавляет перевод строки | Нужно быстро вывести значения |
Printf | Читает спецификаторы из строки формата | Нужно управлять видом результата |
Sprintf | Форматирует и возвращает строку | Результат нужен в переменной |
Fprintf | Форматирует и пишет в io.Writer | Вывод нужен в файл, буфер или другой приёмник |
Print не добавляет перевод строки. Пробел между соседними аргументами он вставляет только тогда, когда ни один из них не является строкой. Поэтому результат зависит от типов:
fmt.Print("id=", 42, ", active=", true, "\n") // id=42, active=true
fmt.Print(10, 20, "\n") // 10 20
fmt.Println("id=", 42) // id= 42
Для сообщений с фиксированной структурой обычно проще прочитать Printf: расположение пробелов видно в шаблоне. Если шаблон не нужен, Println избавляет от ручного \n.
Сам Printf перевод строки не добавляет. Два вызова fmt.Printf("%d", 1) и fmt.Printf("%d", 2) напечатают 12 без пробела и перевода строки. Это полезно для вывода по частям, но при обычной печати сообщений легко забыть \n.
У функций есть и другие сочетания: Sprint возвращает строку по правилам Print, а Sprintln добавляет в возвращаемую строку пробелы между аргументами и перевод строки в конце. Наличие буквы S меняет место назначения результата, но не отменяет правила суффикса.
Основные спецификаторы fmt
Спецификатор начинается с % и заканчивается символом, который задаёт представление значения. В документации этот символ называется verb. Например, в %.2f символ f выбирает десятичную запись с фиксированной точкой, а .2 задаёт число знаков после точки.
Для повседневной работы достаточно следующей таблицы:
| Спецификатор | Назначение | Пример результата |
|---|---|---|
%v | Стандартное представление значения | 42, true, [1 2] |
%+v | Для структуры добавляет имена полей | {ID:42 Name:Анна} |
%#v | Представление в синтаксисе Go | []int{1, 2} |
%T | Тип значения | int, []string |
%t | Логическое значение | true |
%d | Целое число в десятичной системе | 42 |
%b, %o, %x, %X | Целое в двоичной, восьмеричной или шестнадцатеричной системе | 101010, 52, 2a, 2A для 42 |
%f | Дробное число с фиксированной точкой | 12.500000 |
%e | Дробное число с экспонентой | 1.250000e+01 |
%g | Выбор обычной записи или экспоненты в зависимости от величины и точности | 12.5 |
%s | Строка или содержимое []byte | hello |
%q | Строка в кавычках с экранированием; для целого, символ в одинарных кавычках | "hello", 'A' |
%c | Символ по кодовой точке Unicode | A для 65 |
%p | Адрес указателя | Адрес вида 0xc... |
%% | Знак процента без аргумента | % |
Таблица покрывает обычные значения без собственных методов форматирования. Такие методы могут изменить результат; пример с String() разберём отдельно. Полный перечень есть в официальной справке fmt.
Когда использовать %v и %T
%v подходит, если нужно посмотреть значение без настройки его представления:
values := []int{10, 20, 30}
fmt.Printf("values=%v, type=%T\n", values, values)
// values=[10 20 30], type=[]int
Для строки %v по умолчанию даёт текст без кавычек, для целого числа десятичную запись, для bool значение true или false. Срезы и массивы выводятся в квадратных скобках, элементы разделяются пробелами.
%T помогает понять, какое конкретное значение хранится в интерфейсе:
var value any = int64(42)
fmt.Printf("%T: %v\n", value, value) // int64: 42
В выводе будет int64, хотя переменная объявлена как any. fmt работает с конкретным значением внутри интерфейса. Это удобно при отладке кода, который принимает разные типы через один параметр.
Целые числа и знак процента
Для счётчика используйте %d. Для маски битов или шестнадцатеричного кода выберите нужную систему счисления:
n := 42
fmt.Printf("dec=%d bin=%b hex=%x\n", n, n, n)
// dec=42 bin=101010 hex=2a
fmt.Printf("bin=%#b hex=%#x\n", n, n)
// bin=0b101010 hex=0x2a
fmt.Printf("Готово: %d%%\n", 75)
// Готово: 75%
Флаг # добавляет префикс системы счисления для %b и %x. А %% выводит один знак процента и не забирает аргумент. Поэтому в последнем вызове достаточно одного числа.
Не переносите в Go модификаторы длины из C. И int, и int64 выводятся через %d; писать %ld для int64 не нужно. Тип аргумента уже передан вместе со значением.
Ширина, точность и флаги
Практическая форма спецификатора выглядит как %[флаги][ширина][.точность]символ. Квадратные скобки здесь обозначают необязательные части, их не нужно писать в шаблоне. Например, %08.2f задаёт ширину восемь позиций, два знака после точки и заполнение нулями.
Ширина задаёт минимальное место
fmt.Printf("|%6d|\n", 42) // | 42|
fmt.Printf("|%-6d|\n", 42) // |42 |
fmt.Printf("|%06d|\n", 42) // |000042|
fmt.Printf("|%+d|\n", 42) // |+42|
fmt.Printf("|%3d|\n", 12345) // |12345|
По умолчанию fmt дополняет поле пробелами слева. Флаг - переносит пробелы вправо, 0 заменяет заполнение на нули, а + добавляет знак и положительным числам.
Ширина задаёт нижнюю границу. Последний пример выводит все пять цифр, хотя в формате указано 3. Если в столбце помещается трёхзначный номер, номер 12345 всё равно займёт пять позиций и сдвинет следующий столбец.
Для простой текстовой таблицы можно задать отдельную ширину имени и числа:
fmt.Printf("%-10s %6s\n", "Товар", "Штук")
fmt.Printf("%-10s %6d\n", "Книга", 12)
fmt.Printf("%-10s %6d\n", "Блокнот", 3)
Такой подход удобен для короткого отчёта с известными данными. Ширина измеряется в рунах, а не в экранных колонках. Комбинируемые символы и некоторые широкие символы могут нарушить визуальное выравнивание. Сам fmt не рассчитывает, сколько места текст займёт в терминале.
Точность зависит от типа значения
Для %f точность задаёт число знаков после точки:
price := 12.345
fmt.Printf("%f\n", price) // 12.345000
fmt.Printf("%.2f\n", price) // 12.35
fmt.Printf("%8.2f\n", price) // 12.35
fmt.Printf("%.3g\n", price) // 12.3
По умолчанию %f печатает шесть знаков после точки. %.2f округляет представление до двух знаков, но не меняет саму переменную. У %g точность задаёт максимальное число значащих цифр, поэтому .3 здесь даёт 12.3.
Не путайте форматирование с точной десятичной арифметикой. float64 хранит двоичное приближение многих десятичных дробей. Шаблон %.2f определяет вид текста, но не исправляет погрешности расчёта. Для денежных расчётов отдельно выбирают модель хранения, например целое число минимальных денежных единиц, и правила округления.
Для целого числа точность задаёт минимальное число цифр:
fmt.Printf("%.4d\n", 42) // 0042
А для строки точность ограничивает длину входного текста:
fmt.Printf("%.3s\n", "Привет") // При
fmt.Printf("%8.3s\n", "Привет") // При
При %s строка обрезается по рунам. Кириллическая буква не разрывается посередине её UTF-8-представления. Но руна не всегда совпадает с видимым символом: последовательность с комбинируемым знаком может состоять из нескольких рун. Если нужно обрезать пользовательский текст по видимым символам, одной точности fmt недостаточно.
Ширину и точность можно передавать аргументами через *:
width, precision := 8, 2
fmt.Printf("|%*.*f|\n", width, precision, 12.345)
// | 12.35|
Сначала идут ширина и точность типа int, затем форматируемое значение. Это удобно, когда число знаков задаётся настройкой, а не зашито в шаблоне.
Строки, кавычки и байты
%s выводит содержимое строки как есть. Это может скрыть причину ошибки: завершающий пробел почти незаметен, а перевод строки разбивает сообщение. %q показывает границы строки и экранирует управляющие символы:
name := "Анна\n"
fmt.Printf("name=%q\n", name) // name="Анна\n"
fmt.Printf("%+q\n", "Анна") // "\u0410\u043d\u043d\u0430"
Флаг + у %q оставляет только ASCII в результате, заменяя остальные символы escape-последовательностями. Обычный %q удобнее для чтения русского текста, а %+q помогает рассмотреть его кодовые точки.
Для []byte строковые спецификаторы работают с содержимым всего среза:
data := []byte{0x41, 0x42, 0x0a}
fmt.Printf("%v\n", data) // [65 66 10]
fmt.Printf("%q\n", data) // "AB\n"
fmt.Printf("%x\n", data) // 41420a
fmt.Printf("% x\n", data) // 41 42 0a
С %v видны числовые значения элементов. С %q содержимое рассматривается как строка, а %x показывает два шестнадцатеричных знака на байт. Пробел в % x разделяет байты пробелами. В таком выводе перевод строки виден как байт 0a. Его можно найти, даже если в обычной строке он выглядит как разрыв сообщения.
У точности с %x есть отличие: для строк и байтовых срезов она считается в байтах:
fmt.Printf("%.1s\n", "Я") // Я
fmt.Printf("%.1x\n", "Я") // d0
fmt.Printf("%x\n", "Я") // d0af
Здесь буква Я занимает два байта в UTF-8. %.1x выбирает только первый байт. Это не обрезка строки до одного символа, а ограничение входных байтов для шестнадцатеричного вывода.
Структуры: %v, %+v и %#v
Для отладки структуры полезно сначала вывести имена полей:
type User struct {
ID int
Name string
}
u := User{ID: 42, Name: "Анна"}
fmt.Printf("%v\n", u) // {42 Анна}
fmt.Printf("%+v\n", u) // {ID:42 Name:Анна}
fmt.Printf("%#v\n", u) // main.User{ID:42, Name:"Анна"}
В последнем комментарии предполагается, что тип объявлен в пакете main. %#v показывает представление в синтаксисе Go, включая тип. Это помогает отличить строку от числа или увидеть тип пустого среза. Однако такой вывод не стоит использовать как формат хранения данных: представление зависит от типов и их методов. Для обмена данными выбирайте явный формат, например JSON.
Почему String() меняет вывод
Тип может реализовать интерфейс fmt.Stringer, то есть метод String() string. Тогда для обычного %v пакет использует этот метод:
package main
import "fmt"
type User struct {
ID int
Name string
}
func (u User) String() string {
return fmt.Sprintf("user:%d", u.ID)
}
func main() {
u := User{ID: 42, Name: "Анна"}
fmt.Printf("%v\n", u) // user:42
fmt.Printf("%+v\n", u) // user:42
fmt.Printf("%#v\n", u) // main.User{ID:42, Name:"Анна"}
}
Поэтому %+v не гарантирует вывод полей: для этого типа результатом тоже будет user:42. В примере %#v показывает поля, поскольку тип не реализует GoString() или собственный Format. Эти методы тоже участвуют в правилах форматирования.
Не передавайте то же значение в Sprintf("%v", u) внутри его String(): это снова вызовет String() и приведёт к рекурсии. Форматируйте отдельные поля, как в примере. Если метод нужен для именованного строкового типа, перед форматированием можно преобразовать значение в обычный string.
Fprintf: запись в файл или буфер
Если результат нужно отправить в конкретный приёмник, используйте Fprintf. Его первый аргумент должен реализовать io.Writer. Например, соберём отчёт в bytes.Buffer:
package main
import (
"bytes"
"fmt"
)
func main() {
var report bytes.Buffer
fmt.Fprintf(&report, "Заказ: %d\n", 42)
fmt.Fprintf(&report, "Статус: %s\n", "готов")
fmt.Print(report.String())
}
Здесь два вызова дописывают текст в один буфер. У bytes.Buffer запись не возвращает ошибку, поэтому пример пропускает результаты Fprintf. Для файла или сетевого соединения ошибку записи нужно проверять.
Чтобы вывести сообщение в стандартный поток ошибок, передайте os.Stderr:
// Дополнительно нужен импорт os.
if _, err := fmt.Fprintf(os.Stderr, "Заказ %d не найден\n", 42); err != nil {
return
}
В этом фрагменте при ошибке записи функция завершается. В вашем коде обработка зависит от задачи: можно вернуть ошибку вызывающей стороне. Возвращаемое число n содержит количество записанных байтов, поэтому для русского текста оно может отличаться от числа букв.
Ошибки формата: откуда берётся %!
Обычное несовпадение спецификатора и типа не вызывает панику. Вместо этого fmt добавляет диагностику в строку:
| Ошибка | Вызов | Результат |
|---|---|---|
| Неподходящий тип | fmt.Sprintf("%d", "42") | %!d(string=42) |
| Не хватает аргумента | fmt.Sprintf("id=%d") | id=%!d(MISSING) |
| Лишний аргумент | fmt.Sprintf("ready", 42) | ready%!(EXTRA int=42) |
Ширина не типа int | fmt.Sprintf("%*s", "4", "Go") | %!(BADWIDTH)Go |
Эти вызовы намеренно ошибочные. Они показывают, что искать в сообщении: MISSING означает отсутствующий аргумент, а EXTRA указывает на лишний. Если результат собирается через Sprintf, отдельной возвращаемой ошибки не будет: диагностика станет частью строки.
Проверьте порядок аргументов, количество спецификаторов и тип каждого значения. Например, шаблону "id=%d name=%s" нужны номер и имя именно в таком порядке. Исключения: %% не требует аргумента, а * для ширины и точности забирает дополнительные значения.
Для повторного использования аргумента можно указать его индекс, начиная с единицы:
fmt.Printf("dec=%[1]d hex=%[1]x\n", 42)
// dec=42 hex=2a
В коротком шаблоне индекс избавляет от повторной передачи значения. Длинные шаблоны с индексами сложнее проверять, особенно если рядом есть *. Если строка стала трудной для чтения, лучше упростить её, чем добавлять ещё один уровень управления аргументами.
Ошибки в статических шаблонах помогает находить go vet:
$ go vet ./...
Проверка printf в go vet ищет, в частности, несовпадение аргументов со строкой формата. Она не гарантирует обнаружение всех ошибок, особенно если шаблон вычисляется во время выполнения. Поэтому проверка инструментом дополняет проверку готового сообщения.
Пользовательский текст передавайте как значение, если не хотите интерпретировать проценты как формат:
input := "Скидка 10%"
fmt.Print(input)
fmt.Printf("%s\n", input)
В обоих случаях % внутри input остаётся частью текста. Printf разбирает только свой первый аргумент, а содержимое подставляемой строки повторно как шаблон не обрабатывает.
Итог
Выбор функции начинается с места назначения: Printf пишет в стандартный вывод, Sprintf возвращает строку, Fprintf пишет в переданный io.Writer. Для простой печати с переводом строки достаточно Println.
Выбирайте формат по задаче: %d для целого, %.2f для двух знаков после точки, %q для проверки границ строки. При отладке структуры начните с %+v, но помните о методе String(). Ширина дополняет короткое значение, а точность меняет представление по правилам конкретного типа. Если в результате появилось %!, сначала сопоставьте шаблон с аргументами.
Теги: