Перейти до змісту

Форматування через fmt

fmt.Println супроводжував кожен приклад дотепер. Решта пакета — це невелика мова форматування, і два її дієслова — %+v для налагодження та %w для обгортання помилок — трапляються постійно.

type Point struct {
    X, Y int
    name string
}

p := Point{X: 1, Y: 2, name: "origin"}

fmt.Printf("%v\n", p)    // output: {1 2 origin}
fmt.Printf("%+v\n", p)   // output: {X:1 Y:2 name:origin}

Чотири функції

Кожна функція форматування — одна з цих, з префіксом, що вказує, куди йде результат:

Префікс Куди йде Приклад
(немає) стандартний вивід fmt.Printf("%d\n", n)
S повертається як string s := fmt.Sprintf("%d", n)
F у переданий io.Writer fmt.Fprintf(w, "%d", n)
Errorf повертається як error fmt.Errorf("bad %d", n)

Print записує свої аргументи, Println додає пробіли та символ нового рядка, Printf приймає рядок формату. Саме Fprintf робить пакет композиційним — вона пише в будь-що, що реалізує io.Writer, а це і os.Stdout, і файл, і HTTP-відповідь, і strings.Builder.

%v, %+v, %#v і %T

%v — це рендеринг будь-якого значення за замовчуванням. Інші три потрібні, коли ви намагаєтесь з'ясувати, що ж у вас насправді є:

fmt.Printf("%v\n", p)    // output: {1 2 origin}
fmt.Printf("%+v\n", p)   // output: {X:1 Y:2 name:origin}
fmt.Printf("%#v\n", p)   // output: main.Point{X:1, Y:2, name:"origin"}
fmt.Printf("%T\n", p)    // output: main.Point

%+v додає назви полів і саме до нього тягнуться в рядку логу чи швидкому налагоджувальному друку. %#v друкує синтаксис Go, який можна вставити назад у код. %T друкує тип — так ви дізнаєтесь, що насправді всередині any.

Складені значення вкладаються природно, і ключі мапи друкуються у відсортованому порядку, тож вивід відтворюваний:

fmt.Printf("%v\n", []int{1, 2})                    // output: [1 2]
fmt.Printf("%v\n", map[string]int{"b": 2, "a": 1}) // output: map[a:1 b:2]
fmt.Printf("%v\n", &p)                             // output: &{1 2 origin}

Вказівник на структуру друкується як &{...}. Вказівник на будь-що інше друкується як шістнадцятковa адреса, яка відрізняється щоразу.

Рядки, лапки та руни

s := "hi\tthere"
fmt.Printf("%s|%q\n", s, s)   // output: hi there|"hi\tthere"

%q додає лапки й екранування — незамінна річ, коли питання таке: «чи є в цьому рядку кінцеві пробіли або табуляція?» — на що %s відповісти не може. Саме тому стаття про рядки друкує через %q.

rune — це ціле число, тож %v показує число. %q показує символьний літерал, %c — сам символ, %U — кодову точку:

fmt.Printf("%v %q\n", 'A', 'A')        // output: 65 'A'
fmt.Printf("%c %U\n", 0x4e16, 0x4e16)  // output: 世 U+4E16

Числа

fmt.Printf("%d %b %o %x %X\n", 255, 255, 255, 255, 255)
// output: 255 11111111 377 ff FF

fmt.Printf("%f %.2f %e %g\n", 3.14159, 3.14159, 3.14159, 3.14159)
// output: 3.141590 3.14 3.141590e+00 3.14159

fmt.Printf("%t\n", true)   // output: true

%f завжди друкує шість десяткових знаків. %g обирає найкоротше представлення, що при зворотному парсингу дасть те саме число — це зазвичай те, що потрібно, коли порядок величини наперед невідомий.

Ширина та точність

Число між % і дієсловом задає мінімальну ширину; .n задає точність. Мінус вирівнює за лівим краєм, ведучий нуль доповнює нулями:

fmt.Printf("[%5d][%-5d][%05d]\n", 42, 42, 42)
// output: [   42][42   ][00042]

fmt.Printf("[%8.3f][%-8.3f]\n", 3.14159, 3.14159)
// output: [   3.142][3.142   ]

Для рядка точність обрізає:

fmt.Printf("[%6s][%-6s][%.3s]\n", "go", "go", "golang")
// output: [    go][go    ][gol]

* бере ширину зі списку аргументів — для випадків, коли вона обчислюється:

fmt.Printf("%*d\n", 6, 42)   // output:     42

fmt.Stringer

Тип з методом String() string керує власним рендерингом для %v і %s, і Println теж це враховує:

type Temp float64

func (t Temp) String() string { return fmt.Sprintf("%.1f°C", float64(t)) }

fmt.Printf("%v\n", Temp(21.456))   // output: 21.5°C
fmt.Println(Temp(21.456))          // output: 21.5°C

Пастка в методі String — форматувати отримувача дієсловом, схожим на рядкове. fmt звертається до String лише для %v, %s і споріднених з ними, тож ось цей код рекурсивно викликає сам себе, поки не скінчиться стек:

func (t Temp) String() string { return fmt.Sprintf("%v°C", t) }
// go vet: fmt.Sprintf format %v with arg t causes recursive
//         (main.Temp).String method call

Числові дієслова безпечні — %.1f читає число і ніколи не викликає String — тож конвертація float64(t) вище радше для ясності, ніж з необхідності. У сумнівних випадках конвертуйте в базовий тип: це правильно для будь-якого дієслова, а go vet позначає випадки, де це насправді важливо.

%w належить лише Errorf

%w нічого не форматує — вона записує помилку для errors.Is та errors.As, як розглядає стаття про помилки. Візуально %w і %v однакові:

err := errors.New("disk full")
wrapped := fmt.Errorf("saving: %w", err)

fmt.Printf("%v | %s\n", wrapped, wrapped)
// output: saving: disk full | saving: disk full

fmt.Println(errors.Is(wrapped, err))   // output: true

%w розуміє лише Errorf. Передача її в Printf дає таку неприємність.

Коли дієслово й аргумент не збігаються

fmt ніколи не панікує через неузгодженість. Вона записує проблему в результат, а це означає, що зламаний рядок формату з'явиться у ваших логах, а не завалить процес:

fmt.Printf("%d %s\n", 1)      // output: 1 %!s(MISSING)
fmt.Printf("%d\n", "nope")    // output: %!d(string=nope)

go vet ловить обидва випадки під час збірки, і він запускається як частина go test.

З досвіду Python: %v — це str(), а %#v близький до repr(). Sprintf — це %-форматування або f"...", але дієслова тут типізовані — f-рядку в Python байдуже, що ви йому передаєте, тоді як %d з рядком — помилка, на яку вкаже go vet.

Швидка довідка

Дієслово Використання
%v форма будь-якого значення за замовчуванням
%+v структура з назвами полів — типовий варіант для налагодження
%#v синтаксис Go
%T тип
%s / %q рядок / рядок у лапках з екрануванням
%d %b %o %x ціле число за основою 10, 2, 8, 16
%f %.2f %e %g float: фіксований, 2 знаки, науковий, найкоротший
%c / %U руна як символ / як U+XXXX
%t bool
%w обгорнути помилку — лише fmt.Errorf
%5d %-5d %05d ширина, вирівнювання ліворуч, доповнення нулями
%.3s обрізати рядок

Джерела