Перейти к содержимому

Типы и Zero Values в Go

Глубокое погружение в систему типов Go: примитивы, размеры, нулевые значения, конвертация, константы, iota. Без понимания этого раздела все остальные темы (слайсы, мапы, интерфейсы) превращаются в магию. На собесе джуна — это базовый минимум, который проверяют первым.

  1. Базовое определение и API
  2. Внутреннее устройство (под капотом)
  3. Тонкие моменты / Gotchas
  4. Производительность
  5. Типичные вопросы на собеседовании Junior
  6. Practice — задачки на проверку
  7. Источники и дополнительно

Go строго типизирован. Все типы делятся на:

  • Boolean: bool
  • Numeric: целые, числа с плавающей точкой, комплексные
  • String: string
  • Composite: array, slice, map, struct, chan, interface, func, pointer
  • Named types (типы, определённые пользователем через type)
ТипРазмерДиапазон / Описание
bool1 байтtrue / false
int81 байт-128 .. 127
int162 байта-32 768 .. 32 767
int324 байта-2^31 .. 2^31-1
int648 байт-2^63 .. 2^63-1
int4 или 8 байтзависит от платформы (обычно 64 бита)
uint8 (byte)1 байт0 .. 255
uint162 байта0 .. 65 535
uint324 байта0 .. 2^32-1
uint648 байт0 .. 2^64-1
uint4 или 8 байтзависит от платформы
uintptrразмер указателяцелое, в которое помещается указатель
float324 байтаIEEE 754 single precision
float648 байтIEEE 754 double precision
complex648 байтfloat32 + float32i
complex12816 байтfloat64 + float64i
rune4 байта (int32)Unicode code point
string16 байт (на 64-bit)заголовок: ptr + len
package main
import (
"fmt"
"unsafe"
)
func main() {
var b bool
var i int
var s string
var f float64
fmt.Println(unsafe.Sizeof(b)) // 1
fmt.Println(unsafe.Sizeof(i)) // 8 (на 64-bit)
fmt.Println(unsafe.Sizeof(s)) // 16
fmt.Println(unsafe.Sizeof(f)) // 8
}

В Go нет неинициализированных переменных — компилятор автоматически присваивает нулевое значение.

ТипZero value
boolfalse
int*, uint*, byte, rune0
float*0.0
complex*0+0i
string"" (пустая)
pointer (*T)nil
slicenil (len=0, cap=0)
mapnil
channelnil
functionnil
interfacenil
structвсе поля → zero
array [N]Tвсе элементы → zero
var i int // 0
var s string // ""
var p *int // nil
var sl []int // nil, len=0, cap=0
var m map[string]int // nil
// struct: все поля zero
type Point struct {
X, Y int
Name string
}
var p2 Point // {X:0, Y:0, Name:""}
// 4 способа объявить переменную
var a int // zero value
var b int = 10 // явный тип + значение
var c = 10 // тип выводится (int)
d := 10 // short, только внутри функций
// Множественное
var x, y int = 1, 2
a1, b1 := "hello", 42
// var-блок
var (
name string
age int
ok bool
)

⚠️ := работает только внутри функций. На package level нужно var.


Все примитивы Go — это просто байты в памяти. Компилятор знает размер и кодирование:

int64 = 8 байт little-endian (на x86/arm64):
значение 1: 01 00 00 00 00 00 00 00
значение -1: FF FF FF FF FF FF FF FF (two's complement)
значение 256: 00 01 00 00 00 00 00 00
float64 (IEEE 754): знак(1) | экспонента(11) | мантисса(52)
1.0: 00 00 00 00 00 00 F0 3F
0.1: 9A 99 99 99 99 99 B9 3F (НЕ точно 0.1!)
// runtime/string.go (упрощённо)
type stringStruct struct {
str unsafe.Pointer // указатель на байты UTF-8
len int // длина в БАЙТАХ
}

ASCII-схема:

string "Привет"
┌──────────────┬──────┐
│ ptr ─────────┼──┐ │
│ len = 12 │ │ │ (6 символов × 2 байта в UTF-8)
└──────────────┘ │ │
┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐
│ D0 │ 9F │ D1 │ 80 │ D0 │ B8 │ D0 │ B2 │ D0 │ B5 │ D1 │ 82 │
└────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘
П р и в е т
// На 32-bit платформе: int = int32
// На 64-bit платформе: int = int64
// Это НЕ алиас!
var a int = 5
var b int32 = 5
// a == b // НЕ КОМПИЛИРУЕТСЯ — разные типы

Когда использовать что:

  • int — почти всегда (индексы, счётчики)
  • int64 / int32 — когда нужна гарантированная разрядность (binary protocols, hashes)
  • uint — почти никогда; используется в специфичных случаях (битовые поля, размер памяти)

Type conversion — преобразование значения между совместимыми типами (numeric, string ↔ []byte).

var i int = 42
var f float64 = float64(i) // 42.0
var b byte = byte(i) // 42
var s string = "hello"
var bs []byte = []byte(s) // [104 101 108 108 111]

Type assertion — извлечение конкретного типа из интерфейса.

var x interface{} = "hello"
s := x.(string) // OK: s == "hello"
s2, ok := x.(int) // ok == false (safe form)
ConversionAssertion
Между типамиИз интерфейса в тип
Compile-time checkRuntime check
T(v)v.(T)
Panic не бросаетМожет бросить panic

Type definition — новый тип, имеющий собственный набор методов.

type Celsius float64
type Fahrenheit float64
var c Celsius = 100
var f Fahrenheit = c // ОШИБКА компиляции: разные типы
var f2 Fahrenheit = Fahrenheit(c) // OK

Type alias — другое имя для того же типа (с Go 1.9).

type Celsius = float64 // ALIAS — это РОВНО тот же тип
var c Celsius = 100
var f float64 = c // OK, никаких преобразований

⚠️ Type alias использовался для миграции byte = uint8, rune = int32, и при рефакторинге пакетов (например, os/signalsyscall).

iota — это специальный идентификатор, существующий только внутри const-блоков. Он сбрасывается в 0 в начале каждого const-блока и увеличивается на 1 на каждой строке (ConstSpec).

const (
A = iota // 0
B // 1 (повторяется выражение)
C // 2
)

Хитрые случаи:

// Пропуск значений
const (
_ = iota // 0 — отбрасываем
KB = 1 << (10 * iota) // 1 << 10 = 1024
MB // 1 << 20
GB // 1 << 30
TB // 1 << 40
)
// Битовые флаги
type Flags uint
const (
FlagRead Flags = 1 << iota // 1
FlagWrite // 2
FlagExec // 4
)
// Сброс на каждом const-блоке
const (
X = iota // 0
)
const (
Y = iota // 0 (НОВЫЙ блок!)
)

⚠️ Подвох: Использование iota в выражениях с лишними строками.

const (
Bit0 = 1 << iota // 1 << 0 = 1
Bit1 // 1 << 1 = 2
_ // пропустили (iota == 2)
Bit3 // 1 << 3 = 8
)
const Pi = 3.14159 // untyped (default float64)
const MaxItems = 100 // untyped (default int)
var f float32 = Pi // OK! Untyped автоматически конвертируется
var i int8 = MaxItems // OK, если влезает в диапазон
// Точность untyped — выше float64
const Big = 1 << 100 // OK! Хотя в int64 не влезает
var x int = Big // ОШИБКА: 1<<100 не влезает в int
var x2 float64 = Big // OK (потеря точности, но без overflow)

Untyped constants имеют произвольную точность на этапе компиляции. Это позволяет:

  • Писать const c = 1.0/3.0 с максимальной точностью.
  • Использовать одну константу с разными типами без приведений.
const N = 100
var a int = N
var b int64 = N
var c float64 = N
// Все три валидны без N(...)

Не все типы сравнимы через ==:

ТипComparable?
bool, числаДа
stringДа
pointerДа
array [N]TЕсли T comparable
structЕсли все поля comparable
interfaceДа (с runtime panic если динамический тип non-comparable)
sliceНет! (только с nil)
mapНет! (только с nil)
funcНет! (только с nil)
var s1 = []int{1, 2, 3}
var s2 = []int{1, 2, 3}
// s1 == s2 // ОШИБКА компиляции
s1 == nil // OK
type S struct{ data []int }
var a, b S
// a == b // ОШИБКА: struct содержит slice

В generics с Go 1.18 появился constraint comparable:

func Index[T comparable](slice []T, target T) int {
for i, v := range slice {
if v == target {
return i
}
}
return -1
}

С Go 1.20 был расширен набор типов, удовлетворяющих comparable (включая интерфейсы, где compare может паниковать в runtime).


В Go нет паники при overflow целочисленных типов. Просто wrap-around.

var i int32 = 2_147_483_647
i++
fmt.Println(i) // -2147483648 — wrap around!
var u uint8 = 255
u++
fmt.Println(u) // 0

⚠️ Это особенно опасно при работе с временем (time.Duration — int64 наносекунд) и хешированием.

fmt.Println(0.1 + 0.2) // 0.30000000000000004
fmt.Println(0.1+0.2 == 0.3) // false
// Правильное сравнение:
const eps = 1e-9
if math.Abs(a-b) < eps { /* ... */ }
nan := math.NaN()
fmt.Println(nan == nan) // false (!!)
fmt.Println(math.IsNaN(nan)) // true (используйте это)

NaN не равен ничему, включая себя. Это нарушает рефлексивность в map (NaN-ключ нельзя достать).

m := map[float64]int{}
m[math.NaN()] = 1
m[math.NaN()] = 2
fmt.Println(len(m)) // 2 (!)
fmt.Println(m[math.NaN()]) // 0 — нельзя достать

Gotcha 4: Конвертация в меньший тип — потеря данных без warning

Заголовок раздела «Gotcha 4: Конвертация в меньший тип — потеря данных без warning»
var i int = 300
var b byte = byte(i)
fmt.Println(b) // 44 (300 % 256 = 44)

Компилятор НЕ предупреждает. Используйте проверку или math.MaxInt8 и т.д.

type MyError struct{}
func (e *MyError) Error() string { return "err" }
func bad() error {
var e *MyError = nil
return e // возвращаем "пустой" указатель в интерфейсе
}
func main() {
err := bad()
fmt.Println(err == nil) // FALSE!
}

Интерфейс — это пара (type, value). Здесь type = *MyError, value = nil. Сам интерфейс — НЕ nil.

Правило: возвращайте nil напрямую, не присваивайте typed nil в interface.

x := 10
if true {
x := 20 // НОВАЯ переменная в новом scope
fmt.Println(x) // 20
}
fmt.Println(x) // 10

⚠️ Особенно коварно с err :=:

err := doSomething()
if cond {
err := tryAgain() // СОЗДАНА новая err! Внешняя не обновлена.
_ = err
}
// здесь err — это первое err

:= требует, чтобы хотя бы одна переменная слева была новой.

a, err := f1()
b, err := f2() // OK: b — новая, err переиспользуется
a, err := f3() // ОШИБКА: ни a, ни err не новые
// Решение:
a, err = f3()

Gotcha 8: Constant overflow в типизированных контекстах

Заголовок раздела «Gotcha 8: Constant overflow в типизированных контекстах»
const X = 1 << 62 // OK как untyped
var i int64 = X // OK
const Y = 1 << 63 // OK как untyped (не влезет в int64, но это untyped)
var i64 int64 = Y // ОШИБКА: overflow
var b byte = 'A' // 65, ASCII
var r rune = 'Ё' // 1025, Unicode code point
var x byte = 'Ё' // ОШИБКА: значение не влезает в byte
// Строковый литерал — это string, не []byte
s := "A" // string
b2 := 'A' // rune (int32) (!!)

⚠️ 'A' — это rune-литерал, а не byte. Зависит от контекста.

Gotcha 10: Сравнение интерфейсов с разными конкретными типами

Заголовок раздела «Gotcha 10: Сравнение интерфейсов с разными конкретными типами»
var i1 interface{} = int(1)
var i2 interface{} = int32(1)
fmt.Println(i1 == i2) // false (разные конкретные типы!)
const N = 5
fmt.Printf("%T\n", N) // ОШИБКА: cannot use N as untyped
// нужно либо int(N), либо typed const

В большинстве случаев untyped становится typed автоматически (default type). Но через рефлексию untyped не виден.

В отличие от C/Python, в Go true нельзя использовать как 1.

b := true
// i := int(b) // ОШИБКА компиляции
i := 0
if b { i = 1 }

Из-за выравнивания (alignment), порядок полей в struct влияет на размер.

type Bad struct {
a byte // 1 + 7 padding
b int64 // 8
c byte // 1 + 7 padding
} // = 24 байта
type Good struct {
b int64 // 8
a byte // 1
c byte // 1 + 6 padding в конце
} // = 16 байт

Правило: сортируйте поля от больших к меньшим.

import "unsafe"
fmt.Println(unsafe.Sizeof(Bad{})) // 24
fmt.Println(unsafe.Sizeof(Good{})) // 16

Утилита fieldalignment (golang.org/x/tools) проверяет порядок.

Компилятор решает, где разместить переменную: stack или heap.

Окно терминала
go build -gcflags="-m" main.go
func makeInt() *int {
x := 42
return &x // x escape to heap (адрес уходит наружу)
}
func sumLocal() int {
x := 42
return x // stay on stack
}

✅ Локальные значения примитивов — на стеке (быстро). ⚠️ Указатели, замыкания, интерфейсы — часто на куче.

const factor = 0.5
// Untyped — компилятор подставит без преобразований
var f float32 = 100
f *= factor // OK, factor — untyped float

4.4 Битовые операции вместо умножения/деления

Заголовок раздела «4.4 Битовые операции вместо умножения/деления»
// Быстрее
x := n << 1 // n * 2
y := n >> 3 // n / 8
// Медленнее (но обычно компилятор сам оптимизирует)
x := n * 2
y := n / 8

⚠️ Сдвиг даёт результат, отличный от деления для отрицательных чисел (округление к -∞ vs к нулю).

4.5 Избегайте лишних типовых конверсий в циклах

Заголовок раздела «4.5 Избегайте лишних типовых конверсий в циклах»
for i := 0; i < 1000; i++ {
x := float64(i) // вычисляется каждый раз
}

Компилятор обычно справляется, но в горячем коде стоит проверить через -S (ассемблер).


A: Зависит от платформы: 32 бита на 32-битных, 64 бита на 64-битных системах. Это не алиас для int64. Если нужна гарантированная разрядность — int32/int64.

A: Пустая строка "". Длина 0. Под капотом — stringStruct{str: nil, len: 0}. Сравнение s == "" валидно.

A: nil. Под капотом: ptr=nil, len=0, cap=0. Можно делать append к nil-слайсу — работает. Можно делать len(nil_slice) — вернёт 0. Нельзя индексировать.

A: byte — алиас для uint8 (1 байт, 0..255). rune — алиас для int32 (4 байта, Unicode code point). byte — для бинарных данных, rune — для символов Unicode.

var b bool
fmt.Println(b)

A: false. Zero value для bool.

A: Нет (compile error). Только с nil. Для поэлементного сравнения — slices.Equal() (Go 1.21+) или reflect.DeepEqual.

A: Константа без явного типа. Имеет неограниченную точность и default type. Может использоваться с разными типами без явного преобразования: const N=100; var f float64 = N; var i int = N.

A:

  • type A Bновый тип, требует явной конвертации, может иметь свои методы.
  • type A = Balias, синоним. Это тот же самый тип, никакого преобразования не нужно.

A: Счётчик внутри const-блока, начинается с 0, инкрементируется на каждой строке ConstSpec. Подвох: сбрасывается между const-блоками; пропуски через _ тоже инкрементируют его; в выражениях может давать неочевидные значения.

A: Зависит от контекста. var s []int; s == nil → true. Но var i interface{} = (*int)(nil); i == nilfalse, потому что interface содержит type info. Это самая популярная ловушка про nil.

const x = 1 << 30
fmt.Printf("%T\n", x)

A: int. Untyped constant, default type для целого — int. (Если использовать в float-контексте, может быть float64.)

A: Целые числа wrap around (не panic). var x int8 = 127; x++ → -128. Защита: использовать большие типы или проверки math.MaxInt, math.MinInt.

A: Not-a-Number из IEEE 754. math.NaN(). Особенность: NaN != NaN (даже сам с собой). Проверка только через math.IsNaN(x). В map может создать “недостижимые” ключи.

A:

  • string(65)"A" (Unicode code point, это gotcha!)
  • strconv.Itoa(65)"65" (правильно для числа)
  • С Go 1.15 go vet ругается на string(int) для не-rune.

A: Нет. var i int = 5; var f float64 = 1.0; i + f — ошибка. Нужна явная конверсия: float64(i) + f.

type Celsius float64
var c Celsius = 100
var f float64 = c

A: Ошибка компиляции. Celsius — новый тип, нужен float64(c).

A:

  • uintptr — целое число, в которое влезает указатель. GC его не отслеживает.
  • unsafe.Pointer — настоящий указатель, GC видит, но проверка типов отключена.

Использовать uintptr для хранения адреса опасно — GC может переместить объект.

A: Constraint для типов, поддерживающих ==/!=. С Go 1.18. С Go 1.20 расширен: интерфейсы тоже удовлетворяют (но могут паниковать в runtime, если внутри лежит non-comparable). Используется в map-ключах, slices.Index и т.д.

A: rune (то есть int32). Значение 65.

var a int = 10
b := &a
*b++
fmt.Println(a)

A: 11. *b++ инкрементирует значение, на которое указывает b. (Эквивалент *b = *b + 1.)

A: Да, struct{} имеет размер 0 байт. Используется как маркер: map[K]struct{} — set, chan struct{} — сигнальный канал.

fmt.Println(unsafe.Sizeof(struct{}{})) // 0

A: nil — отсутствие значения для ссылочных типов (pointer, slice, map, chan, func, interface). 0 — нулевое значение для числовых типов. Они не взаимозаменяемы: var i int = nil — ошибка.

var x interface{} = 5
y, ok := x.(int)
fmt.Println(y, ok)

A: 5 true. Type assertion с , ok form — безопасный. Без ok, при ошибочном типе — panic.

Q24: Объясните, почему сравнение float опасно.

Заголовок раздела «Q24: Объясните, почему сравнение float опасно.»

A: Float — приближение десятичных чисел в двоичной системе. 0.1 + 0.2 != 0.3. Сравнение через == непредсказуемо. Используется epsilon: math.Abs(a-b) < 1e-9.

A:

  • array [N]T — все элементы zero (по типу T).
  • struct — все поля zero.
  • slice — nil (но len(s) == 0, можно append).
  • map — nil (читать можно, писать → panic).
  • chan — nil (read/write блокирует навсегда → deadlock).
  • func — nil (вызов → panic).
  • interface — nil (вызов метода → panic).
  • pointer — nil.

type Config struct {
Timeout time.Duration
Hosts []string
Cache map[string]int
OnFatal func(error)
}
var c Config
// Что вернёт каждое из выражений?
fmt.Println(c.Timeout) // ?
fmt.Println(c.Hosts == nil) // ?
fmt.Println(c.Cache == nil) // ?
fmt.Println(c.OnFatal == nil) // ?

Решение:

  • c.Timeout0s (Duration — int64, zero = 0)
  • c.Hosts == niltrue
  • c.Cache == niltrue
  • c.OnFatal == niltrue
const (
_ = iota // ?
A // ?
B = iota * 10 // ?
C // ?
D = "x" // ?
E // ? (внимание!)
)

Решение:

  • _ = 0
  • A = 1 (повторение iota)
  • B = 20 (iota=2, expression iota*10)
  • C = 30 (iota=3, повторение выражения)
  • D = “x” (iota=4, но не используется)
  • E = “x” (iota=5, повторение литерала "x")

Объяснение: повторяется полное выражение предыдущей строки. Иначе ошибка.

type Writer interface {
Write([]byte) (int, error)
}
type NopWriter struct{}
func (NopWriter) Write(b []byte) (int, error) { return len(b), nil }
func getWriter(useNop bool) Writer {
var nw *NopWriter
if useNop {
nw = &NopWriter{}
}
return nw
}
w := getWriter(false)
fmt.Println(w == nil) // ?

Решение: false. getWriter возвращает интерфейс с (type=*NopWriter, value=nil). Сам интерфейс — не nil.

✅ Правильный способ:

func getWriter(useNop bool) Writer {
if !useNop {
return nil
}
return &NopWriter{}
}
type MyInt int
type MyIntAlias = int
func plusOne(x int) int { return x + 1 }
var a MyInt = 5
var b MyIntAlias = 5
// plusOne(a) // ?
// plusOne(b) // ?

Решение:

  • plusOne(a)ошибка, MyInt — отдельный тип.
  • plusOne(b)OK, MyIntAlias это int.
var x int8 = 100
y := x * x
fmt.Println(y) // ?

Решение: Результат int8 * int8 = int8. 100*100 = 10000, не влезает в int8 (max 127). Wrap around: 10000 % 256 = 16, со знаком — может быть отрицательное. Реально: 10000 - 39*256 = 16, но т.к. знаковое, 16 — итог.

Чтобы избежать: int(x) * int(x) = 10000.


  1. The Go Programming Language Specificationhttps://go.dev/ref/spec (раздел Types, Constants, Conversions).
  2. Go Blog — Constants (Rob Pike) — https://go.dev/blog/constants — фундаментальное объяснение untyped constants.
  3. Dave Cheney — Go internalshttps://dave.cheney.net/category/golang — серия постов про размеры, layout, escape analysis.
  4. Habr — “Скрытые особенности типов в Go” (2024-2025) — поищите свежие обзоры.
  5. Effective Gohttps://go.dev/doc/effective_go — раздел про объявления и nil.
  6. go vet — встроенный линтер ловит string(int) без rune-конверсии, printf несоответствия и др.
  7. fieldalignmentgolang.org/x/tools/go/analysis/passes/fieldalignment — оптимизация порядка полей.
  8. Russ Cox — Go Data Structureshttps://research.swtch.com/godata — низкоуровневое представление типов.

Знание этого файла отличает джуна, который “слышал про Go” от того, кто понимает фундамент.