Урок 41 (бонус): Пакет fmt — форматирование, методы и лучшие практики

Урок 41 (бонус). Пакет fmt — полный разбор

🔄 Node.js → Go (аналогии с console):

📋 Что изучаем


1. Обзор пакета fmt

Пакет fmt — аналог console + util.format в Node.js. Он реализует форматированный I/O с помощью функций, разделённых на три семейства:

СемействоВыводСигнатура
PrintstdoutPrint(a...any), Printf(format, a...), Println(a...)
Fprintлюбой io.WriterFprint(w, a...), Fprintf(w, format, a...), Fprintln(w, a...)
Sprintвозвращает строкуSprint(a...), Sprintf(format, a...), Sprintln(a...)
💡 Практический совет:

Правило запоминания: Первая буква — куда выводим (P = stdout, F = File/Writer, S = String). Суффикс — формат (f = format, ln = newline, без суффикса = значения через пробел).

1.1 Семейство Print — вывод в stdout

name := "Алиса"
age := 30

fmt.Print("Hello, ")         // Hello,
fmt.Print(name, "!\n")       // Hello, Алиса!
fmt.Println("Age:", age)     // Age: 30 (с переносом строки)
fmt.Printf("%s is %d years old\n", name, age) // Алиса is 30 years old

1.2 Семейство Fprint — вывод в io.Writer

Используется для записи в файлы, HTTP-ответы, буферы — любой объект, реализующий io.Writer.

var buf bytes.Buffer
fmt.Fprintf(&buf, "User: %s, Score: %d", name, score)
// buf.String() → "User: Алиса, Score: 42"

// Запись в файл
f, _ := os.Create("output.txt")
defer f.Close()
fmt.Fprintln(f, "лог-строка")  // эквивалент f.WriteString("лог-строка\n")

Аналог в Node.js: fs.writeFileSync('file.txt', util.format(...))

1.3 Семейство Sprint — возврат строки

s := fmt.Sprintf("Hello, %s! You have %d messages.", name, count)
// s = "Hello, Алиса! You have 5 messages."

Используется для построения сообщений, SQL-запросов (осторожно с инъекциями!), шаблонов.


2. Вербо-форматирование (%verbs)

2.1 Основные вербо

ВербоНазначениеПримерРезультат
%vЗначение (value) по умолчаниюfmt.Printf("%v", user){Алиса 30}
%+vЗначение с именами полейfmt.Printf("%+v", user){Name:Алиса Age:30}
%#vGo-синтаксис представленияfmt.Printf("%#v", user)main.User{Name:"Алиса", Age:30}
%TТип значенияfmt.Printf("%T", user)main.User
%tboolfmt.Printf("%t", true)true
%dДесятичное целоеfmt.Printf("%d", 42)42
%bДвоичноеfmt.Printf("%b", 42)101010
%x / %XШестнадцатеричноеfmt.Printf("%x", 255)ff / FF
%fЧисло с плавающей точкойfmt.Printf("%f", 3.14)3.140000
%.2fС точностьюfmt.Printf("%.2f", 3.1415)3.14
%sСтрокаfmt.Printf("%s", "hello")hello
%qСтрока в кавычкахfmt.Printf("%q", "hello")"hello"
%pУказательfmt.Printf("%p", &x)0xc0000b2000
%%Литерал процентаfmt.Printf("100%%")100%

2.2 Примеры со структурами

type User struct {
    Name string
    Age  int
}

u := User{"Боб", 25}

fmt.Printf("1: %v\n", u)    // 1: {Боб 25}
fmt.Printf("2: %+v\n", u)   // 2: {Name:Боб Age:25}
fmt.Printf("3: %#v\n", u)   // 3: main.User{Name:"Боб", Age:25}
fmt.Printf("4: %T\n", u)    // 4: main.User

2.3 Ширина и выравнивание

fmt.Printf("|%10s|%10s|\n", "Name", "Age")     // |      Name|       Age|
fmt.Printf("|%-10s|%-10s|\n", "Name", "Age")    // |Name      |Age       |
fmt.Printf("|%10d|%10d|\n", 42, 7)              // |        42|         7|

Аналог в Node.js: padStart/padEnd или util.format с %10s.

2.4 Форматирование чисел

n := 1234567.89
fmt.Printf("%%.2f:  %.2f\n", n)      // 1234567.89
fmt.Printf("%,.2f:  %,.2f\n", n)    // 1,234,567.89 (Windows)
fmt.Printf("%%10.2f: %10.2f\n", n)  // " 1234567.89"
fmt.Printf("%%e:     %e\n", n)      // 1.234568e+06

3. Интерфейс Stringer и кастомное форматирование

3.1 Интерфейс fmt.Stringer

Это аналог toString() в JavaScript/Java. Определяет, как объект выводится через %v, %s, Println.

type Stringer interface {
    String() string
}
type User struct {
    Name string
    Age  int
}

// String реализует fmt.Stringer
func (u User) String() string {
    return fmt.Sprintf("👤 %s (%d лет)", u.Name, u.Age)
}

func main() {
    u := User{"Алиса", 30}
    fmt.Println(u)          // 👤 Алиса (30 лет)
    fmt.Printf("%v\n", u)   // 👤 Алиса (30 лет)
    fmt.Printf("%s\n", u)   // 👤 Алиса (30 лет)
}

3.2 Интерфейс GoStringer

Используется с %#v. Позволяет контролировать, как тип выглядит в Go-синтаксисе.

func (u User) GoString() string {
    return fmt.Sprintf("User{Name: %q, Age: %d}", u.Name, u.Age)
}

fmt.Printf("%#v", u) // User{Name: "Алиса", Age: 30}

3.3 Интерфейс Formatter (продвинутый)

Позволяет полностью контролировать форматирование для всех вербо.

type User struct{ Name string }

func (u User) Format(f fmt.State, verb rune) {
    switch verb {
    case 'v':
        if f.Flag('+') {
            fmt.Fprint(f, "User("+u.Name+")")
            return
        }
        fmt.Fprint(f, u.Name)
    case 's':
        fmt.Fprint(f, u.Name)
    case 'q':
        fmt.Fprintf(f, "%q", u.Name)
    }
}

4. Форматирование ошибок: fmt.Errorf и %w

4.1 Базовое создание ошибок

err := fmt.Errorf("ошибка подключения к БД: %s", dbName)

4.2 Wrap-ошибки с %w (Go 1.13+)

Позволяет создавать цепочки ошибок, которые можно разворачивать через errors.Is и errors.As.

import "errors"

var ErrNotFound = errors.New("запись не найдена")

func GetUser(id int) (User, error) {
    // ... запрос в БД
    return User{}, fmt.Errorf("GetUser(%d): %w", id, ErrNotFound)
}

func main() {
    _, err := GetUser(42)
    fmt.Println(err)                    // GetUser(42): запись не найдена
    fmt.Println(errors.Is(err, ErrNotFound)) // true
}

4.3 Множественные %w — только один

// ⚠️ НЕЛЬЗЯ использовать два %w — скомпилируется, но работать будет неправильно
err := fmt.Errorf("A: %w, B: %w", err1, err2) // работает только err1

Для множественной обёртки используйте errors.Join (Go 1.20+).


5. Сканирование ввода: fmt.Scan, fmt.Sscanf

5.1 Чтение из stdin

var name string
var age int

fmt.Print("Введите имя и возраст: ")
n, err := fmt.Scan(&name, &age)
if err != nil {
    fmt.Println("Ошибка ввода:", err)
    return
}
fmt.Printf("Прочитано %d значений: %s, %d\n", n, name, age)

5.2 Разбор строки (Sscanf)

Аналог sscanf из C. Разбирает строку по шаблону.

input := "Алиса 30 65.5"
var name string
var age int
var weight float64

fmt.Sscanf(input, "%s %d %f", &name, &age, &weight)
fmt.Printf("Name: %s, Age: %d, Weight: %.1f\n", name, age, weight)
// Name: Алиса, Age: 30, Weight: 65.5
💡 Практический совет:

⚠️ Важно: fmt.Scan и fmt.Sscanf останавливаются на пробелах. Для построчного чтения используйте bufio.Scanner.


6. Полезные функции-хелперы

6.1 fmt.Fscanln — чтение строки с пробелами

var line string
fmt.Scanln(&line) // читает до новой строки, включая пробелы

6.2 fmt.Fscan — чтение из любого reader

reader := strings.NewReader("hello 42")
var s string
var n int
fmt.Fscan(reader, &s, &n) // s="hello", n=42

6.3 Проверка реализации интерфейса

var _ fmt.Stringer = (*User)(nil) // compile-time check

7. Практические паттерны

7.1 Логирование с контекстом

func Log(level, msg string, fields ...any) {
    fmt.Fprintf(os.Stderr, "[%s] %s | fields=%+v\n", level, msg, fields)
}

Log("ERROR", "БД недоступна", "db", "pgx", "timeout", "5s")
// [ERROR] БД недоступна | fields=[db:pgx timeout:5s]

7.2 Pretty-print структуры для отладки

func Debug(v any) {
    fmt.Fprintf(os.Stderr, "--- DEBUG ---\n%+v\n--- END ---\n", v)
}

7.3 Форматирование SQL-запросов (осторожно!)

query := fmt.Sprintf("SELECT * FROM users WHERE id = %d", userID)
// ⚠️ Если userID пришёл от пользователя — SQL injection!
// Для БД используйте placeholders: $1, $2

7.4 Текстовые таблицы

func PrintTable(header []string, rows [][]string) {
    // Формирование строки с шириной
    line := strings.Repeat("+", 40)
    fmt.Println(line)
    for _, h := range header {
        fmt.Printf("| %-15s", h)
    }
    fmt.Println("|")
    for _, row := range rows {
        for _, cell := range row {
            fmt.Printf("| %-15s", cell)
        }
        fmt.Println("|")
    }
}

8. Сравнение с Node.js (шпаргалка)

ЗадачаNode.jsGo
Вывод в консольconsole.log(x)fmt.Println(x)
Форматированная строкаutil.format("x=%d", n)fmt.Sprintf("x=%d", n)
Вывод объектаconsole.log(obj)fmt.Printf("%+v\n", obj)
Deep pretty-printconsole.dir(obj, {depth: null})spew.Dump(obj)
Запись в файлfs.writeFileSync(f, str)fmt.Fprintln(f, str)
toString()obj.toString()func (o Obj) String() string
Ошибка с контекстомnew Error("msg: " + ctx)fmt.Errorf("msg: %w", err)
Парсинг строкиsscanf (нет встроенного)fmt.Sscanf(s, "%d", &n)

🔑 Ключевые выводы

← Предыдущий урок Следующий урок →