Язык Go — fmt: форматированный вывод
Пакет fmt в Go — Printf, глаголы форматирования, интерфейс Stringer, рекурсия в String() и вариадические функции.
Содержание
Пакет fmt в Go похож на printf из C, но устроен богаче. Функции с заглавной буквой: fmt.Printf, fmt.Fprintf, fmt.Sprintf — и каждая имеет вариант без строки формата.
Printf, Fprintf, Sprintf — и их «принт»-варианты
Для каждой из трёх функций есть пара без строки формата:
fmt.Printf("Hello %d\n", 23) // в os.Stdout, с форматом
fmt.Fprint(os.Stdout, "Hello ", 23, "\n") // в io.Writer, без формата
fmt.Println("Hello", 23) // в os.Stdout, без формата + перенос
fmt.Println(fmt.Sprint("Hello ", 23)) // Sprint возвращает строку
// все четыре строки печатают одно и то же: Hello 23
Fprint/Fprintf/Fprintln принимают любой io.Writer первым аргументом — os.Stdout, os.Stderr, файл, буфер. Sprintf возвращает строку, не пишет в буфер.
Разница между Print и Println: Println всегда вставляет пробел между аргументами и добавляет \n. Print добавляет пробел только если ни один из соседних аргументов не является строкой.
Числовые форматы не требуют флагов знака и размера
В C для вывода числа нужно указывать %u, %ld и так далее. В Go тип аргумента определяет поведение сам:
var x uint64 = 1<<64 - 1 // максимальное значение uint64
fmt.Printf("%d %x; %d %x\n", x, x, int64(x), int64(x))
// 18446744073709551615 ffffffffffffffff; -1 -1
// uint64 печатается как беззнаковое, int64 — как знаковое
Глагол %v — универсальный формат
%v печатает значение в формате по умолчанию. Работает с любым типом — числом, строкой, слайсом, структурой, map:
// timeZone определён как map[string]int{"UTC": 0, "EST": -18000, ...}
fmt.Printf("%v\n", timeZone)
// map[CST:-21600 EST:-18000 MST:-25200 PST:-28800 UTC:0]
// fmt сортирует ключи map лексикографически при выводе
Форматы для структур: %v, %+v, %#v
Три варианта для вывода структур — каждый даёт чуть больше информации:
type T struct {
a int
b float64
c string
}
t := &T{7, -2.35, "abc\tdef"}
fmt.Printf("%v\n", t) // &{7 -2.35 abc def} — значения полей
fmt.Printf("%+v\n", t) // &{a:7 b:-2.35 c:abc def} — имена + значения
fmt.Printf("%#v\n", t) // &main.T{a:7, b:-2.35, c:"abc\tdef"} — Go-синтаксис
fmt.Printf("%#v\n", timeZone)
// map[string]int{"CST":-21600, "EST":-18000, "MST":-25200, "PST":-28800, "UTC":0}
// %#v для map тоже даёт валидный Go-синтаксис
Обратите внимание на & — t это указатель, %v это учитывает.
Остальные полезные глаголы
s := "hello"
b := []byte("world")
r := '⌘'
fmt.Printf("%q\n", s) // "hello" — строка в кавычках с эскейпами
fmt.Printf("%q\n", r) // '⌘' — руна в одинарных кавычках
fmt.Printf("%#q\n", s) // `hello` — бэктики, если возможно
fmt.Printf("%x\n", s) // 68656c6c6f — hex-дамп строки
fmt.Printf("%x\n", b) // 776f726c64 — hex-дамп слайса байт
fmt.Printf("% x\n", b) // 77 6f 72 6c 64 — hex с пробелами между байтами
fmt.Printf("%T\n", timeZone) // map[string]int — тип значения
fmt.Printf("%T\n", t) // *main.T
Интерфейс Stringer: свой формат для своего типа
Если тип реализует метод String() string, fmt будет использовать его при выводе через %v и %s:
type T struct {
a int
b float64
c string
}
func (t *T) String() string {
return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c)
}
t := &T{7, -2.35, "abc\tdef"}
fmt.Printf("%v\n", t) // 7/-2.35/"abc\tdef"
Ресивер — указатель, потому что для структур это эффективнее и идиоматичнее. Если нужно печатать и T, и *T, ресивер должен быть значением.
Ловушка: бесконечная рекурсия в String()
Если внутри String() вызвать Sprintf с тем же типом без конвертации — получим бесконечную рекурсию:
type MyString string
// НЕПРАВИЛЬНО — бесконечная рекурсия:
// Sprintf пытается напечатать m через %s,
// это вызывает String(), которая снова вызывает Sprintf — и так до переполнения стека
func (m MyString) String() string {
return fmt.Sprintf("MyString=%s", m)
}
// ПРАВИЛЬНО — явная конвертация в базовый тип string:
// string(m) не имеет метода String(), рекурсии нет
func (m MyString) String() string {
return fmt.Sprintf("MyString=%s", string(m))
}
Правило простое: если тип имеет метод String(), не передавайте его в Sprintf с глаголом %s или %v без явной конвертации в базовый тип.
Вариадические функции и проброс аргументов
Printf принимает произвольное количество аргументов через ...interface{}:
// сигнатура Printf:
func Printf(format string, v ...interface{}) (n int, err error)
Внутри функции v ведёт себя как []interface{}. Чтобы пробросить его в другую вариадическую функцию — используем ... при вызове:
// реализация log.Println — проброс аргументов в fmt.Sprintln
func Println(v ...interface{}) {
std.Output(2, fmt.Sprintln(v...)) // v... разворачивает слайс в список аргументов
// без ... передали бы v как один аргумент типа []interface{}
}
Вариадик может быть конкретного типа, не только interface{}:
// функция находит минимум среди произвольного числа int
func Min(a ...int) int {
min := int(^uint(0) >> 1) // максимальное значение int
for _, i := range a {
if i < min {
min = i
}
}
return min
}
fmt.Println(Min(3, 1, 4, 1, 5, 9)) // 1
Итоги
Printf/Fprintf/Sprintf— с форматом;Print/Fprint/Sprint— без;Printlnдобавляет\n- Числовые глаголы (
%d,%x) не требуют флагов знака — тип аргумента определяет поведение %v— универсальный формат для любого типа;%+vдобавляет имена полей;%#v— Go-синтаксис%q— строка в кавычках с эскейпами;%T— тип значения;% x— hex с пробелами- Реализуйте
String() stringдля кастомного форматирования своих типов - В
String()не передавайте receiver вSprintfбез конвертации в базовый тип — бесконечная рекурсия - Вариадические аргументы проксируются через
v...— без...слайс передаётся как один аргумент