1. Зачем нужны «свои типы», если есть int
Когда вы только начинаете программировать, int кажется универсальным молотком: им можно забить любой гвоздь, а если гвоздь — строка, то можно и строкой забить (не спрашивайте как). Проблема в том, что в реальных программах разные числа означают разные вещи: «статус задачи», «уровень доступа», «код ошибки», «количество дней». И если всё это хранить просто как int, компилятор не сможет защитить вас от случайного смешивания смыслов.
Представьте, что у нас есть учебное консольное приложение TaskBox (мы его постепенно «прокачиваем» примерами). Пока оно очень простое: мы читаем из ввода id задачи и её статус (числом), а затем печатаем понятное сообщение.
С «голым int» легко написать что-то странное и даже не заметить:
package main
import "fmt"
func main() {
var taskID int = 42
var status int = 2
// Ой… случайно перепутали местами:
status = taskID
fmt.Println(status) // 42 (и компилятор не возмущается)
}
С точки зрения компилятора всё легально: оба значения — int. С точки зрения смысла — это тихая катастрофа (как минимум, странный баг).
И вот здесь появляются пользовательские типы на базе чисел: мы создаём новый тип, который по сути хранится как число, но имеет отдельный смысл и отдельные правила совместимости.
2. Как работает type Status int
Что создаёт type Status int
Сейчас важно не «проскочить» мимо сути. Строка type Status int — это не переменная и не константа. Это объявление нового типа. У него будет то же «внутреннее представление», что у int, но для компилятора это другая сущность, и он перестанет позволять вам смешивать её с любым int без явного разрешения.
Такой приём в Go — абсолютно нормальная практика. Даже в официальных материалах часто встречается идея «смыслового» числового типа вроде type Pill int (тип «таблетка» как число), чтобы затем задавать набор именованных значений.
Минимальный пример:
package main
import "fmt"
func main() {
type Status int
var s Status = 1
fmt.Printf("%T %v\n", s, s) // main.Status 1
}
Обратите внимание: тип выводится как main.Status, а не int. Это не косметика — это подсказка компилятора: «друг, это отдельный тип».
Status и int — разные типы
Теперь самое полезное: покажем, как именно Go начинает защищать вас.
Сделаем Status и попробуем присвоить ему обычный int без конверсии:
package main
import "fmt"
func main() {
type Status int
var code int = 2
var s Status = code // так нельзя: разные типы
fmt.Println(code) // 2
}
Эта строка (var s Status = code) не скомпилируется. И это хорошо: компилятор заставляет вас проговорить вслух (в коде): «Да, я осознанно превращаю int в Status».
То же самое в обратную сторону:
package main
import "fmt"
func main() {
type Status int
var s Status = 2
var code int = s // и так нельзя
fmt.Println(s) // 2
}
Да, выглядит «строго». Но эта строгость — как ремень безопасности: сначала давит, потом спасает.
Кстати, это ровно то, чем type definition отличается от «просто другого имени». В Go есть также алиасы типов, но type Status int создаёт именно новый (defined) тип со своими правилами присваивания.
Явная конверсия: Status(code) и int(s)
Когда компилятор запретил нам прямое присваивание, следующий шаг — научиться делать явную конверсию. Мы с этим уже сталкивались раньше, когда переводили между числовыми типами. Здесь тот же принцип: в Go нет «автоприведения» — вы явно пишете, что происходит.
Пример: читаем число как int, превращаем в Status.
package main
import "fmt"
func main() {
type Status int
var code int = 2
var s Status = Status(code) // явная конверсия
fmt.Printf("code=%d (%T)\n", code, code) // code=2 (int)
fmt.Printf("s=%d (%T)\n", s, s) // s=2 (main.Status)
}
И обратная конверсия:
package main
import "fmt"
func main() {
type Status int
var s Status = 2
code := int(s) // превращаем обратно в int
fmt.Printf("code=%d (%T)\n", code, code) // code=2 (int)
}
Почему это не «лишняя возня»? Потому что в момент чтения кода вы видите смысловую границу. Это как табличка «осторожно, дальше другой режим»: вы не перепутаете «статус» и «id задачи», если они разных типов.
Именованные константы для Status
Когда в коде начинают появляться числа 0, 1, 2, очень быстро вы встречаете классический баг: через неделю никто не помнит, что такое 2. «Вроде done? Или in progress? Или “всё сломалось”?» — и начинается археология.
Правильнее дать этим значениям имена через const. Сейчас мы сделаем это без iota, потому что iota — отдельная тема следующей лекции. Здесь мы пишем значения явно, чтобы всё было максимально прозрачно.
package main
import "fmt"
func main() {
type Status int
const (
StatusNew Status = 0
StatusInProgress Status = 1
StatusDone Status = 2
)
var s Status = StatusDone
fmt.Println(s) // 2
}
Обратите внимание на две вещи.
Первая: константы типизированы как Status, а не как int. Это помогает компилятору продолжать защищать нас.
Вторая: теперь сравнения можно писать по-человечески:
package main
import "fmt"
func main() {
type Status int
const StatusDone Status = 2
var s Status = 2
if s == StatusDone {
fmt.Println("task is done") // task is done
}
}
Да, s печатается как 2, но в коде вы читаете не «2», а StatusDone. Смысл важнее цифры.
Пример TaskBox: ввод статуса и вывод текста
Сейчас соберём небольшой кусок реального кода, который выглядит как часть нормального приложения. Напомню: у нас пока нет структур и функций (кроме main), поэтому делаем всё прямолинейно.
Мы хотим такое поведение: пользователь вводит taskID и statusCode, а программа печатает понятный текст. Здесь как раз удобно применить пользовательский тип Status, чтобы не таскать “сырые int” по всей логике.
package main
import (
"fmt"
)
func main() {
type Status int
const (
StatusNew Status = 0
StatusInProgress Status = 1
StatusDone Status = 2
)
var taskID int
var statusCode int
fmt.Scan(&taskID, &statusCode)
status := Status(statusCode) // явное превращение int → Status
if status == StatusNew {
fmt.Printf("task #%d: new\n", taskID) // например: task #10: new
} else if status == StatusInProgress {
fmt.Printf("task #%d: in progress\n", taskID)
} else if status == StatusDone {
fmt.Printf("task #%d: done\n", taskID)
} else {
fmt.Printf("task #%d: unknown status (%d)\n", taskID, statusCode)
}
}
Здесь есть важная привычка, которую стоит прочувствовать.
Мы не доверяем входному числу. Мы его конвертируем в Status, но всё равно считаем, что оно может быть «мусором», и поэтому держим ветку else для неизвестных значений.
Почему это важно? Потому что сам факт Status(statusCode) не гарантирует, что значение «валидное». Конверсия говорит только: «Это число теперь рассматриваем как статус». Но не говорит: «Это статус из нашего списка».
Почему Status иногда сравнивается с числом
Сейчас будет момент «вроде магия, но на самом деле правила».
В Go существуют untyped константы. Они умеют «подстраиваться под контекст» — вы видели это в лекции про typed/untyped константы.
Из-за этого такой код часто компилируется:
package main
import "fmt"
func main() {
type Status int
var s Status = 2
if s == 2 {
fmt.Println("looks like done") // looks like done
}
}
Почему так? Потому что 2 здесь — нетипизированная константа, и компилятор может «примерить» её как Status (если значение представимо). Это иногда удобно в мини-примерах, но в реальном коде быстро превращается в «магические числа» и ухудшает читаемость.
Для учебной дисциплины и будущих проектов лучше держать правило: сравниваем только с именованными константами:
package main
import "fmt"
func main() {
type Status int
const StatusDone Status = 2
var s Status = 2
if s == StatusDone {
fmt.Println("done") // done
}
}
Смысл читается глазами, а не воспоминаниями «что там за 2 было в прошлом месяце».
3. Когда нужен свой числовой тип
Полезно мысленно разделять «просто числа» и «числа со смыслом». type Status int нужен именно во второй категории.
Ниже небольшая таблица (не для зубрёжки, а чтобы было проще принимать решения):
| Ситуация | Что выбрать | Почему |
|---|---|---|
| Считаем сумму, разницу, количество элементов, минуты/секунды в вычислении | |
Это «математика», смысл обычно очевиден в контексте формулы |
| Храним код состояния (статус задачи, уровень доступа, режим работы) | |
Компилятор защищает от смешивания смыслов, код читабельнее |
| Храним «идентификатор» (id пользователя, id задачи) | |
Чисто технически тоже число, но смысл другой; смешивать с другими числами опасно |
| Читаем значение из ввода и используем как «смысловой код» | |
Ввод — это сырые данные, а логика — смысловые типы |
В нашем TaskBox мы уже почувствовали выигрыш: статус перестал быть «просто числом», и теперь у него есть отдельное имя и отдельные правила.
4. Типичные ошибки
Ошибка №1: ожидать, что type Status int — это «просто другое имя для int».
Новички часто думают, что это как псевдоним: мол, раз внутри всё равно число, то можно без проблем присваивать int в Status и обратно. Но определение типа в Go специально сделано строгим: Status и int — разные типы, и компилятор не даст их смешивать. Это не вредность языка, а защита от случайных ошибок.
Ошибка №2: делать конверсию и считать, что теперь значение автоматически «валидное».
Конструкция Status(code) не проверяет, что code равен 0, 1 или 2. Она просто меняет «ярлык» типа. Поэтому при чтении из ввода (или из любой внешней системы) всегда нужен план на случай неизвестного значения: ветка else или default (если используете switch).
Ошибка №3: продолжать сравнивать статусы с голыми числами (if s == 2).
Иногда это компилируется из-за untyped констант, и создаётся ощущение «ну значит нормально». Но смысл теряется: через пару дней 2 превращается в загадку. Если уж мы завели смысловой тип, логично довести мысль до конца и сравнивать с StatusDone, StatusNew и так далее.
Ошибка №4: смешивать разные «категории чисел» в одной переменной.
Например, хранить в переменной status то статус, то id, то «что пришло из ввода», потому что «всё равно int». Это почти всегда рождает баги, которые выглядят как случайность. Пользовательские типы как раз помогают держать порядок: отдельно «что пришло» (int), отдельно «смысл в программе» (Status).
Ошибка №5: делать слишком общие имена (type Code int).
Если назвать тип просто Code, он быстро начнёт означать «всё на свете» и потеряет пользу. Лучше выбирать имя, которое отражает смысл: Status, Perm, TaskState. Тогда код читается без перевода с «числового» на «человеческий».
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ