JavaRush /Курсы /Go SELF /Округление и форматирование чисел в Go

Округление и форматирование чисел в Go

Go SELF
7 уровень , 5 лекция
Открыта

1. Введение

Почти все новички (и некоторые опытные тоже, особенно по пятницам) путают две вещи: округление как математическую операцию и округление как формат вывода. Снаружи выглядит одинаково — “хочу 2 знака после точки” — но внутри это разные действия и разные последствия для программы.

Представьте, что число — это еда, а вывод — это тарелка. Вы можете нарезать еду (изменить число), а можете красиво разложить на тарелке (изменить только отображение). Вторая операция не меняет саму еду — только то, как вы её показываете.

В Go:

  • форматирование через fmt.Printf("%.2f", x) не меняет x;
  • математическое округление через math.Round(x) возвращает новое значение.

Это различие — главный смысл лекции.

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

Очень полезно один раз в жизни (желательно пораньше) увидеть глазами, что форматирование не меняет переменную. Иначе можно нечаянно построить логику программы на красивом выводе — а это примерно как строить дом по фотографии кирпича.

Сделаем простую проверку: выведем число “красиво”, а потом выведем его же с высокой точностью, чтобы увидеть, что оно не стало “короче”.

package main

import "fmt"

func main() {
	x := 2.0 / 3.0

	fmt.Printf("Pretty: %.2f\n", x)   // Pretty: 0.67
	fmt.Printf("Raw:    %.17f\n", x)  // Raw:    0.66666666666666663
	fmt.Printf("Still x: %.17f\n", x) // Still x: 0.66666666666666663
}

Если от этого примера у вас появляется лёгкое чувство “меня обманули” — поздравляю, вы начали понимать float. Это не ошибка Go и не заговор математиков. В двоичной системе не все десятичные дроби представимы точно, поэтому появляются хвосты.

2. Форматирование float в fmt.Printf

Когда вы печатаете float64, по умолчанию Go старается вывести число “разумно”, но это не всегда то, что нужно человеку. Поэтому fmt.Printf даёт вам управление: сколько знаков после точки показывать, какой формат выбрать, как печатать число в отчёте, и так далее.

Что означает %.2f

Начнём с самого популярного: %f и его “красивая версия” %.2f.

  • %f печатает число с плавающей точкой в десятичном виде.
  • %.2f печатает ровно 2 знака после точки, округляя для вывода.
  • %.0f печатает без дробной части (опять же округляя для вывода).
  • %.3f — три знака после точки, и так далее.

Посмотрим на простом примере.

package main

import "fmt"

func main() {
	x := 1.0 / 3.0

	fmt.Printf("x as %%f   = %f\n", x)   // x as %f   = 0.333333
	fmt.Printf("x as %%.2f = %.2f\n", x) // x as %.2f = 0.33
	fmt.Printf("x as %%.0f = %.0f\n", x) // x as %.0f = 0
}

Обратите внимание на две тонкости.

Первая: в строке формата %% — это способ вывести буквальный символ %. Иначе fmt будет думать, что вы снова начинаете форматирование.

Вторая: x не изменился. Он как был “одна треть” (примерно), так и остался. Мы просто показали его по-разному.

Другие полезные форматы: %.0f и %g

Когда начинаешь печатать числа, очень быстро появляется вопрос: “А всегда ли %.2f хорош?” Ответ — нет. Он хороший для денег и для “человеческих” отчётов, но иногда вам нужно другое.

Важно не выучить все спецификаторы наизусть (это путь к грусти), а понимать идею: формат выбирается под смысл вывода.

Небольшая таблица, которую полезно держать в голове:

Формат Идея Типичный смысл
%f
“обычное” вещественное число технический вывод, отладка
%.2f
фиксированная точность после точки деньги, проценты, отчёты
%.0f
без дроби “штук”, “человек”, “целых единиц”, но число всё ещё float
%g
“компактно”: либо обычный вид, либо экспонента диагностика, когда масштабы разные
%.3g
значащие цифры (не “после точки”) компактные отчёты, научные числа

Пощупаем %g на коротком примере: он часто выглядит “умнее”, когда числа бывают и маленькие, и большие.

package main

import "fmt"

func main() {
	a := 12345.6789
	b := 0.000012345

	fmt.Printf("a with %%f  = %f\n", a)    // a with %f  = 12345.678900
	fmt.Printf("a with %%g  = %g\n", a)    // a with %g  = 12345.6789
	fmt.Printf("b with %%g  = %g\n", b)    // b with %g  = 1.2345e-05
	fmt.Printf("a with %%.3g = %.3g\n", a) // a with %.3g = 1.23e+04
}

Если вы делаете “человеческий чек” — обычно лучше %.2f. Если вы печатаете диагностический вывод — %g иногда понятнее, потому что не заваливает вас лишними нулями.

Мини-приложение: калькулятор чаевых

Сейчас соберём небольшой кусочек в стиле нашего курса: программа читает ввод, что-то считает и печатает результат. Никакой архитектуры, никаких классов, только честный main и понятные переменные. Это важно: мы тренируем навык “я могу собрать полезную штуку из базовых деталей”.

Представим, что мы пишем мини-калькулятор чаевых:

  • сумма счёта (вещественное число),
  • процент чаевых (целое число, например 10 или 15),
  • итоговая сумма к оплате.

Печатать будем как деньги: %.2f.

package main

import "fmt"

func main() {
	var bill float64
	var tipPercent int

	fmt.Scan(&bill, &tipPercent)

	tip := bill * float64(tipPercent) / 100
	total := bill + tip

	fmt.Printf("Tip:   %.2f\n", tip)   // Tip:   3.75   (пример)
	fmt.Printf("Total: %.2f\n", total) // Total: 28.75  (пример)
}

Здесь мы используем сразу несколько вещей, которые “срастаются” в один навык.

Во-первых, fmt.Scan спокойно читает float64 и int, если ввод корректный.

Во-вторых, мы явно приводим tipPercent к float64, потому что иначе выражение bill * tipPercent не скомпилируется (типы-то разные, магии нет).

В-третьих, %.2f делает вывод таким, как привык видеть человек. И да, именно по этой причине %.2f так любят в примерах про деньги: не потому что “так принято”, а потому что это минимальный порядок.

3. Округление значения через пакет math

Иногда вам недостаточно “красивого вывода”. Например, вы хотите:

  • посчитать сумму и дальше использовать округлённое значение в следующих вычислениях;
  • сделать правило “округляем вверх” (например, “доставка всегда округляется до целых евро вверх”);
  • отрезать дробную часть вниз и получить “целое количество упаковок”.

В таких случаях вам нужны функции из пакета math.

Round, Floor, Ceil

В базовом наборе три штуки закрывают большинство бытовых сценариев:

  • math.Round(x) — округление к ближайшему целому;
  • math.Floor(x) — округление вниз (к минус бесконечности);
  • math.Ceil(x) — округление вверх (к плюс бесконечности).

Посмотрим на примере.

package main

import (
	"fmt"
	"math"
)

func main() {
	fmt.Println(math.Round(2.5)) // 3
	fmt.Println(math.Floor(2.9)) // 2
	fmt.Println(math.Ceil(2.1))  // 3
}

Это уже изменение значения, точнее — получение нового значения. Переменная сама по себе не меняется (потому что мы её не меняли), но результат функции — это новое число, которое можно присваивать, складывать, сохранять.

Округление до двух знаков как значение: “масштабирование → Round → обратно”

Форматирование %.2f печатает две цифры после точки, но само значение не меняет. А если вы хотите именно значение с двумя знаками (например, сохранить “сумму после скидки” уже округлённой), то часто делают так:

  1. умножаем на 100,
  2. округляем до целого через math.Round,
  3. делим обратно на 100.

Пример:

package main

import (
	"fmt"
	"math"
)

func main() {
	x := 12.3456
	y := math.Round(x*100) / 100

	fmt.Printf("x = %.10f\n", x) // x = 12.3456000000
	fmt.Printf("y = %.10f\n", y) // y = 12.3500000000
}

Почему это работает? Потому что мы превращаем задачу “округлить до 2 знаков” в задачу “округлить до целого”, а это math.Round умеет напрямую.

Сразу важная ремарка: мы работаем с float, а значит “идеальной финансовой математики” тут нет. Но на нашем учебном уровне это нормальный и понятный инструмент: вы явно видите формулу и понимаете, что происходит.

4. Где делать округление в программе

Когда программа начинает считать что-то длиннее “два плюс два”, место округления становится важным. Если округлять слишком рано, можно накопить ошибку. Если не округлять вообще — пользователю будет больно смотреть на отчёт. Поэтому полезно мысленно разделять этапы.

flowchart TD
    A[Ввод данных] --> B[Вычисления в float64]
    B --> C{Нужно округлённое значение для дальнейших вычислений?}
    C -->|нет| D["Форматируем при выводе fmt.Printf('%.2f')"]
    C -->|да| E["Округляем значение< math.Round(x*100)/100"]
    E --> D

Идея простая: если округление нужно только “чтобы было красиво” — делайте его в Printf. Если округление — часть бизнес-правила (пусть даже очень простого) — округляйте значение через math.

Кстати, философия “делайте код проще, чтобы не ошибаться” — это не только нудная мораль. В Go-сообществе это часто проговаривают именно как инженерный подход: меньше лишней сложности — меньше шансов “не заметить мелочь и потом неделю искать баг”.

5. Особенности float: пример 0.1 + 0.2

Этот пример уже стал мемом, но он полезен. Мы не будем уходить в глубокую теорию, просто закрепим практическую мысль: float — приближённый, а форматирование может скрыть хвосты… но хвосты всё равно существуют.

package main

import "fmt"

func main() {
	x := 0.1 + 0.2

	fmt.Printf("x = %.17f\n", x) // x = 0.30000000000000004
	fmt.Printf("x = %.2f\n", x)  // x = 0.30
}

Если вы делаете отчёт для человека, %.2f — это нормально и даже нужно. Если вы пишите логику вида if x == 0.3 { ... }, то вы, скорее всего, придёте к странным результатам (но стратегии сравнения с допуском — это отдельная дисциплина, и мы сейчас туда не лезем специально).

6. Типичные ошибки

Ошибка №1: “Я напечатал %.2f, значит число стало с двумя знаками”.
Это самая частая путаница. Форматирование — это только способ показать значение. Переменная не изменится, даже если вывод выглядит идеально. Если дальше по коду вы используете исходное x, то вы используете исходный x со всеми его “хвостами”. Если вам нужно именно округлённое значение, придётся вычислить его отдельно и сохранить в переменную.

Ошибка №2: попытка округлять через int(x) вместо math.Round.
Преобразование float64 в int отбрасывает дробную часть, то есть делает усечение в сторону нуля. Для 3.9 это будет 3, а для -3.9 это будет -3. Это не округление “по-человечески”, а совсем другая операция. В результате можно получить ошибки, которые выглядят как “иногда на копейку меньше”, а это типичная дорога к вечной печали.

Ошибка №3: печать денег через %v или %f без точности.
Технически программа работает, но UX — нет. Пользователь не обязан любить ваши шесть знаков после точки. Деньги и проценты почти всегда ожидаются в формате %.2f (или другом фиксированном), иначе вывод воспринимается как “сырые внутренности”, а не как результат.

Ошибка №4: “Сначала округлю каждую мелочь, потом сложу — так точнее”.
Интуитивно кажется логичным, но часто это даёт больше накопленной ошибки: округления “съедают” информацию. На базовом уровне полезное правило такое: если округление нужно только для вывода, округляйте в самом конце (через Printf). Если округление — часть правила, делайте его ровно в том месте, где правило должно вступить в силу, и не размазывайте округление по всему коду.

Ошибка №5: забыть импорт "math" и долго спорить с компилятором.
Компилятор в Go обычно ругается довольно честно: если вы пишете math.Round, а import "math" забыли, он не “догадается” за вас. Это раздражает примерно 20 секунд, зато потом вы привыкаете, что зависимости в коде видны явно. И да, это тоже часть той самой “простоты, которая уменьшает сюрпризы”.

1
Задача
Go SELF, 7 уровень, 5 лекция
Недоступна
Витрина точности
Витрина точности
1
Задача
Go SELF, 7 уровень, 5 лекция
Недоступна
Чаевые в кафе
Чаевые в кафе
1
Задача
Go SELF, 7 уровень, 5 лекция
Недоступна
Скидка с округлением
Скидка с округлением
1
Задача
Go SELF, 7 уровень, 5 лекция
Недоступна
Коробки доставки
Коробки доставки
1
Опрос
Числа и преобразования типов, 7 уровень, 5 лекция
Недоступен
Числа и преобразования типов
Числа и преобразования типов
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ