JavaRush /Курсы /Go SELF /Вещественные числа в Go: float32/float64

Вещественные числа в Go: float32/float64

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

1. Знакомство с float

Если вы только привыкли к int, то вещественные числа (типа “3.14”, “2.5”, “0.1”) сначала кажутся простыми: ну дробь и дробь. Но в программировании они похожи на человека, который обещал прийти “ровно в 19:00”, а приходит “примерно около семи, но в реальности — в восемь”. То есть вещественные числа в компьютере почти всегда приближённые.

В Go вещественные числа представлены типами float32 и float64. Они нужны, когда мы работаем с измерениями, средними значениями, процентами, деньгами в виде “евро с центами” (хотя для денег часто используют целые центы), координатами, временем в секундах с дробной частью и так далее.

Проблема в том, что компьютер хранит такие числа не в десятичной системе, а в двоичной, и некоторые привычные нам десятичные дроби в двоичной системе не представляются “ровно”. Поэтому вы периодически встречаете “хвосты” и странности в сравнении.

2. float32 и float64: различия и что выбирается по умолчанию

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

float32 занимает 32 бита (4 байта), а float6464 бита (8 байт). Чем больше бит, тем больше точности (и обычно больше диапазон). В реальной жизни программирования float64 используется чаще, потому что на современных компьютерах разница в памяти редко критична, а вот разница в точности — очень даже.

Небольшая шпаргалка:

Тип Размер Примерная точность (в десятичных цифрах) “По умолчанию” для литерала 0.1
float32
4 байта ~6–7 значащих цифр нет
float64
8 байт ~15–16 значащих цифр да

В Go нетипизированные числовые литералы (например, 0.1, 2.5, 1.0/3.0) по умолчанию становятся float64, когда вы присваиваете их переменной без явного типа. Это обычно удобно: точности больше, сюрпризов меньше.

Мини-пример: посмотрим на тип литерала через %T (диагностическая печать — это наша “лупа” программиста):

package main

import "fmt"

func main() {
	x := 0.1
	var y float32 = 0.1

	fmt.Printf("x=%v, type=%T\n", x, x) // x=0.1, type=float64
	fmt.Printf("y=%v, type=%T\n", y, y) // y=0.1, type=float32
}

3. Эффект 0.1 + 0.2 и сравнение float

Почему 0.1 + 0.2 может быть не равно 0.3

Сейчас будет главный “фокус сезона”, который в первый раз ломает мозг почти всем (и это нормально).

Математика говорит:

  • 0.1 + 0.2 = 0.3

Компьютер иногда говорит:

  • 0.1 + 0.2 = 0.30000000000000004

И вы такие: “Go, ты серьёзно?” А он — серьёзно. Просто Go честный.

Причина: дроби типа 0.1 в двоичной системе — бесконечные (как 1/3 в десятичной: 0.333333...). Поэтому число хранится приближённо, и дальше арифметика честно работает с этим приближением.

Давайте напишем минимальный пример и выведем результат с высокой точностью. Тут важно: мы не “сходим с ума от лишних цифр”, мы используем их как диагностику.

package main

import "fmt"

func main() {
	a := 0.1
	b := 0.2
	c := a + b

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

Да, это выглядит как баг, но это ожидаемое поведение чисел с плавающей точкой.

Чтобы закрепить интуицию, представим путь числа в компьютере как набор действий:

flowchart LR
    A["Десятичная дробь<br/>0.1"] --> B["Двоичное представление<br/>(бесконечная дробь)"]
    B --> C["Округление до float64<br/>(ограниченная точность)"]
    C --> D["Арифметика<br/>+ 0.2"]
    D --> E["Печать результата<br/>(видим хвост)"]

И даже == может предать в трудную минуту

В мире int сравнение == почти всегда ведёт себя так, как вы ожидаете. В мире float — осторожнее.

Когда вы пишете:

x := 0.1 + 0.2
y := 0.3
fmt.Println(x == y)

вы ожидаете true. Но на практике легко получить false, потому что x и y могут отличаться на микроскопическую величину, а == требует абсолютного совпадения битового представления.

Мини-пример:

package main

import "fmt"

func main() {
	x := 0.1 + 0.2
	y := 0.3

	fmt.Println(x)      // 0.30000000000000004 (часто)
	fmt.Println(y)      // 0.3
	fmt.Println(x == y) // false (часто)
}

Что с этим делать? На уровне текущей лекции достаточно запомнить практическое правило: если float получен вычислениями, сравнение через == может быть неточным.

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

4. Погрешности в вычислениях и маленькая практика

float32 vs float64 на примере 1.0/3.0

На “красивых” десятичных дробях иногда всё выглядит нормально, но стоит взять число, которое точно не представляется в конечном виде, например 1/3, и разница точности становится заметнее.

package main

import "fmt"

func main() {
	var f32 float32 = 1.0 / 3.0
	var f64 float64 = 1.0 / 3.0

	fmt.Printf("f32 = %.10f\n", f32) // f32 = 0.3333333433
	fmt.Printf("f64 = %.10f\n", f64) // f64 = 0.3333333333
}

Обратите внимание: float32 “косячит” сильнее. И это не “плохой Go”, это просто меньше бит на точность.

Накопление ошибок при множестве операций

Есть ещё один эффект, который полезно увидеть: даже если каждая отдельная погрешность маленькая, при большом числе операций она может “накопиться”.

Например, давайте 10 раз прибавим 0.1 и посмотрим, получится ли 1.0:

package main

import "fmt"

func main() {
	sum := 0.0
	for i := 0; i < 10; i++ {
		sum += 0.1
	}
	fmt.Printf("sum = %.17f\n", sum) // sum = 0.99999999999999989 (часто)
}

С математической точки зрения мы должны получить ровно 1.0, но в двоичном мире мы 10 раз складывали приближённые значения. Поэтому итог может быть чуть меньше или чуть больше ожидаемого.

Важно: это не значит, что float “нельзя использовать”. Это значит, что нужно понимать его природу и не строить бизнес-логику на “идеальной точности десятичной дроби”.

Мини‑практика: считаем среднее

Давайте продолжим наш учебный стиль “read → compute → print” и соберём мини-программу, которая считает среднее значение. Это простой и жизненный кейс: средняя температура, средний балл, средняя скорость.

Предположим, пользователь вводит два числа с дробной частью (например, две оценки или две температуры). Мы читаем их как float64, считаем среднее и печатаем.

package main

import (
	"fmt"
)

func main() {
	var a float64
	var b float64

	fmt.Scan(&a, &b) // ввод: 0.1 0.2

	avg := (a + b) / 2
	fmt.Printf("avg = %.17f\n", avg) // avg = 0.15000000000000002 (возможный хвост)
}

Даже среднее от 0.1 и 0.2 иногда даст “хвост”. И это отличный момент: теперь вы не паникуете, а понимаете, откуда он.

Как отлаживать float, не впадая в хондру

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

Для этого удобно:

  • печатать много знаков после точки (например, %.17f для float64);
  • печатать тип значения через %T, если вы сомневаетесь, не уехали ли вы в float32 случайно;
  • иногда печатать обычным %v, чтобы видеть “человеческий” вид.

Небольшой пример “диагностики”:

package main

import "fmt"

func main() {
	x := 0.1 + 0.2
	fmt.Printf("x=%v\n", x)    // x=0.30000000000000004 (часто)
	fmt.Printf("x=%.17f\n", x) // x=0.30000000000000004
	fmt.Printf("type=%T\n", x) // type=float64
}

5. Типичные ошибки при работе с float

Ошибка №1: ожидать “идеальной десятичной математики”.
Часто новичок думает: “Я же ввёл 0.1, значит внутри 0.1”. Но внутри хранится ближайшее представимое двоичное значение. Симптом обычно такой: в выводе появляются хвосты вроде ...0000004 или ...9999998. Лечится не “заклинанием fmt”, а пониманием природы float.

Ошибка №2: использовать float32 по умолчанию без причины.
Иногда кажется: “float32 меньше — значит лучше”. Но в большинстве учебных и прикладных задач float64 даёт более предсказуемые результаты. Симптом: точность “сыпется” слишком рано, особенно после нескольких операций или при делении.

Ошибка №3: сравнивать результаты вычислений через == и строить на этом логику.
Это классика жанра. Симптом: “иногда условие срабатывает, иногда нет”, особенно если в коде есть суммы дробей. В int это почти всегда означает баг в логике, а в float — ещё и проблему точности. Полезная привычка: если очень хочется ==, сначала спросите себя, откуда взялось число — “из ввода как фиксированная величина” или “из вычислений”.

Ошибка №4: печатать float без понимания, что форматирование — это только отображение.
Если вы печатаете %.2f, вы делаете число красивым для человека, но не меняете то, что хранится в переменной. Симптом: на экране “всё ровно 0.30”, а логика сравнения/суммирования всё равно ведёт себя странно. Здесь важно разделять “значение в памяти” и “как мы его показываем”.

Ошибка №5: удивляться, что многократное сложение даёт неожиданный итог.
Сумма 0.1 десять раз — один из самых известных примеров накопления погрешности. Симптом: вместо 1.0 вы видите 0.999999999999.... Это не означает, что цикл неправильный. Это означает, что вы столкнулись с реальностью представления чисел с плавающей точкой.

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