Skip to main content

Как работает преобразование типов в Go? Почему нет неявного преобразования?

Короткий ответ

Преобразование типа создаёт значение одного типа на основе значения другого типа.

Синтаксис:

НовыйТип(значение)

Пример:

var number int = 10
var result float64 = float64(number)

Go не выполняет автоматические преобразования между переменными разных числовых типов:

var number int = 10
var result float64 = number // ошибка компиляции

Преобразование нужно указать явно:

var result float64 = float64(number)

Это сделано, чтобы в коде были заметны возможные:

Основное исключение — нетипизированные константы. Они могут автоматически получить подходящий тип, если их значение в него помещается:

const value = 10

var a int = value
var b int64 = value
var c float64 = value

Важно: явное преобразование не гарантирует сохранность данных. Например, int64 при преобразовании в int32 может потерять старшие биты, а float64 при преобразовании в int — дробную часть.


Что такое преобразование типов

Преобразование типов, или type conversion, создаёт новое значение указанного типа.

i := 42
f := float64(i) // int → float64

x := 3.9
n := int(x) // float64 → int

Общий синтаксис:

T(x)

Где:

Почему Go не выполняет неявные числовые преобразования

1. Преобразование может потерять данные

При преобразовании дробного числа в целое дробная часть отбрасывается:

fmt.Println(int(3.9))  // 3
fmt.Println(int(-3.9)) // -3

Значение отбрасывается в сторону нуля. Округления не происходит.

При преобразовании большого числа в меньший целочисленный тип могут потеряться старшие биты:

var big int64 = 5_000_000_000

small := int32(big)

fmt.Println(small) // 705032704

Go требует явной записи int32(big), чтобы такое потенциально опасное действие было видно в коде.

2. Может потеряться точность

Не все значения int64 можно точно представить в float64:

var number int64 = 9_007_199_254_740_993

result := float64(number)

fmt.Printf("%.0f\n", result) // 9007199254740992

Число изменилось, потому что float64 не может точно хранить все большие целые значения.

3. Код становится понятнее

При явном преобразовании сразу видно намерение разработчика:

result := float64(count)

Понятно, что значение специально переводится в дробный тип.

Без явного преобразования было бы сложнее понять, является ли изменение типа намеренным или случайным.

4. Правила остаются простыми и предсказуемыми

В Go нет сложной системы автоматического повышения числовых типов.

Например, нельзя напрямую сложить int и int64:

var a int = 10
var b int64 = 20

result := a + b // ошибка компиляции

Нужно привести значения к одному типу:

result := int64(a) + b

Преобразования между целочисленными типами

var value int64 = 100

a := int(value)
b := int32(value)
c := uint(value)

Преобразование не проверяет, помещается ли исходное значение в новый тип:

var big int64 = 5_000_000_000

small := int32(big)

fmt.Println(small) // 705032704

Поэтому перед опасным преобразованием диапазон иногда нужно проверять вручную:

if big < math.MinInt32 || big > math.MaxInt32 {
	return errors.New("value does not fit into int32")
}

small := int32(big)

Преобразование между целыми и дробными числами

Целое число в дробное

number := 42
result := float64(number)

fmt.Println(result) // 42

Большие целые числа могут потерять точность.

Дробное число в целое

number := 3.99
result := int(number)

fmt.Println(result) // 3

Дробная часть отбрасывается, а не округляется.

Для округления используются функции пакета math:

number := 3.99

fmt.Println(math.Round(number)) // 4
fmt.Println(math.Floor(number)) // 3
fmt.Println(math.Ceil(number))  // 4

Преобразовывать нужно до вычисления

Преобразование результата не защищает от переполнения исходного выражения.

var a int32 = 1_000_000
var b int32 = 1_000_000

result := int64(a * b)

Сначала a * b вычисляется как int32. Переполнение происходит до преобразования в int64.

Правильно преобразовать операнды до вычисления:

result := int64(a) * int64(b)

fmt.Println(result) // 1000000000000

То же правило важно при делении:

total := 100
count := 3

average := total / count

fmt.Println(average) // 33

Оба значения имеют тип int, поэтому выполняется целочисленное деление.

Для дробного результата:

average := float64(total) / float64(count)

fmt.Println(average) // 33.333333...

Преобразование числа в строку

Конструкция string(number) не превращает число в строку с цифрами:

number := 65
text := string(number)

fmt.Println(text) // A

Значение 65 воспринимается как кодовая точка Unicode, соответствующая символу A.

Для получения строки "65" используется пакет strconv:

import "strconv"

number := 65
text := strconv.Itoa(number)

fmt.Println(text) // 65

Обратное преобразование:

number, err := strconv.Atoi("65")

if err != nil {
	fmt.Println("invalid number")
	return
}

fmt.Println(number) // 65

Для других типов:

text := strconv.FormatFloat(3.14, 'f', 2, 64) // "3.14"
flag := strconv.FormatBool(true)              // "true"

Преобразование между string, []byte и []rune

text := "Привет"

bytes := []byte(text)
runes := []rune(text)

textFromBytes := string(bytes)
textFromRunes := string(runes)

[]byte содержит отдельные байты UTF-8:

fmt.Println(len([]byte("Привет"))) // 12

[]rune содержит кодовые точки Unicode:

fmt.Println(len([]rune("Привет"))) // 6

Полученный слайс можно менять независимо от исходной строки:

text := "hello"
bytes := []byte(text)

bytes[0] = 'H'

fmt.Println(text)          // hello
fmt.Println(string(bytes)) // Hello

Пользовательские типы

Разные именованные типы считаются разными, даже если у них одинаковый базовый тип:

type Celsius float64
type Fahrenheit float64

var c Celsius = 100

var f Fahrenheit = c // ошибка компиляции

Нужно явное преобразование:

var f Fahrenheit = Fahrenheit(c)

Но такое преобразование меняет только тип, а не физический смысл значения:

f := Fahrenheit(c)

fmt.Println(f) // 100, а не 212

Для настоящего перевода температуры нужна формула:

f := Fahrenheit(c*9/5 + 32)

fmt.Println(f) // 212

Преобразование указателей

Обычные указатели разных типов нельзя напрямую преобразовывать друг в друга:

var number int = 10

var pointer *float64 = (*float64)(&number) // ошибка компиляции

Подобные операции возможны через пакет unsafe, но они обходят систему безопасности типов:

pointer := (*float64)(unsafe.Pointer(&number))

Такой код нужен редко и может привести к некорректной работе программы.

Type assertion — это не преобразование

Когда значение хранится в интерфейсе, для получения его конкретного типа используется type assertion — проверка типа:

var value any = "hello"

text, ok := value.(string)

if ok {
	fmt.Println(text)
}

Здесь Go не преобразует значение в string. Он проверяет, имеет ли значение внутри интерфейса тип string.

Небезопасный вариант:

text := value.(string)

Если внутри находится другой тип, программа завершится с panic.

Особый случай: нетипизированные константы

Нетипизированная константа может получить тип из контекста:

const value = 100

var a int = value
var b int64 = value
var c float64 = value

Это возможно только тогда, когда значение представимо целевым типом:

const value = 300

var number uint8 = value // ошибка: 300 не помещается в uint8

Переменная уже имеет конкретный тип:

var value = 100 // int

var result int64 = value // ошибка

Нужно явное преобразование:

var result int64 = int64(value)

Поэтому точная формулировка звучит так:

> Go не выполняет неявные преобразования между переменными разных числовых типов, но нетипизированные константы могут получать подходящий тип из контекста.

Сравнение разных типов

Переменные разных числовых типов нельзя сравнивать напрямую:

var a int = 5
var b int64 = 5

fmt.Println(a == b) // ошибка компиляции

Нужно привести их к одному типу:

fmt.Println(int64(a) == b) // true

С нетипизированной константой сравнение разрешено:

fmt.Println(a == 5) // true
fmt.Println(b == 5) // true

Константа 5 получает нужный тип из контекста.

Практический пример: проценты

done := 50
total := 75

percent := done / total * 100

fmt.Println(percent) // 0

Сначала выполняется целочисленное деление 50 / 75, результат которого равен 0.

Правильно:

percent := float64(done) / float64(total) * 100

fmt.Println(percent) // 66.666666...

Практический пример: объединение строки и числа

Go не объединяет число со строкой автоматически:

number := 42

text := "Number: " + number // ошибка компиляции

Нужно явно получить строковое представление числа:

text := "Number: " + strconv.Itoa(number)

Или использовать форматирование:

text := fmt.Sprintf("Number: %d", number)

Краткое резюме

Главная причина отсутствия неявных числовых преобразований в Go — возможная потеря данных должна быть явно заметна в коде.

Serzh AI Academy — Как работает преобразование типов в Go? Почему нет неявного преобразования?