· 11 мин 👁 1.6k Начинающий

Язык Go — fmt: форматированный вывод

Пакет fmt в Go — Printf, глаголы форматирования, интерфейс Stringer, рекурсия в String() и вариадические функции.

fmtprintfstringerформатированиевариадик
Содержание

Пакет 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... — без ... слайс передаётся как один аргумент