Модуль 2. Базовые понятия языка
Цели модуля
После модуля студент сможет:
- Объявлять переменные и константы с правильными типами
- Использовать управляющие конструкции (if, for, switch)
- Писать функции с множественными возвращаемыми значениями
- Работать со слайсами и мапами, понимать их внутреннее устройство
- Проектировать структуры с методами и конструкторами
- Использовать embed для встраивания файлов в бинарник
- Понимать разницу между value и pointer semantics
2.1 Переменные, константы, типы
Объявление переменных
В Go есть несколько способов объявить переменную:
go// Явное объявление с типом var name string = "repo-service" // Объявление с выводом типа var count = 42 // Короткое объявление (только внутри функций) id := "repo-123" // Множественное объявление var ( host = "localhost" port = 5432 ssl = true )
Zero values
Каждый тип в Go имеет zero value - значение по умолчанию:
textint, float64 -> 0 string -> "" bool -> false pointer -> nil slice -> nil map -> nil interface -> nil
Zero values делают код предсказуемым: переменная всегда инициализирована.
Константы
Константы объявляются через const. Значение должно быть известно на этапе компиляции.
goconst MaxRetries = 3 const DefaultTimeout = 30 * time.Second
iota для перечислений
iota - автоинкрементный счётчик для const-блоков. Используется для создания перечислений (enum).
gotype Status int const ( StatusActive Status = iota // 0 StatusArchived // 1 StatusDeleted // 2 )
Основные типы
| Категория | Типы |
|---|---|
| Целые | int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64 |
| Дробные | float32, float64 |
| Строки | string |
| Булевы | bool |
| Композитные | array, slice, map, struct |
| Специальные | pointer, function, interface, channel |
Приведение типов
В Go нет неявного приведения типов. Все преобразования явные:
govar i int = 42 var f float64 = float64(i) var u uint = uint(f) // Ошибка компиляции: // var x float64 = i // cannot use i (type int) as type float64
2.2 Управляющие конструкции
if / else
В Go условие не требует скобок. Можно объявлять переменную перед условием (короткий scope):
goif err != nil { return err } // Переменная count видна только внутри if/else if count := len(items); count > 10 { fmt.Println("too many items") } else if count > 0 { fmt.Println("some items") } else { fmt.Println("no items") }
for - единственный цикл в Go
В Go есть только один цикл - for. Он заменяет for, while, do-while.
go// Классический for for i := 0; i < 10; i++ { fmt.Println(i) } // Аналог while count := 0 for count < 10 { count++ } // Бесконечный цикл for { // ... break } // Range по слайсу items := []string{"a", "b", "c"} for index, value := range items { fmt.Println(index, value) } // Range по мапе m := map[string]int{"a": 1, "b": 2} for key, value := range m { fmt.Println(key, value) }
switch
switch в Go не требует break. Каждый case завершается автоматически.
goswitch status { case StatusActive: fmt.Println("active") case StatusArchived: fmt.Println("archived") default: fmt.Println("unknown") }
switch без условия заменяет цепочку if/else:
goswitch { case count > 100: fmt.Println("large") case count > 10: fmt.Println("medium") default: fmt.Println("small") }
defer
defer откладывает выполнение функции до выхода из текущей. Выполняются в порядке LIFO (последний добавленный - первый выполненный).
gofunc readFile(path string) ([]byte, error) { f, err := os.Open(path) if err != nil { return nil, err } defer f.Close() // закроется при любом выходе из функции return io.ReadAll(f) }
Типичные использования defer:
- Закрытие файлов и соединений
- Разблокировка мьютексов
- Откат транзакций
- Восстановление после panic
2.3 Функции и множественные возвращаемые значения
Базовый синтаксис
gofunc add(a, b int) int { return a + b }
Множественные возвращаемые значения
Go функции могут возвращать несколько значений. Это основа паттерна обработки ошибок.
gofunc divide(a, b float64) (float64, error) { if b == 0 { return 0, errors.New("division by zero") } return a / b, nil } // Использование result, err := divide(10, 2) if err != nil { log.Fatal(err) }
Именованные возвращаемые значения
gofunc split(sum int) (x, y int) { x = sum * 4 / 9 y = sum - x return // возвращает x и y автоматически }
Используйте с осторожностью: ухудшают читаемость при длинных функциях.
Функции как значения
Функции в Go - first-class citizens. Их можно передавать, возвращать, присваивать переменным.
gofunc apply(items []int, fn func(int) int) []int { result := make([]int, len(items)) for i, item := range items { result[i] = fn(item) } return result } // Использование doubled := apply([]int{1, 2, 3}, func(x int) int { return x * 2 })
Variadic functions
gofunc sum(nums ...int) int { total := 0 for _, n := range nums { total += n } return total } sum(1, 2, 3) // 6 sum([]int{1, 2}...) // распаковка слайса
2.4 Массивы, слайсы, мапы
Массивы (используются редко)
Массив имеет фиксированный размер, заданный на этапе компиляции.
govar arr [3]int = [3]int{1, 2, 3} var arr2 = [...]int{1, 2, 3} // размер выводится
Массивы передаются по значению (копируются). В Go их используют крайне редко - обычно работают со слайсами.
Слайсы (основная структура данных)
Слайс - динамический массив. Состоит из трёх компонентов:
- pointer на базовый массив
- length (текущая длина)
- capacity (ёмкость)
go// Создание items := []int{1, 2, 3} items := make([]int, 0, 10) // len=0, cap=10 // Добавление items = append(items, 4) // Срез (не создаёт новый массив, использует тот же базовый) sub := items[1:3] // Копирование (создаёт новый слайс) copied := make([]int, len(items)) copy(copied, items)
Внутреннее устройство слайса
goitems := make([]int, 2, 4) //items -> [pointer, len=2, cap=4] // | // v // [0, 0, _, _] // базовый массив
При append если capacity недостаточно, выделяется новый массив с увеличенной ёмкостью (обычно x2).
Частые ошибки со слайсами
Ошибка 1: Неожиданное разделение памяти
gooriginal := []int{1, 2, 3, 4, 5} sub := original[1:3] // sub и original делят базовый массив sub[0] = 99 // изменяет original[1]
Ошибка 2: Утечка памяти
gofunc getFirst10(huge []byte) []byte { return huge[:10] // держит ссылку на весь huge массив } // Правильно: копировать func getFirst10(huge []byte) []byte { result := make([]byte, 10) copy(result, huge[:10]) return result }
Мапы
Мапа - хеш-таблица с ключами и значениями.
go// Создание m := make(map[string]int) m := map[string]int{"a": 1, "b": 2} // Операции m["key"] = 42 // запись value := m["key"] // чтение (вернёт 0 если нет) value, ok := m["key"] // проверка существования delete(m, "key") // удаление len(m) // количество элементов
Проверка существования ключа
govalue, exists := m["key"] if !exists { // ключа нет }
Итерация по мапе
Порядок итерации по мапе не определён (случайный намеренно).
gofor key, value := range m { fmt.Println(key, value) }
Мапы не потокобезопасны
Одновременная запись в мапу из нескольких горутин вызывает panic. Для конкурентного доступа используйте sync.Mutex или sync.Map.
2.5 Структуры и методы
Объявление структур
gotype Repo struct { ID string Name string OwnerID string CreatedAt time.Time }
Создание экземпляров
go// Поля по имени repo := Repo{ ID: "repo-123", Name: "my-project", } // По порядку полей (хрупко, не рекомендуется) repo := Repo{"repo-123", "my-project", "user-1", time.Now()} // Указатель на структуру repo := &Repo{ ID: "repo-123", Name: "my-project", }
Методы
Метод - функция с получателем (receiver).
go// Value receiver - получает копию func (r Repo) FullName() string { return r.OwnerID + "/" + r.Name } // Pointer receiver - может изменять оригинал func (r *Repo) Rename(newName string) { r.Name = newName }
Value vs Pointer receiver
| Критерий | Value receiver | Pointer receiver |
|---|---|---|
| Изменение | Не изменяет оригинал | Может изменять |
| Копирование | Копирует структуру | Не копирует |
| Когда использовать | Маленькие неизменяемые | Большие или изменяемые |
| nil-safety | Не может быть nil | Может быть nil |
Правило: если хотя бы один метод требует pointer receiver, все методы должны быть pointer receiver (консистентность).
Пример из шаблона: internal/models/repo.go
gopackage models import ( "time" ) type Repo struct { ID string `json:"id" db:"id"` Name string `json:"name" db:"name"` OwnerID string `json:"owner_id" db:"owner_id"` CreatedAt time.Time `json:"created_at" db:"created_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` } func NewRepo(name, ownerID string) *Repo { now := time.Now() return &Repo{ ID: generateID(), Name: name, OwnerID: ownerID, CreatedAt: now, UpdatedAt: now, } } func (r *Repo) Validate() error { if r.Name == "" { return errors.New("repo name is required") } if len(r.Name) < 3 || len(r.Name) > 100 { return errors.New("repo name must be 3-100 characters") } return nil }
Теги структур
Теги (tags) - метаданные для reflection. Используются для:
- JSON сериализации: json:"name"
- БД маппинга: db:"name"
- Валидации: validate:"required,min=3"
gotype User struct { ID string `json:"id" db:"id"` Email string `json:"email" db:"email" validate:"required,email"` Name string `json:"name" db:"name" validate:"max=255"` }
2.6 Embed - встраивание файлов
Директива go:embed
embed позволяет включить файлы в бинарник на этапе компиляции. Это устраняет зависимость от файловой системы в runtime.
gopackage config import "embed" // Встроить один файл //go:embed templates/default.yaml var defaultConfig string // Встроить в []byte //go:embed static/logo.png var logoBytes []byte // Встроить директорию //go:embed static/* var staticFiles embed.FS
Пример из шаблона: миграции в бинарнике
gopackage migrations import "embed" //go:embed *.sql var MigrationFiles embed.FS func GetMigration(name string) (string, error) { data, err := MigrationFiles.ReadFile(name) if err != nil { return "", err } return string(data), nil }
Когда использовать embed
- Конфигурационные файлы по умолчанию
- SQL миграции
- Шаблоны
- Статические файлы (для небольших проектов)
- OpenAPI спецификации
Ограничения embed
- Файлы должны быть в той же директории или поддиректории
- Нельзя встроить файлы из вышестоящих директорий
- Нельзя встроить скрытые файлы (начинающиеся с точки)
- Размер файлов влияет на размер бинарника
Привязка к шаблону проекта
Структура internal/models/ в шаблоне
textinternal/models/ ├── repo.go # Модель репозитория ├── user.go # Модель пользователя ├── pull_request.go # Модель pull request ├── outbox_event.go # Событие для outbox pattern ├── saga_instance.go # Инстанс саги └── idempotency_key.go # Ключ идемпотентности
Что изучаем на примере шаблона
- Модели - структуры с тегами json и db
- Конструкторы - функции NewRepo(), NewUser()
- Валидация - метод Validate() на модели
- Zero values - как используются в коде
- Time handling - time.Time с тегами сериализации
Задание: изучите internal/models/repo.go
- Какие поля есть у модели Repo?
- Какие теги используются и зачем?
- Есть ли конструктор? Какой у него паттерн?
- Есть ли метод Validate? Какие правила проверяет?
💱 Компромиссы
- Срез vs копия:
sub := original[1:3]не аллоцирует память, но делит базовый массив —sub[0] = 99изменяетoriginal;copyизолирует данные, но платит новой аллокацией. - Ёмкость заранее vs рост через
append:make([]int, 0, n)убирает реаллокации при известном размере, но завышенныйnдержит память впустую; чистыйappendэкономен в коде и платит копированием при каждом удвоении. mapvssync.Map: обычная мапа быстрее и проще, но паникует при конкурентной записи и требует внешнегоsync.Mutex;sync.Mapбезопасна из коробки, но выигрывает только на редких обновлениях.- Zero values vs конструктор:
var repo Repoвсегда инициализирован, но ноль не отличим от «не задано»;NewRepo()делает состояние явным ценой дополнительного кода и вызова. go:embedvs файлы на диске: встраивание миграций и конфигов в бинарник убирает зависимость от файловой системы в runtime, но размер бинарника растёт, а любое изменение файла требует пересборки.
Практическое задание
Задание 1: Создание модели Repo
Создайте файл internal/models/repo.go:
gopackage models import ( "errors" "time" ) type Repo struct { ID string `json:"id" db:"id"` Name string `json:"name" db:"name"` OwnerID string `json:"owner_id" db:"owner_id"` IsPrivate bool `json:"is_private" db:"is_private"` CreatedAt time.Time `json:"created_at" db:"created_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` } func NewRepo(name, ownerID string, isPrivate bool) *Repo { now := time.Now() return &Repo{ ID: generateID(), Name: name, OwnerID: ownerID, IsPrivate: isPrivate, CreatedAt: now, UpdatedAt: now, } } func (r *Repo) Validate() error { if r.Name == "" { return errors.New("repo name is required") } if len(r.Name) < 3 || len(r.Name) > 100 { return errors.New("repo name must be 3-100 characters") } if r.OwnerID == "" { return errors.New("owner id is required") } return nil }
Задание 2: Создание модели User
Создайте файл internal/models/user.go с полями:
- ID string
- Email string
- Name string
- CreatedAt time.Time
Добавьте конструктор NewUser() и метод Validate().
Задание 3: Работа со слайсами и мапами
Напишите функцию для работы с коллекцией репозиториев:
go// FilterRepos фильтрует репозитории по владельцу func FilterRepos(repos []*Repo, ownerID string) []*Repo // CountReposByOwner считает репозитории по владельцам func CountReposByOwner(repos []*Repo) map[string]int // SortReposByName сортирует репозитории по имени func SortReposByName(repos []*Repo)
Задание 4: Embed
Создайте файл с конфигурацией по умолчанию и встройте его через go:embed. Напишите функцию которая возвращает конфиг.
Задание 5: Юнит-тесты
Напишите table-driven тесты для:
- NewRepo() - проверка инициализации полей
- Validate() - проверка валидных и невалидных случаев
- FilterRepos() - проверка фильтрации
Quiz "Проверь себя"
append?iota?m := map[string]int{}
fmt.Println(m["key"])m := map[string]int{"a": 1}var x int и x := 0?go:embed?const (
A = iota
B
C
)
fmt.Println(B)✅ Итоги модуля
Студент должен:
- Объявлять переменные и константы разными способами
- Использовать if, for, switch, defer в коде
- Писать функции с множественными возвращаемыми значениями
- Работать со слайсами и мапами, понимать их устройство
- Проектировать структуры с методами и конструкторами
- Использовать embed для встраивания файлов
- Объяснить разницу между value и pointer receiver
- Написать модели Repo и User с валидацией
- Ответить на все вопросы Quiz
🔗 Мостик к Модулю 3
Следующий модуль: Модуль 3. Указатели, интерфейсы и композиция - семантика указателей, интерфейсы, generics, встраивание, functional options. Привязка к internal/repository/ шаблона.