Здравствуйте. В прошлом уроке вы подготовили модуль task-service и восстановили базовый цикл работы: форматирование, запуск, сборка, тесты и управление зависимостями. Теперь наполним эту заготовку логикой: научимся хранить данные в переменных, выбирать подходящие базовые типы, принимать решения через условия и повторять действия в циклах.
Это небольшой, но важный шаг к backend-коду: практически любой обработчик запроса сначала получает значения, затем проверяет их и обрабатывает набор действий по определённым правилам.
Переменные: имя, тип и значение
Переменная связывает имя, тип и текущее значение. Тип определяет, какие операции допустимы: числа можно складывать, строки сравнивать и объединять, а bool использовать в условиях.
В Go есть два основных способа объявить переменную.
var projectName string
var taskLimit int = 10
var isArchived = false
В форме с var тип стоит после имени. Если значение задано сразу, компилятор часто способен вывести тип самостоятельно:
var taskLimit = 10 // int
var projectName = "API" // string
Внутри функций чаще используют короткое объявление:
projectName := "API"
taskLimit := 10
isArchived := false
Оператор := одновременно объявляет переменную и присваивает значение. Он разрешён только внутри функций. После объявления переменной для изменения значения применяется обычное присваивание =:
taskLimit := 10
taskLimit = 20
Нельзя написать :=, если в текущей области видимости нет хотя бы одной новой переменной:
limit := 10
limit = 20 // правильно
// limit := 20 // ошибка: новой переменной здесь нет
Нулевые значения
Go не оставляет обычные переменные «неинициализированными». Если объявить переменную через var без значения, она получит нулевое значение своего типа:
| Тип | Нулевое значение |
|---|---|
int, int64, float64 | 0 |
string | "" |
bool | false |
var title string
var taskCount int
var ready bool
fmt.Printf("title=%q tasks=%d ready=%t\n", title, taskCount, ready)
Вывод будет таким:
title="" tasks=0 ready=false
Нулевые значения удобны, но в backend-разработке нельзя автоматически считать их корректными бизнес-данными. Например, пустое название проекта, идентификатор 0 или лимит 0 могут означать, что клиент не передал поле, а не то, что это осмысленное значение. Позже вы будете явно валидировать такие случаи в HTTP-обработчиках.
Прочитайте короткий раздел «Variables» из официального интерактивного курса A Tour of Go. Он закрепит синтаксис var и покажет, что переменные можно объявлять как на уровне пакета, так и внутри функции.
В разделе Variables прочитайте текст и пример целиком. Обратите внимание на правило объявления: тип пишется после имени, а var допустим и вне функций, и внутри них. В текущем проекте почти все переменные пока будут локальными, внутри main.
Базовые типы: выбирать по смыслу, а не «на всякий случай»
Для большинства прикладного Go-кода вам постоянно будут встречаться несколько типов:
title := "Prepare interview" // string
completed := 3 // int
ownerID := int64(42) // int64
isArchived := false // bool
progress := 60.0 // float64
string
string хранит текст. Строковые литералы обычно записываются в двойных кавычках:
status := "open"
emptyStatus := ""
Строки удобно использовать для названий, описаний, email-адресов и внешних идентификаторов. В доменной модели статус задачи позже тоже можно будет представить строкой, но только с проверкой допустимых значений.
Целые числа: int и int64
int — стандартный выбор для локальных счётчиков, индексов и размеров:
attempts := 3
Размер int зависит от архитектуры: обычно это 64 бита на современных серверных системах. Когда нужен конкретный размер, используйте тип с явной разрядностью, например int64.
var projectID int64 = 42
На практике int64 часто появляется на границе с PostgreSQL, если столбец имеет тип BIGINT. Не стоит выбирать int64 для каждой переменной без причины: для обычного счётчика задач в цикле достаточно int.
Беззнаковые типы uint, uint64 и другие существуют, но использовать их для бизнес-значений «потому что число не бывает отрицательным» обычно не нужно. У них есть неприятное свойство: при вычитании из нуля возникает переполнение. В прикладном backend-коде отрицательное значение int часто полезнее заметить и отклонить валидацией.
bool
bool принимает только два значения:
isOwner := true
isArchived := false
Булевы значения обычно получаются из сравнений:
hasAccess := ownerID == currentUserID
hasValidCount := completed >= 0 && completed <= total
float64
float64 хранит числа с дробной частью:
ratio := 3.5
Он подходит, например, для отображения процента выполнения. Но не используйте float64 для денег: двоичная плавающая точка может неточно представить десятичные дроби. Денежные суммы в сервисах обычно хранят в минимальных единицах целым числом: например, 19990 копеек вместо 199.90.
byte — это псевдоним uint8, а rune — псевдоним int32, представляющий одну Unicode-кодовую точку. Они понадобятся при более детальной работе с байтами и текстом; пока достаточно знать, что строка и отдельный Unicode-символ — не одно и то же.
Официальный раздел A Tour of Go даёт компактную карту базовых типов Go. Используйте его как справочник: не нужно заучивать все размеры чисел, важнее понимать, почему по умолчанию обычно выбирают int.
В разделе Basic types сначала просмотрите полный список типов, затем прочитайте пояснение под ним. Особенно запомните выбор int: берите int, пока конкретная разрядность или беззнаковое представление действительно не требуются.
Изучаем Go. Урок №2. Переменные, константы. Типы. Области видимости. Преобразование типов #golang
Посмотрите фрагмент видео «Изучаем Go. Урок №2. Переменные, константы. Типы. Области видимости. Преобразование типов» от ThisIsIT. Он наглядно повторяет базовые типы, способы объявления переменных и их нулевые значения.
Начните с типов данных: обратите внимание на различие между bool, string, целыми числами, byte и rune. Затем посмотрите объявления и нулевые значения. Сопоставляйте примеры с правилом: var подходит и для объявления без значения, а := удобен для локальных переменных с немедленной инициализацией.
Явное преобразование типов
Go не выполняет неявные преобразования между числовыми типами. Это защищает от незаметной потери точности и ошибок в контракте данных.
var dbID int64 = 42
count := 3
// total := dbID + count // ошибка: int64 и int — разные типы
total := dbID + int64(count)
fmt.Println(total)
Преобразование записывается как имя целевого типа и значение в скобках:
int64(count)
float64(completed)
Особенно часто это встречается при вычислении дробного процента. Деление двух int даёт целое число:
completed := 2
total := 3
ratio := completed / total
fmt.Println(ratio) // 0
Чтобы получить дробный результат, преобразуйте хотя бы один операнд до деления:
progress := float64(completed) / float64(total) * 100
fmt.Printf("%.1f%%\n", progress) // 66.7%
Но сначала необходимо удостовериться, что total не равен нулю. Это уже задача условия.
Константы для неизменяемых правил
Если значение не должно меняться во время работы программы, объявляйте константу:
const maxTasksPerProject = 100
Константы подходят для лимитов, строковых кодов и иных стабильных правил. Имя должно объяснять смысл числа: maxTasksPerProject лучше, чем просто 100, разбросанное по нескольким местам кода.
Условия: превращаем правила в код
Условие выбирает ветку выполнения на основании выражения типа bool.
if completed == total {
fmt.Println("All tasks are completed")
}
В Go условие не оборачивают в круглые скобки, но тело всегда заключают в фигурные скобки. Это правило уменьшает риск ошибок при последующем добавлении строк в ветку.
Основные операторы сравнения:
| Оператор | Значение |
|---|---|
== | равно |
!= | не равно |
<, > | меньше, больше |
<=, >= | меньше или равно, больше или равно |
Важно не путать присваивание и сравнение:
isArchived = true // присвоить значение
isArchived == true // сравнить значения
Для булевой переменной обычно читается лучше короткая форма:
if isArchived {
fmt.Println("Project is archived")
}
if !isArchived {
fmt.Println("Project is active")
}
В Go нет неявной «истинности» чисел или строк. Следующий код некорректен:
// if completed {
// }
Нужно явно сформулировать правило:
if completed > 0 {
fmt.Println("There are completed tasks")
}
Несколько ветвей: if, else if, else
Когда сценарии взаимоисключающие, используйте одну цепочку:
if completed == total {
fmt.Println("Project is complete")
} else if completed == 0 {
fmt.Println("No tasks completed yet")
} else {
fmt.Println("Project is in progress")
}
Выполнится ровно одна ветка. else пишется на той же строке, где закрывается предыдущий блок: } else {. После go fmt код будет приведён к этому стандартному стилю автоматически.
Логические операторы
Практические правила обычно состоят из нескольких проверок:
validCounts := completed >= 0 && completed <= total
&&— И: всё выражение истинно, только если истинны обе части.||— ИЛИ: достаточно, чтобы истинной была хотя бы одна часть.!— НЕ: меняетtrueнаfalseи наоборот.
isEmptyProject := total == 0 || completed == 0
canEdit := isOwner && !isArchived
Go проверяет части логического выражения слева направо и использует короткое вычисление. В выражении с && правая часть не будет вычислена, если левая уже false; в выражении с || правая часть не нужна, если левая уже true. Это позволяет безопасно строить проверки, которые защищают дальнейшее вычисление:
if total > 0 && float64(completed)/float64(total) >= 0.8 {
fmt.Println("Progress is at least 80%")
}
Здесь деление произойдёт только при total > 0.
Инициализация в if
Go позволяет создать переменную прямо перед условием:
if isComplete := completed == total; isComplete {
fmt.Println("Project is complete")
}
Переменная isComplete существует только внутри этой конструкции if, включая возможный блок else. Это полезно, когда промежуточный результат не нужен дальше. Если проверка начинает выглядеть громоздко, лучше объявить переменную отдельной строкой: читаемость важнее компактности.
Изучаем Go. Урок №5. Условный оператор (if/else). Логические операторы #golang
Фрагменты видео «Изучаем Go. Урок №5. Условный оператор (if/else). Логические операторы» от ThisIsIT помогут быстро восстановить синтаксис условий и логику составных проверок.
Сначала посмотрите сравнения и if, затем после короткого пропуска перейдите к ветке else. Закончите логическими операторами: соотнесите &&, || и ! с проверками валидности количества задач и прав доступа.
Цикл for: повторяем работу без копирования кода
В Go есть один ключевой оператор цикла — for. Его формы покрывают привычные циклы while, классический счётчик и бесконечное повторение.
Цикл с условием
Эта форма выполняется, пока условие истинно:
retriesLeft := 3
for retriesLeft > 0 {
fmt.Println("Trying again")
retriesLeft--
}
Не забывайте изменять значение, от которого зависит условие. Иначе цикл может стать бесконечным.
Цикл со счётчиком
Для известного числа повторений используйте три части: начальное значение, условие продолжения и действие после каждой итерации.
for attempt := 1; attempt <= 3; attempt++ {
fmt.Printf("Attempt %d\n", attempt)
}
Здесь:
attempt := 1выполняется один раз перед началом цикла.attempt <= 3проверяется перед каждой итерацией.attempt++увеличивает счётчик после выполнения тела.
attempt++ — сокращение для attempt = attempt + 1. В Go это отдельная инструкция: его нельзя использовать внутри выражения.
break и continue
break немедленно завершает цикл:
for attempt := 1; attempt <= 3; attempt++ {
fmt.Printf("Attempt %d\n", attempt)
if attempt == 2 {
break
}
}
continue пропускает оставшуюся часть текущей итерации и начинает следующую:
for number := 1; number <= 5; number++ {
if number <= 2 {
continue
}
fmt.Printf("Task %d needs attention\n", number)
}
В реальном коде break и continue полезны, но чрезмерное количество таких переходов усложняет чтение. Держите тело цикла коротким и формулируйте условие так, чтобы было ясно, почему обработка прекращается или пропускается.
В следующем уроке о массивах и срезах появится форма for range, которая обычно применяется для обхода реальных коллекций задач. Пока достаточно уверенно владеть for с условием и счётчиком.
Собираем мини-программу для проверки проекта
Замените содержимое main.go на программу ниже. Она не моделирует полноценные задачи — для этого пока нет коллекций и структур, — но уже использует типичные элементы прикладной логики: входные значения, инварианты, ветвления и обработку серии условных задач.
package main
import "fmt"
const maxTasksPerProject = 100
func main() {
title := "Interview preparation"
ownerID := int64(42)
completed := 2
total := 5
isArchived := false
validCounts := total >= 0 && completed >= 0 && completed <= total
if title == "" {
fmt.Println("Project title is required")
return
}
if !validCounts {
fmt.Println("Invalid task counts")
return
}
if total > maxTasksPerProject {
fmt.Println("Too many tasks in project")
return
}
if isArchived {
fmt.Println("Project is archived")
return
}
fmt.Printf("Project %q, owner %d\n", title, ownerID)
if completed == total {
fmt.Println("Project is complete")
} else if completed == 0 {
fmt.Println("No tasks completed yet")
} else {
progress := float64(completed) / float64(total) * 100
fmt.Printf("Progress: %.0f%%\n", progress)
}
// В этой учебной модели первые completed задач считаются завершёнными.
for taskNumber := 1; taskNumber <= total; taskNumber++ {
if taskNumber <= completed {
continue
}
fmt.Printf("Task %d needs attention\n", taskNumber)
}
}
Обратите внимание на несколько инженерных решений в этом коде:
ownerIDимеет типint64, поскольку идентификаторы нередко приходят из БД в таком виде.- Проверка
validCountsзащищает инвариант: завершённых задач не может быть меньше нуля или больше общего числа. - Ранние
returnне дают программе перейти к вычислению процента и циклу при некорректных данных. - Преобразование в
float64происходит только после проверки корректности счётчиков; приtotal == 0код попадёт в веткуcompleted == total, не выполняя деление. continueне печатает завершённые задачи и оставляет в выводе только требующие внимания.
Запустите привычный цикл проверки из корня модуля:
go fmt ./...
go test ./...
go run .
Ожидаемый результат для исходных значений:
Project "Interview preparation", owner 42
Progress: 40%
Task 3 needs attention
Task 4 needs attention
Task 5 needs attention
Для самостоятельной ручной проверки меняйте значения по одному:
- установите
completed := 5, чтобы попасть в ветку полного завершения; - установите
completed := 0, чтобы увидеть ветку отсутствия прогресса; - установите
completed := 6, чтобы сработала проверка инварианта; - установите
isArchived := true, чтобы программа завершилась до обработки задач.
После изменений каждый раз запускайте go fmt ./... и go test ./...: даже пока тест содержит только smoke-проверку, эта команда также компилирует весь пакет и ловит ошибки типов и синтаксиса.
Итоги
Теперь у вас восстановлена базовая логика Go-программы:
varобъявляет переменную, а:=кратко объявляет и инициализирует её внутри функции;- у базовых типов есть предсказуемые нулевые значения;
- для обычных целых счётчиков подходит
int, аint64нужен при явной необходимости; - Go требует явного преобразования между числовыми типами;
if,else ifиelseвыражают взаимоисключающие правила;&&,||и!позволяют собирать несколько проверок в одно условие;forпокрывает циклы с условием и со счётчиком, аbreakиcontinueуправляют ходом обработки.
В следующем уроке вы перейдёте к функциям: выделите проверки и вычисления из main, научитесь возвращать несколько значений, включая привычную для Go пару «результат и ошибка».
Can't find a good explanation? Sign up and we'll make it for you
Sign up