1.1Типы и базовые конструкции
Разберём, что копирует присваивание и как тип задаёт допустимые операции. Затем свяжем zero value, переполнение, явные конверсии, константы и выравнивание структур с правилами языка.
Переменная, присваивание, указатель
Считай, что вся глава про один вопрос: если я где-то поменяю значение, увидит ли это изменение кто-то другой?
1. Переменная - это кусочек памяти с именем
Когда ты пишешь var x int64, программа откладывает в
памяти кусочек размером 8 байт и вешает на него ярлык
x. Размер кусочка задаёт тип:
bool занимает 1 байт, int64 - 8, структура из
двух int64 - 16. Компилятор знает размер заранее, ещё до
запуска, поэтому и отводит ровно столько места, сколько нужно.
2. Присваивание - это копирование байтов из одного кусочка в другой
b := a означает буквально: «взять байты, которые
лежат в кусочке a, и записать их в кусочек
b». Два разных кусочка памяти, побайтовое копирование
из первого во второй. С аргументом функции то же самое: у неё появляется свой кусочек
памяти под параметр, и туда копируются байты из аргумента.
Отсюда и ответ на главный вопрос: раз кусочки разные, изменение одного не трогает другой. Загвоздка в одном: всё зависит от того, что именно лежало в скопированных байтах.
3. Указатель - переменная, в которой лежит не значение, а адрес
Иногда в кусочке памяти лежат не «полезные» данные, а
адрес другого кусочка: число, которое говорит «нужное лежит вон
там». Такая переменная называется указателем (тип пишется
как *T, взять адрес - &x, сходить по
адресу - *p).
Аналогия: значение - квартира, указатель - бумажка с её адресом. Скопировав бумажку, ты получил вторую бумажку, но квартира осталась одна. Придёшь по любой из двух бумажек, попадёшь в одну и ту же квартиру, и следы чужого ремонта увидишь по обеим.
Система типов: что вообще есть
Все типы Go делятся на четыре группы, и отличаются они ровно одним: что лежит в том самом кусочке памяти, который скопируется при присваивании.
| Группа | Типы | Что лежит в кусочке памяти | Что будет при b := a |
|---|---|---|---|
| Базовые |
bool, числовые (int*,
uint*, float*),
byte, rune
|
Само число или флаг - и всё | Полностью независимая копия |
| Составные | array, struct |
Все поля или элементы подряд, одним блоком | Копируется весь блок целиком. Структура из 100 полей - скопируются все 100 |
| С указателем внутри |
slice, map, chan,
func, *T
|
Маленькая служебная структурка, и внутри неё - адрес настоящих данных | Копируется структурка с адресом. Данные остаются одни на двоих |
| Строки | string |
Адрес байтов + длина. Сами байты лежат отдельно и неизменяемы | Копируется адрес и длина, но менять строку всё равно нельзя - поэтому «одни на двоих» тут безопасно |
| Интерфейсы | any, именованные интерфейсы |
Два адреса: «какой тут лежит тип» и «где само значение» | Копируются оба адреса. Разбирается подробно в главе 1.5 |
В Go нет передачи по ссылке. Вообще. Присваиваешь или передаёшь в функцию: байты уезжают в новый кусочек памяти. Так происходит каждый раз, без исключений. Вопрос только в том, что было в этих байтах: само значение, и копия независима; адрес, и две переменные смотрят на одни и те же данные.
Заодно про жаргон. В книгах и на собесах говорят «значимые типы» и «ссылочные типы»: термины пришли из C# и Java. В спецификации Go их нет. Если интервьюер спрашивает «какие типы в Go ссылочные», сильный ответ звучит так: «формально таких нет, Go всегда копирует значение; но обычно ссылочными называют слайсы, мапы, каналы, функции и указатели, потому что внутри у них лежит адрес, и копия видит те же данные».
Целые числа: размер и переполнение
int и uint -
платформозависимые типы: они шириной в машинное слово
целевой архитектуры. На всех современных сборках (amd64,
arm64) это 64 бита, на 32-битных (386, arm) - 32.
Смотри strconv.IntSize или
unsafe.Sizeof(int(0)). При этом int,
int64 и int32 - это
три разных типа, между которыми нет неявного приведения,
даже когда их размеры совпадают.
var a int = 5
var b int64 = 10
// _ = a + b // ошибка компиляции:
// // invalid operation: a + b (mismatched types int and int64)
_ = int64(a) + b // ок - явная конверсия
// размер int зависит от платформы:
fmt.Println(strconv.IntSize) // 64 на amd64/arm64
fmt.Println(unsafe.Sizeof(int(0))) // 8
Переполнение целых в Go не паникует и не проверяется в
рантайме: это арифметика по модулю
2^N («заворачивается»). Знаковые прыгают с максимума
на минимум, беззнаковые с нуля на максимум.
var i8 int8 = 127
i8++ // -128, без паники
var u uint8 = 0
u-- // 255
// В проде чаще всего подводит беззнаковая арифметика:
var n uint = 3
for i := n; i >= 0; i-- { // бесконечный цикл: uint никогда не станет < 0
_ = i
}
// И вторая: разность длин в uint
a, b := len(x), len(y) // len возвращает int, тут всё ок
var la, lb uint = 2, 5
_ = la - lb // 18446744073709551613, а не -3
«Где здесь бесконечный цикл?» И показывают
for i := uint(n); i >= 0; i--. Ответ:
беззнаковое всегда >= 0, условие никогда не
станет ложным. Второй вариант того же вопроса:
for i := 0; i < len(s)-1; i++. Если
s пустой, то len(s)-1 == -1, а
int знаковый, так что цикл просто не выполнится. А
будь длина беззнаковой, полезли бы по огромному индексу.
Компилятор ловит переполнение только у констант, потому что константы вычисляются с произвольной точностью на этапе компиляции:
const big = 1 << 62 // ок, нетипизированная константа: побитовый сдвиг "1 << 62" даёт 2⁶²
var x int8 = 200 // ошибка компиляции: constant 200 overflows int8
var y = big * 4 // ошибка: constant overflows int (при присвоении в int)
Zero values: почему в Go нет мусора в памяти
Любая объявленная переменная гарантированно инициализирована.
Рантайм обнуляет выделенную память (memclr), и на
стеке, и в куче. Это решение сознательное: оно убирает целый
класс багов «использование неинициализированной переменной»,
делает var mu sync.Mutex сразу готовым к работе, а
var buf bytes.Buffer - валидным. Формулировка для
собеса: в Go нулевое значение должно быть полезным (zero
value is useful), это принцип дизайна стандартной библиотеки.
| Тип | Zero value | Можно ли пользоваться сразу |
|---|---|---|
bool |
false |
да |
| числовые | 0 / 0.0 |
да |
string |
"" (не nil!) |
да, len == 0 |
*T, func, interface
|
nil |
нет - разыменование/вызов паникует |
slice |
nil |
да частично: len, cap,
range, append работают
|
map |
nil |
только чтение: чтение даёт zero value, запись - паника |
chan |
nil |
нет - чтение и запись блокируются навсегда |
array |
массив из zero values | да |
struct |
структура, все поля обнулены | да (если так спроектирована) |
Слово nil в Go не обозначает один объект. Это
нулевое значение для шести разных категорий типов, и ведут они
себя по-разному. Классический добивающий вопрос: «а
nil слайс и nil мапа одинаково себя
ведут?» Нет: в nil-слайс можно append,
в nil-мапу нельзя писать.
byte и rune
Оба - псевдонимы, не самостоятельные типы: type byte = uint8 и
type rune = int32 объявлены через =, то
есть это буквально одно и то же имя для одного типа. Поэтому
byte и uint8 взаимозаменяемы без
конверсии, а вот type MyByte uint8 (без
=) создал бы новый тип, требующий явного приведения.
-
byte- один октет. Из них состоят строки, содержимое[]byte, элементы I/O. -
rune- одна кодовая точка Unicode, 32 бита. Это не «символ», каким его видит человек: составные эмодзи и буквы с диакритикой могут состоять из нескольких рун.
s := "Привет"
fmt.Println(len(s)) // 12 - байты, кириллица по 2 байта в UTF-8
fmt.Println(utf8.RuneCountInString(s)) // 6 - руны
fmt.Printf("%T %v\n", s[0], s[0]) // uint8 208 - байт, не буква
fmt.Printf("%T %c\n", []rune(s)[0], []rune(s)[0]) // int32 П
Приведение типов: почему всё явно
В Go есть конверсия (T(v)) и нет неявного
приведения между разными типами, даже если базовое представление у
них одинаковое. Причина:
код предсказуем, скрытых потерь нет. В C/C++/Java
правила неявных промоушенов занимают страницы спецификации и
регулярно рождают баги вида «поделили int на int и получили 0»,
«сравнили signed с unsigned». Go эту область просто удалил.
type UserID int64
type OrderID int64
var u UserID = 7
var o OrderID = u // ошибка компиляции: cannot use u (variable of int64
// type UserID) as OrderID value in variable declaration
// ради этого отдельные типы и заводят
var o2 OrderID = OrderID(u) // явно, на ревью сразу видно
// Конверсия может молча терять данные:
var big int32 = 300
var small int8 = int8(big) // 44, старшие биты отброшены, ошибки нет
// Конверсия float→int обрезает, а не округляет:
f := 2.99
fmt.Println(int(f)) // 2
fmt.Println(int(-f)) // -2
// Но только для переменной. int(2.99) от нетипизированной константы -
// ошибка компиляции: cannot convert 2.99 (untyped float constant) to type int
T(v) - конверсия, работает на конкретных типах,
проверяется компилятором. v.(T) - type assertion,
работает только с интерфейсными значениями и проверяется в
рантайме. Путать их на собесе - заметный минус.
Константы и iota
Константы в Go вычисляются на этапе компиляции и бывают
типизированными и нетипизированными.
Нетипизированная константа хранится с произвольной точностью
(минимум 256 бит по спецификации) и получает тип только в момент
использования. Поэтому и можно писать math.MaxInt64 и
1 << 62 без гимнастики с приведениями.
const Pi = 3.14159 // нетипизированная float-константа
const TypedPi float64 = 3.14 // типизированная
var f float32 = Pi // ок: нетипизированная подстроится
// var g float32 = TypedPi // ошибка компиляции: cannot use TypedPi (constant
// // 3.14 of type float64) as float32 value
const Huge = 1 << 100 // ок, пока не присваиваем в переменную
fmt.Println(Huge >> 98) // 4 - вычислено компилятором
iota - счётчик внутри блока const. Он
равен индексу строки (ConstSpec) в блоке, начиная с нуля, и
сбрасывается на каждом новом const (. Тонкость: если
у строки нет своего выражения, повторяется выражение предыдущей
строки. Так и работают «пустые» строки в перечислении.
type Weekday int
const (
Sunday Weekday = iota // 0
Monday // 1 - выражение унаследовано
Tuesday // 2
)
// Пропуск значений через _
const (
_ = iota // 0 отбрасываем
KB = 1 << (10 * iota) // 1 << 10 = 1024
MB // 1 << 20
GB // 1 << 30
)
// Битовые флаги
type Perm uint8
const (
Read Perm = 1 << iota // 1
Write // 2
Exec // 4
)
p := Read | Write
fmt.Println(p&Write != 0) // true
Значения привязаны к порядку строк. Вставили новую
константу в середину - все последующие значения сдвинулись. Если
эти числа попадают в БД, в API или в сообщения брокера, это
тихая порча данных. Правило: для внешне видимых перечислений
задавай значения явно либо добавляй новые только в конец, и
обязательно закрывай тестом (например, golden-тест на
String() из stringer).
switch: чем отличается от C-подобных языков
| Отличие | Как в Go |
|---|---|
| Провал в следующий case |
Нет по умолчанию. Есть явный
fallthrough (передаёт управление в тело
следующего case без проверки его условия)
|
| Тип выражения |
Любой сравнимый тип, включая string, а не
только целые
|
| Несколько значений в case | case 1, 2, 3: |
| Switch без выражения |
switch { case x > 10: ... } - заменяет
цепочку if/else if
|
| Инициализатор |
switch v := f(); v { - v живёт
только внутри switch
|
| Type switch |
switch v := i.(type) { - работает только с
интерфейсами
|
break |
Не нужен для выхода из case. Нужен, чтобы прервать цикл извне switch - тогда с меткой |
// break внутри switch выйдет из switch, а не из цикла, тут часто ошибаются
loop:
for _, ev := range events {
switch ev.Kind {
case KindStop:
break loop // без метки прервали бы только switch
case KindSkip:
continue loop
}
handle(ev)
}
// switch без выражения идиоматично заменяет лестницу if
switch {
case age < 13:
return "child"
case age < 20:
return "teen"
default:
return "adult"
}
Структуры: сравнение и выравнивание
Структура comparable, если comparable все её поля. Не comparable: слайсы, мапы, функции, а значит и любая структура, которая их содержит. Массивы comparable, если comparable их элементы. Структуры сравниваются пополево, а не побайтово (иначе padding-байты давали бы ложные различия).
type Point struct {
X, Y int
}
type Bag struct {
Items []string
}
fmt.Println(Point{1, 2} == Point{1, 2}) // true
// _ = Bag{} == Bag{} // ошибка компиляции: invalid operation:
// // Bag{} == Bag{} (struct containing
// // []string cannot be compared)
// Обходной путь для «глубокого» сравнения:
reflect.DeepEqual(a, b) // медленно, но работает со всем
cmp.Equal(a, b) // github.com/google/go-cmp - стандарт де-факто в тестах
Выравнивание. Процессор читает память словами, поэтому
компилятор кладёт каждое поле по адресу, кратному его выравниванию
(int64 - по 8, int32 - по
4, bool - по 1), а между полями вставляет
padding. Размер всей структуры дополняется до кратного
максимальному выравниванию полей, чтобы в массиве структур каждый
следующий элемент тоже был выровнен. Go
не переупорядочивает поля сам: порядок в исходнике =
порядок в памяти.
fieldalignment находит такие структуры
автоматически.
$ go vet -vettool=$(which fieldalignment) ./...
$ fieldalignment -fix ./... # умеет переставлять поля автоматически
$ go run honnef.co/go/tools/cmd/structlayout@latest -json pkg Bad
Анонимные структуры и функции
// 1. Табличные тесты, самый частый случай
tests := []struct {
name string
in string
want int
err bool
}{
{"пусто", "", 0, true},
{"обычный", "42", 42, false},
}
// 2. Одноразовый JSON-ответ, ради которого нет смысла заводить тип
json.NewEncoder(w).Encode(struct {
Status string `json:"status"`
Count int `json:"count"`
}{"ok", n})
// 3. Множество (set) с нулевым оверхедом на значение
seen := map[string]struct{}{}
seen["a"] = struct{}{}
// 4. Анонимные функции: замыкания, defer, горутины, функциональные опции
defer func() {
if r := recover(); r != nil { log.Println("recovered:", r) }
}()
Пустая структура struct{} занимает 0 байт, а
все её экземпляры лежат по одному адресу (runtime.zerobase). Отсюда идиомы
map[T]struct{} для множества и
chan struct{} для сигнального канала: памяти под
значение не выделяется вообще.
Вопросы
10Четыре группы типов:
-
Базовые - bool, строки, числовые (
int,uint,float,complex), их псевдонимыbyteиrune. -
Составные -
arrayиstruct: фиксированный размер, элементы лежат подряд. -
Дескрипторные (их и называют «ссылочными») -
slice,map,chan,func, указатель*T. -
Интерфейсные -
anyи именованные интерфейсы: два слова (тип + данные).
Присвоил или передал в функцию: значение копируется
побитово. Для struct и
array копируются все данные, и получаются два
независимых объекта. Для слайса копируется заголовок из трёх
слов (ptr/len/cap), для мапы и канала - указатель на
hmap/hchan, поэтому данные
остаются общими.
type P struct {
X int
}
a := P{1}; b := a; b.X = 9
// a.X == 1 - копия независима
s1 := []int{1, 2, 3}; s2 := s1; s2[0] = 9
// s1[0] == 9 - общий массив
arr1 := [3]int{1, 2, 3}; arr2 := arr1; arr2[0] = 9
// arr1[0] == 1 - массив копируется целиком
«Термина reference type в спецификации Go нет, это жаргон. Формально Go всегда передаёт по значению, а
изменяемость через слайс или мапу - следствие того, что в
скопированном дескрипторе лежит указатель. Единственный
способ дать функции изменить саму переменную вызывающего -
передать *T.»
int - платформозависимый (64 бита
на amd64/arm64), и это отдельный тип от
int64, даже когда размеры совпадают.
Размер int/uint/uintptr
совпадает с машинным словом: 8 байт на amd64, arm64 и wasm, 4 байта на 386 и arm. Проверить:
strconv.IntSize или
unsafe.Sizeof(int(0)).
Смешивать нельзя даже при равном размере: компилятор
потребует явной конверсии. Совместимость определяет тип, а
не представление. На практике: в протоколах и БД фиксируй
int64/int32, а
int оставь для индексов, длин и внутренних
счётчиков.
Переполнение
Ни знаковое, ни беззнаковое переполнение не паникует: это
арифметика по модулю
2^N. Компилятор ловит только константное.
var i int8 = 127; i++ // -128
var u uint8 = 0; u-- // 255
// Ловушка №1: обратный цикл по uint
for i := uint(3); i >= 0; i-- { // бесконечный: uint всегда >= 0
}
// Ловушка №2: разность беззнаковых
var a, b uint = 2, 5
fmt.Println(a - b) // 18446744073709551613
// Ловушка №3: проверка переполнения после операции
c := math.MaxInt64
c++ // уже -9223372036854775808
if c < 0 { /* поздно, но так обычно и ловят */ }
Как правильно проверять переполнение
// знаковые - руками:
func addSafe(a, b int64) (int64, error) {
if (b > 0 && a > math.MaxInt64-b) || (b < 0 && a < math.MinInt64-b) {
return 0, errors.New("overflow")
}
return a + b, nil
}
// сейчас для беззнаковых есть math/bits:
sum, carry := bits.Add64(x, y, 0) // carry == 1 при переполнении
«Почему len() возвращает int, а
не uint, ведь длина не бывает отрицательной?»
Затем, чтобы выражения вида len(s)-1 и
обратные циклы вели себя предсказуемо. Роб Пайк объяснял
это так: беззнаковые типы в арифметике приносят больше
багов, чем экономят диапазона. uint в Go -
для битовых операций и протоколов, а не для
«неотрицательных чисел».
0 для чисел, false для bool,
"" для строк, nil для указателей,
функций, интерфейсов, слайсов, мап и каналов; для массивов и
структур - рекурсивно нулевые элементы/поля.
Почему так сделано
- Убирает класс UB из C: чтение мусора со стека невозможно.
-
Можно проектировать типы, готовые к работе «из коробки»:
var mu sync.Mutex,var buf bytes.Buffer,var wg sync.WaitGroup- все валидны без конструктора. - Упрощает JSON/десериализацию: отсутствующее поле = нулевое значение.
Цена и подводные камни
-
Неотличимо «не задано» от «задано нулём». Классика:
omitemptyвыброситfalseи0, хотя пользователь их явно прислал. Помогают указатели (*bool) илиsql.NullXxx/ собственный тип с флагом. -
nil-мапа читается, но не пишется: паникаassignment to entry in nil map. -
nil-канал блокирует навсегда (иногда это полезно: так «выключают» ветку в select).
var m map[string]int
fmt.Println(m["x"], len(m)) // 0 0 - чтение ок
m["x"] = 1 // panic: assignment to entry in nil map
var s []int
s = append(s, 1) // ок: append работает и с nil-слайсом
var ch chan int
<-ch // висит навсегда (deadlock, если спят все горутины)
byte = uint8,
rune = int32. byte - единица
хранения, rune - единица смысла (кодовая точка
Unicode).
Объявлены через =, то есть это те же самые типы
под другим именем, а не новые типы. Поэтому
var b byte = uint8(5) компилируется без
конверсии.
Go хранит строки в UTF-8. По индексу получишь
байт, а range по строке выдаст
руну и её байтовое смещение. Это главный источник
ошибок с не-ASCII текстом.
s := "Go♥"
fmt.Println(len(s)) // 5: 'G','o' по 1 байту, '♥' - 3 байта
fmt.Printf("%T\n", s[0]) // uint8
for i, r := range s {
fmt.Printf("%d:%c(%d) ", i, r, r) // 0:G(71) 1:o(111) 2:♥(9829)
}
fmt.Println(utf8.RuneCountInString(s)) // 3
rs := []rune(s) // аллоцирует 3 × 4 = 12 байт
fmt.Println(len(rs)) // 3
Руна - это code point, а не grapheme cluster. Флаг «🇷🇺» -
две руны, «é» может быть одной руной (U+00E9) или двумя (e
+ U+0301). Для «символов как их видит человек» нужна итерация по графемным кластерам, например библиотекой
rivo/uniseg, а golang.org/x/text/unicode/norm
только приводит «é» к одной форме. На собесе достаточно этот нюанс
упомянуть: видно, что ты понимаешь границы модели.
Единственное «неявное» поведение - нетипизированные
константы, которые подстраиваются под контекст. Всё
остальное требует T(v).
Аргументы, которые стоит назвать
-
Читаемость. Глядя на
int64(x) * y, ты сразу видишь и стоимость, и намерение. В C всё это прошло бы молча. -
Именованные типы как защита.
type UserID int64иtype OrderID int64нельзя перепутать, компилятор поймает. С неявными приведениями от этой защиты ничего не остаётся. -
Нет signed/unsigned сюрпризов. В C при сравнении
intсunsignedпервое продвигается в беззнаковое, отсюда классические уязвимости. - Простота спецификации. Меньше правил - меньше расхождений между компиляторами и меньше кейсов на ревью.
Что конверсия всё же делает молча
// От константы такие конверсии не скомпилируются,
// компилятор ловит их ещё при сборке:
// int8(300) → constant 300 overflows int8
// int(2.99) → cannot convert 2.99 (untyped float constant) to type int
// uint(-1) → constant -1 overflows uint
// А вот от переменной те же конверсии проходят молча:
var a int32 = 300
var b float64 = 2.99
var c int = -1
fmt.Println(int8(a)) // 44 - старшие биты отрезаны молча
fmt.Println(int(b)) // 2 - обрезает, а не округляет
fmt.Println(int(-b)) // -2 - к нулю, а не «вниз»
fmt.Println(uint(c)) // 18446744073709551615
string(65) // "A" - go vet ругается: подозрительная конверсия int→string
string([]byte{65}) // "A" - а так правильно
-
T(v)- конверсия, компайл-тайм, конкретные типы. -
i.(T)- type assertion, рантайм, только интерфейсы. -
switch v := i.(type)- type switch, рантайм, ветвление по динамическому типу.
iota - индекс строки внутри блока
const, считает с 0 и сбрасывается на каждом
новом блоке. Пропущенное выражение повторяется с предыдущей
строки.
const (
A = iota // 0
B // 1 - выражение "iota" унаследовано
C // 2
)
const (
D = iota // 0 - новый блок, счётчик сброшен
)
// iota считает строки, а не константы:
const (
X, Y = iota, iota * 10 // X=0, Y=0
Z, W // Z=1, W=10
)
Идиомы
// 1) Перечисление статусов, строки для них сгенерирует stringer
type Status int
const (
StatusUnknown Status = iota
StatusPending
StatusDone
)
//go:generate stringer -type=Status
// 2) Размеры
const (
_ = iota
KB = 1 << (10 * iota) // 1024
MB // 1048576
)
// 3) Битовые маски
type Flag uint
const (
FlagRead Flag = 1 << iota
FlagWrite
FlagExec
)
// 4) Пропуск ненужных значений
const (
_ = iota
First // 1 - чтобы zero value означал "не задано"
Second
)
Начинай перечисление с _ = iota или заводи
явный StatusUnknown = 0, чтобы нулевое
значение означало «не заполнено», а не какой-то
легальный статус. Иначе забытое поле в структуре молча
становится валидным первым статусом. Типичный баг на
проде.
Значения зависят от порядка строк. Если константы уезжают в БД/API/Kafka, вставка новой строки в середину меняет смысл уже сохранённых данных. Для внешних контрактов значения задают явно.
-
Нет провала. Тело case отработало, switch
закончился. Явный
fallthroughпередаёт управление в тело следующего case, не проверяя его условие, и обязан быть последним оператором в case. -
breakне нужен для выхода из case. Внутри циклаbreakвыйдет из switch, а чтобы прервать цикл, нужна метка. - Любые типы: строки, структуры (если comparable), не только целые.
-
Несколько значений в одном case:
case "a", "b":. -
Switch без выражения - эквивалент
switch true, идиоматичная замена лестницеif / else if. -
Инициализатор:
switch v, err := f(); {, область видимости ограничена switch. -
Type switch:
switch v := i.(type)- только по интерфейсам. Внутри каждого casevполучает свой конкретный тип; в case с несколькими типамиvостаётся интерфейсом. - Порядок проверки - сверху вниз, первый совпавший case побеждает (в отличие от C, где switch может компилироваться в jump table и порядок не важен).
switch v := x.(type) {
case nil:
fmt.Println("nil интерфейс")
case int, int64:
fmt.Printf("целое, но v тут ещё any: %T\n", v)
case fmt.Stringer:
fmt.Println(v.String()) // v уже fmt.Stringer
default:
fmt.Printf("%T\n", v)
}
switch { // без выражения
case n < 0: return "neg"
case n == 0: return "zero"
default: return "pos"
}
switch n {
case 1:
fmt.Println("один")
fallthrough // безусловно выполнит тело case 2
case 2:
fmt.Println("два")
}
// при n == 1 вывод:
// один
// два
for i := 0; i < 3; i++ {
switch i {
case 1:
break // выходит из switch, НЕ из цикла
}
fmt.Print(i)
}
012. Чтобы прервать цикл, нужен
break label.
Отличия от переменных
- Вычисляются компилятором; в бинарнике их «нет», они подставлены в места использования.
-
Нельзя взять адрес:
&MaxSize- ошибка компиляции. -
Могут быть только базовых типов: булев, строковый,
числовой. Нельзя
const s = []int{1}илиconst t = time.Now(). - Не участвуют в escape analysis и не создают аллокаций.
Нетипизированные константы
Константа без явного типа хранится как «идеальное» значение произвольной точности (спецификация требует минимум 256 бит мантиссы) и приобретает тип только там, где используется: по правилу «тип по умолчанию» или по контексту.
const c = 1 << 100 // ок, пока не присвоено переменной
fmt.Println(c >> 98) // 4
const half = 1 / 2 // 0 - целочисленное деление констант
const halfF = 1.0 / 2 // 0.5 - одна из констант вещественная
var f float64 = 3 // 3 подстроится под float64
var d time.Duration = 5 // ок: нетипизированная 5 станет Duration
var n int = 5
// var d2 time.Duration = n // ошибка: int → Duration неявно нельзя
// Типы по умолчанию, если контекста нет:
x := 42 // int
y := 4.2 // float64
z := 'a' // rune (int32)
s := "hi" // string
b := true // bool
Из-за нетипизированных констант
time.Second * 5 работает, а
time.Second * n (где n int) -
нет. И поэтому math.MaxUint64 можно объявить
константой, хотя она не влезает в int64: до
присваивания у неё просто нет типа.
Comparable
- Comparable: числа, строки, bool, указатели, каналы, интерфейсы, массивы из comparable, структуры из comparable.
-
Не comparable:
slice,map,funcи всё, что их содержит. Сравнение сnilдля них разрешено, друг с другом - нет. - Только comparable-типы могут быть ключами мапы, это то же ограничение.
type A struct {
X int
S string
} // comparable
type B struct {
Items []int
} // не comparable
_ = A{1,"a"} == A{1,"a"} // true
// _ = B{} == B{} // compile error
// С интерфейсным полем структура comparable, но в рантайме может запаниковать:
type C struct {
V any
}
_ = C{1} == C{1} // true
_ = C{[]int{1}} == C{[]int{1}} // panic: comparing uncomparable type []int
Выравнивание
Каждое поле кладётся по адресу, кратному его выравниванию
(обычно = размеру: int64→8, int32→4, bool→1). Между полями
появляется padding. Размер структуры округляется вверх до
кратного максимальному выравниванию её полей, чтобы в
[]T все элементы были выровнены.
type Bad struct { // 24 байта
a bool // 1 + 7 padding
b int64 // 8
c bool // 1 + 7 padding (добивка до кратного 8)
}
type Good struct { // 16 байт
b int64 // 8
a bool // 1
c bool // 1 + 6 padding
}
unsafe.Sizeof(Bad{}) // 24
unsafe.Sizeof(Good{}) // 16
unsafe.Alignof(Bad{}) // 8
unsafe.Offsetof(Bad{}.b) // 8
Когда это реально важно
- Структура живёт миллионами экземпляров (кэш, индекс в памяти, элементы большого слайса). Тогда −33 % размера дают и меньше давления на GC, и лучшую локальность в L1/L2.
-
False sharing в конкурентном коде: два часто изменяемых
поля попадают в одну 64-байтовую кэш-линию, ядра начинают
гонять её друг у друга. Лечат вручную: padding или
[64]byte-заполнитель между полями.
Клади поля от «широких» к «узким»: указатели и int64 →
int32 → int16 → bool. Такие структуры находит линтер
fieldalignment (входит в
golangci-lint как часть govet),
он же их чинит флагом -fix. Но
не начинай ответ с этого: сначала измерь, потом
переставляй поля.
Анонимные структуры
- Табличные тесты - самый частый и самый идиоматичный случай.
- Одноразовый JSON на входе или выходе хендлера, когда тип не нужен в домене.
-
Множества:
map[string]struct{}, где значение занимает 0 байт. -
Сигнальные каналы:
chan struct{}, где важен только факт события. -
Группировка полей внутри конфига:
DB struct{ Host string; Port int }прямо в структуреConfig.
cfg := struct {
Host string
Port int
}{"localhost", 8080}
var payload struct {
Email string `json:"email"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { ... }
Анонимные функции (литералы функций)
- Замыкания захватывают переменные окружения по ссылке на переменную.
-
deferс логикой:defer func(){ ... }(). - Горутины:
go func(){ ... }(). -
Функциональные опции:
func WithTimeout(d time.Duration) Option { return func(c *Client){ c.timeout = d } }. -
Middleware: обёртка, возвращающая
http.Handler. - Немедленный вызов ограничивает область видимости переменных.
// функциональные опции: на собесах по архитектуре их спрашивают чаще всего
type Option func(*Server)
func WithTimeout(d time.Duration) Option { return func(s *Server) { s.timeout = d } }
func WithLogger(l *slog.Logger) Option { return func(s *Server) { s.log = l } }
func New(addr string, opts ...Option) *Server {
s := &Server{addr: addr, timeout: 5 * time.Second}
for _, o := range opts { o(s) }
return s
}
- У анонимной структуры нельзя объявить методы, только поля.
- Две анонимные структуры с одинаковым набором полей и тегов - один и тот же тип, их можно присваивать друг другу.
- Анонимные структуры в сигнатурах публичных функций - плохая идея: вызывающему негде записать этот тип.
Что стоит запомнить
- Тип определяет набор операций и правила совместимости, а не только размер.
- Присваивание копирует значение; указатели и ссылочные типы могут сохранить общий объект.
- Конверсия чисел может терять диапазон без ошибки.
Частые ошибки
- Не путай размер типа с размером объекта, на который он ссылается.
- Signed overflow нельзя использовать как проверку границы.
Самопроверка
Сначала реши задачу самостоятельно, затем открой ответ и сравни ход рассуждений.
Чтение кода
1. Проследи значения
Что напечатает фрагмент? Объясни, почему получился такой результат.
x := int8(127)
x++
fmt.Println(x)
Ответ и разбор
Печатается -128: арифметика int8 остаётся в пределах 8 бит, увеличение максимального значения переходит к минимальному.
Код-ревью
2. Найди ошибку и исправь
Значение count вышло за диапазон int8. Как безопасно преобразовать его, сохранив корректность?
var count int64 = 300
var small int8 = int8(count)
fmt.Println(small)
Ответ и разбор
Преобразование в Go не сообщает об усечении: здесь small станет 44. Сначала проверь диапазон, затем конвертируй: if count < math.MinInt8 || count > math.MaxInt8 { return err }; small := int8(count). Для API лучше вернуть ошибку переполнения, чем принять изменённое значение.
Применение
3. Реши задачу
Функция копирует Point со значениями x=2 и y=7 в другую переменную, после чего меняет копию. Какое значение останется у исходной структуры?
Ответ и разбор
Структура присваивается целиком по значению. Изменение копии не затрагивает исходную, поэтому исходные поля останутся x=2 и y=7.
Применение
4. Реши задачу
Счётчик байтов файла хранится как int32, а файл может быть больше 2 GiB (2³¹ байт). Что изменить и какую проверку оставить при конверсии из int64?
Ответ и разбор
Храни размер в int64 на всём пути. При передаче в API, требующий int, проверь допустимый диапазон для целевой платформы до конверсии. Не полагайся на переполнение или на одинаковую ширину int на всех архитектурах.