1. Знакомство с float
Если вы только привыкли к int, то вещественные числа (типа “3.14”, “2.5”, “0.1”) сначала кажутся простыми: ну дробь и дробь. Но в программировании они похожи на человека, который обещал прийти “ровно в 19:00”, а приходит “примерно около семи, но в реальности — в восемь”. То есть вещественные числа в компьютере почти всегда приближённые.
В Go вещественные числа представлены типами float32 и float64. Они нужны, когда мы работаем с измерениями, средними значениями, процентами, деньгами в виде “евро с центами” (хотя для денег часто используют целые центы), координатами, временем в секундах с дробной частью и так далее.
Проблема в том, что компьютер хранит такие числа не в десятичной системе, а в двоичной, и некоторые привычные нам десятичные дроби в двоичной системе не представляются “ровно”. Поэтому вы периодически встречаете “хвосты” и странности в сравнении.
2. float32 и float64: различия и что выбирается по умолчанию
Когда вы видите float32 и float64, самое важное — не запоминать “миллионы цифр после запятой”, а понять идею: это числа с плавающей точкой (floating point), и у них есть ограниченная точность, зависящая от количества бит.
float32 занимает 32 бита (4 байта), а float64 — 64 бита (8 байт). Чем больше бит, тем больше точности (и обычно больше диапазон). В реальной жизни программирования float64 используется чаще, потому что на современных компьютерах разница в памяти редко критична, а вот разница в точности — очень даже.
Небольшая шпаргалка:
| Тип | Размер | Примерная точность (в десятичных цифрах) | “По умолчанию” для литерала 0.1 |
|---|---|---|---|
|
4 байта | ~6–7 значащих цифр | нет |
|
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.... Это не означает, что цикл неправильный. Это означает, что вы столкнулись с реальностью представления чисел с плавающей точкой.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ