Форматування через 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 адреса, яка відрізняється щоразу.
Рядки, лапки та руни¶
%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.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 |
обрізати рядок |