Перейти до змісту

Перетворення типів

Суворе правило

«Явні перетворення обов'язкові, коли в одному виразі або присвоєнні змішуються різні числові типи.» — специфікація Go

У Go немає неявних числових перетворень. Навіть між типами однакового розміру потрібно явно перетворювати.

var i int   = 42
var f float64 = i               // compile error: cannot use i (int) as float64
var f float64 = float64(i)      // ok

var a int32 = 1
var b int64 = a                 // compile error: cannot use a (int32) as int64
var b int64 = int64(a)          // ok

З досвіду Python: Python вільно змішує int і float в арифметиці. Go відмовляє — потрібно явно вказувати перетворення щоразу. Як тільки ви приймаєте це, ви виявите, що в програмах на Go менше помилок типу «стривай, звідки взявся цей float?».

Синтаксис перетворення: T(x)

var i int       = 42
var f float64   = float64(i)        // int → float64
var u uint32    = uint32(f)         // float64 → uint32 (відкидає дробову частину до нуля)
var b byte      = byte(i)           // int → uint8 (відкидає старші біти)

Перетворення числа з рухомою комою в ціле відкидає дробову частину. Число має бути змінною, а не голим літералом — int(3.9), записаний як літерал, є помилкою компіляції, оскільки Go відмовляється мовчки відкидати .9 з нетипізованої константи. Як тільки значення живе у float64, перетворення є коректним:

f := 3.9
fmt.Println(int(f))     // 3
fmt.Println(int(-f))    // -3 — до нуля, а не до -нескінченності

Перетворення більшого цілого в менше відкидає старші біти — результат загортається:

var big int32 = 257
var small int8 = int8(big)
fmt.Println(small)       // 1   (257 mod 256)

Рядки, байти та руни

Ці три перетворення достатньо поширені, щоб їх запам'ятати.

string ↔ []byte — сирі UTF-8 байти

s := "hello"
b := []byte(s)              // [104 101 108 108 111]
s2 := string(b)             // "hello"

string ↔ []rune — кодові точки Unicode

s := "日本語"
r := []rune(s)              // [26085 26412 35486]
s2 := string(r)             // "日本語"

fmt.Println(len(s))         // 9 — байти
fmt.Println(len(r))         // 3 — руни

Окрема руна до рядка

s := string('A')            // "A"
s := string(rune(0x4e2d))   // "中"

Є тонкість: string(65) також дає "A", але go vet це позначить. Завжди спочатку приводьте до rune, щоб намір був зрозумілий.

Рядки ↔ числа — використовуйте strconv, а не перетворення типів

Синтаксис T(x) не розбирає рядок у число. Для цього стандартна бібліотека має strconv.

import "strconv"

// рядок → число
n, err := strconv.Atoi("42")
if err != nil { /* ... */ }                 // n == 42

f, err := strconv.ParseFloat("3.14", 64)    // f == 3.14
b, err := strconv.ParseBool("true")         // b == true

// число → рядок
s := strconv.Itoa(42)                       // "42"
s := strconv.FormatFloat(3.14, 'f', 2, 64)  // "3.14"
s := strconv.FormatInt(255, 16)             // "ff"

fmt.Sprintf — це універсальна альтернатива: повільніша, але гнучка:

s := fmt.Sprintf("%d items, %.2f each", 3, 9.5)
// "3 items, 9.50 each"

З досвіду Python: int("42") і str(42) у Python є глобальними вбудованими функціями. Go поміщає їх у strconv, щоб зробити вартість (і можливість помилки) явною. strconv.Atoi повертає (int, error) — жодних винятків для перехоплення немає.

Нетипізовані константи — єдиний виняток

Це, мабуть, найбільша пастка для Python-розробника, що вивчає Go, і вона зринає в кількох місцях далі в курсі, тому варто приділити їй хвилину.

Нетипізовані константи не мають типу до моменту використання

Літерал на кшталт 42, 3.14 або "hi", записаний у вихідному коді, є нетипізованою константою. Вона не має фіксованого типу Go — вона несе ідеалізоване числове значення (довільна точність для чисел). Тип визначається лише тоді, коли цього вимагає контекст.

var x float64 = 42        // ok — 42 є нетипізованою цілочисельною константою, підходить для float64
var y int     = 0.0       // ok — 0.0 можна представити як int (немає дробової частини)
var z int     = 3.14      // compile error: 3.14 (untyped float) truncated to int

Третій рядок — це те саме правило, що спрацювало вище з int(3.9): Go відмовляється мовчки відкидати дробову частину константи.

Арифметика констант також є довільно точною

Наслідок, який дивує Python-розробників: коли ви виконуєте арифметику над константами, Go робить це під час компіляції з довільною точністю, а не з числами IEEE 754. Класична пастка порівняння чисел з рухомою комою не спрацьовує:

fmt.Println(0.1 + 0.2 == 0.3)                       // true  (constants)

var a, b, c float64 = 0.1, 0.2, 0.3
fmt.Println(a + b == c)                             // false (float64 variables)

Ті самі числа, зовсім різні відповіді — залежно від того, чи виразу до часу виконання, чи він згортається під час компіляції.

Щойно константа отримала тип, правило відновлюється

const limit int = 10
var f float64 = limit               // compile error
var f float64 = float64(limit)      // ok

Саме тому більшість констант на рівні пакету залишаються нетипізованими — вони адаптуються до будь-якого типу, потрібного в місці виклику. Явний тип вказуйте лише тоді, коли хочете їх обмежити.

З досвіду Python: у Python немає справжнього аналога. 42 — це завжди int, 3.14 — завжди float, а операції підпорядковуються семантиці часу виконання. Модель Go «константа — це значення, а не типізована річ» справді інша — і вона є джерелом кількох тонких відмінностей у поведінці, з якими ви зустрінетеся (результати порівняння літерал-проти-змінна, що компілюється, а що ні, типи за замовчуванням у :=).

T(x) також працює між користувацькими типами

Той самий синтаксис T(x) підходить для будь-яких двох типів, що мають спільний базовий тип, — не лише для зумовлених числових. Повна розповідь є в 09-custom-types.md; коротко:

type Celsius float64
type Fahrenheit float64

c := Celsius(20.0)
f := Fahrenheit(c)            // ok — однаковий базовий тип
var raw float64 = float64(c)  // ok — знімаємо іменований тип

Перетворення інтерфейсів (ствердження типів) використовують інший синтаксис — розглянемо в наступній темі.

Короткий довідник

Що Синтаксис Примітки
Числові між типами T(x) Завжди явно. Відкидає дроби; загортає при звуженні.
string → байти []byte(s) UTF-8 байти, незмінна копія.
байти → string string(b) Копіює.
string → руни []rune(s) Декодує UTF-8 у кодові точки.
руни → string string(r) Перекодовує у UTF-8.
string → число strconv.Atoi, strconv.ParseInt, strconv.ParseFloat, ... Повертає (value, error).
число → string strconv.Itoa, strconv.FormatInt, strconv.FormatFloat або fmt.Sprintf Вибирайте за швидкістю/гнучкістю.
Окрема руна → рядок string(rune(0x4e2d)) Завжди спочатку приводьте до rune.

Джерела