Как правильно дополнять числа нулями в Go: гайд по форматированию

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Вечная проблема: «0» слева или «сдвиг вправо»?

Вы когда-нибудь задумывались, почему в SQL-запросах номера заказов выглядят как 00042, а в логах — как 23:05:07? Всё просто: числа, дополненные нулями, выравниваются по ширине и не ломают сортировку. Но в Go, в отличие от Python или JavaScript, нет встроенного метода zfill. И тут начинается самое интересное.

На Stack Overflow вопрос «How to pad a number with zeros when printing?» набрал тысячи просмотров и десятки ответов. Люди ищут не только решение, но и быстрое решение. В этой статье мы разберём все способы дополнить число нулями в Go, сравним их производительность и выберем лучший для вашего проекта.

Почему это важно для Go-разработчика?

Go — язык для системного программирования, где каждая наносекунда на счету. Если вы пишете высоконагруженный сервис, неправильное форматирование строк может стать узким местом. Особенно если вы форматируете тысячи чисел в секунду.

  • Логирование: временные метки, ID запросов, коды ошибок.
  • Генерация отчётов: CSV, JSON, таблицы.
  • Сетевые протоколы: фиксированная длина полей.
  • Игры и графика: выравнивание текста в консоли.

Игнорирование производительности может привести к тому, что ваш сервис будет тратить 10% времени на форматирование чисел. Не верите? Давайте проверим.

Способ №1: Классика через fmt.Sprintf

Самый очевидный и часто рекомендуемый способ — использовать fmt.Sprintf с форматным спецификатором %0Xd, где X — общая ширина числа вместе с нулями.

package main

import "fmt"

func main() {
    num := 42
    padded := fmt.Sprintf("%05d", num)
    fmt.Println(padded) // Вывод: 00042
}

Здесь %05d означает: «выведи число как десятичное целое, заполни слева нулями до ширины 5 символов». Если число уже длиннее (например, 123456), то нули не добавляются — ширина игнорируется.

Плюсы:

  • Просто и читаемо.
  • Поддерживает отрицательные числа (знак будет слева от нулей).
  • Работает с разными типами: %05d для int, %05.2f для float.

Минусы:

  • Использует рефлексию внутри fmt — медленно.
  • Создаёт лишнюю строку и interface{}.
  • Не подходит для горячих путей.

Способ №2: Быстрый strconv.FormatInt с ручным дополнением

Если производительность критична, используйте пакет strconv. Он работает напрямую с байтами, без рефлексии.

package main

import (
    "fmt"
    "strconv"
    "strings"
)

func padNumber(num int64, width int) string {
    s := strconv.FormatInt(num, 10)
    if len(s) >= width {
        return s
    }
    return strings.Repeat("0", width-len(s)) + s
}

func main() {
    fmt.Println(padNumber(42, 5)) // 00042
}

Плюсы:

  • В 2-3 раза быстрее fmt.Sprintf.
  • Нет рефлексии.
  • Полный контроль над логикой.

Минусы:

  • Кода больше.
  • Нужно обрабатывать отрицательные числа отдельно (знак слева от нулей).
  • strings.Repeat создаёт новую строку — можно оптимизировать дальше.

Способ №3: Максимальная производительность через strings.Builder

Для тех, кто хочет выжать все соки, есть способ без лишних аллокаций — использовать strings.Builder и заранее выделять буфер.

package main

import (
    "fmt"
    "strconv"
    "strings"
)

func padNumberFast(num int64, width int) string {
    s := strconv.FormatInt(num, 10)
    if len(s) >= width {
        return s
    }
    var builder strings.Builder
    builder.Grow(width)
    for i := 0; i < width-len(s); i++ {
        builder.WriteByte('0')
    }
    builder.WriteString(s)
    return builder.String()
}

func main() {
    fmt.Println(padNumberFast(42, 5)) // 00042
}

Но можно пойти ещё дальше — полностью отказаться от strconv и написать свою функцию конвертации числа в строку с дополнением нулями. Это даст прирост ещё на 10-20%, но код станет сложнее.

Совет: Если вам нужно форматировать много чисел в одном месте, используйте []byte и работайте с буфером напрямую.

Бенчмарки: кто быстрее?

Давайте напишем простой бенчмарк и сравним три подхода.

package main

import (
    "fmt"
    "strconv"
    "strings"
    "testing"
)

func padSprintf(num int64, width int) string {
    return fmt.Sprintf("%05d", num)
}

func padStrconv(num int64, width int) string {
    s := strconv.FormatInt(num, 10)
    if len(s) >= width {
        return s
    }
    return strings.Repeat("0", width-len(s)) + s
}

func padBuilder(num int64, width int) string {
    s := strconv.FormatInt(num, 10)
    if len(s) >= width {
        return s
    }
    var builder strings.Builder
    builder.Grow(width)
    for i := 0; i < width-len(s); i++ {
        builder.WriteByte('0')
    }
    builder.WriteString(s)
    return builder.String()
}

func BenchmarkPadSprintf(b *testing.B) {
    for i := 0; i < b.N; i++ {
        padSprintf(42, 5)
    }
}

func BenchmarkPadStrconv(b *testing.B) {
    for i := 0; i < b.N; i++ {
        padStrconv(42, 5)
    }
}

func BenchmarkPadBuilder(b *testing.B) {
    for i := 0; i < b.N; i++ {
        padBuilder(42, 5)
    }
}

Результаты (на моём ноутбуке):

  • fmt.Sprintf — ~150 нс/операция
  • strconv + Repeat — ~80 нс/операция
  • strings.Builder — ~70 нс/операция

Разница в два раза между первым и последним. Для 100 000 операций это 8 миллисекунд против 15. Не критично для одного вызова, но в цикле — заметно.

Особые случаи: отрицательные числа, float, hex

Форматирование с нулями для отрицательных чисел требует аккуратности. Например, fmt.Sprintf("%05d", -42) вернёт -0042

Блоги

Рекомендаций по книгам