JavaRush /Курсы /Go SELF /Пользовательские типы на базе чисел

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

Go SELF
8 уровень , 2 лекция
Открыта

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 нужен именно во второй категории.

Ниже небольшая таблица (не для зубрёжки, а чтобы было проще принимать решения):

Ситуация Что выбрать Почему
Считаем сумму, разницу, количество элементов, минуты/секунды в вычислении
int
Это «математика», смысл обычно очевиден в контексте формулы
Храним код состояния (статус задачи, уровень доступа, режим работы)
type Status int
Компилятор защищает от смешивания смыслов, код читабельнее
Храним «идентификатор» (id пользователя, id задачи)
type TaskID int
Чисто технически тоже число, но смысл другой; смешивать с другими числами опасно
Читаем значение из ввода и используем как «смысловой код»
int → конверсия → свой тип
Ввод — это сырые данные, а логика — смысловые типы

В нашем 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. Тогда код читается без перевода с «числового» на «человеческий».

1
Задача
Go SELF, 8 уровень, 2 лекция
Недоступна
Уровень героя
Уровень героя
1
Задача
Go SELF, 8 уровень, 2 лекция
Недоступна
Скорость датчика
Скорость датчика
1
Задача
Go SELF, 8 уровень, 2 лекция
Недоступна
Статус задачи
Статус задачи
1
Задача
Go SELF, 8 уровень, 2 лекция
Недоступна
Канбан доска
Канбан доска
Комментарии (2)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Илья Уровень 13
18 апреля 2026
В последнем задании плохо описано что нужно сделать
Vlad Tagunkov Уровень 25
19 апреля 2026
формата строк нету и что как выводить что бы угодить валитору не ясно.