Skip to content

cagnit/go-course

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 

Repository files navigation

Неделя 1. Введение в Go: Переменные, Ввод/Вывод и Основы

Что такое Go?

Go (или Golang) — это компилируемый, строго типизированный язык программирования от Google, созданный для разработки быстрых, надёжных и масштабируемых приложений.

💡 Особенности:

  • Простота синтаксиса, как у Python
  • Производительность на уровне C
  • Встроенная конкурентность через goroutines
  • Идеально для серверной разработки, микросервисов, сетевых программ, CLI-инструментов

Установка Go


Go Workspace и структура проекта

Go использует модули для управления зависимостями и сборкой. Каждый проект — это отдельный модуль.

Пример структуры:

amfs/
├── cmd/
│   └── amfs/        ← точка входа (main.go) для приложения
├── internal/        ← внутренние пакеты (нельзя импортировать извне)
├── pkg/             ← переиспользуемые публичные пакеты
├── go.mod           ← описание модуля и зависимостей
├── main.go          ← стартовый файл
└── other.go         ← дополнительные исходники

main() — точка входа

Функция main() — стартовая точка выполнения программы.
Она должна находиться в пакете main, иначе компиляция завершится с ошибкой.


Компиляция и запуск

go mod init amfs       # инициализация модуля
go run main.go         # компиляция и запуск
go build               # компиляция → бинарный файл
go get <package>       # установка стороннего пакета

Пример:

go get github.com/shopspring/decimal

Переменные и константы

var x int = 10       // явное объявление
x := 10              // короткая форма (внутри функций)
const Pi = 3.14      // константа (нельзя изменить)

Типы данных

Категория Типы
Целые int, int8, int16, int32, int64
Беззнаковые uint, uint8, uint16, uint32, uint64
С плавающей точкой float32, float64
Логические bool
Строки string
Символы rune (аналог char, по сути int32)
Указатели *int, *string и т. д.
Обобщённый тип interface{}

⚠️ Никогда не используйте float для хранения финансов — используйте decimal.


Модуль fmt

Модуль fmt используется для ввода/вывода и форматирования.

Вывод:

fmt.Println("Hello")       // С пробелами и новой строкой
fmt.Printf("x = %d", 10)   // Форматированный вывод
s := fmt.Sprintf("Pi=%.2f", 3.1415) // Возврат строки

Ввод:

fmt.Scan(&x, &y)           // Считывает через пробел
fmt.Scanf("%d %s", &a, &b) // Форматированный ввод

Форматные спецификаторы:

Спецификатор Назначение
%d Целое число (int)
%f Число с плавающей точкой
%.2f Float с 2 знаками после запятой
%s Строка
%t Boolean
%v Значение как есть
%T Тип значения

Условные конструкции: if / else if / else

if a > b {
    fmt.Println("a больше b")
} else if a == b {
    fmt.Println("равны")
} else {
    fmt.Println("b больше a")
}
  • Скобки () не требуются!
  • Переменные можно объявлять внутри if:
if n := rand.Intn(10); n > 5 {
    fmt.Println("Большое:", n)
}

Переменная n не видна за пределами if.


Альтернатива ifswitch

switch выражение {
case значение1:
    // действие
case значение2:
    // другое действие
default:
    // если ничего не подошло
}

Пример:

type EComStatus uint8

const (
    New EComStatus = iota
    PreAuth
    Complete
    Declined
    Refunded
    Reversed
)

func (status EComStatus) IsValid() bool {
    switch status {
    case New, PreAuth, Complete, Declined, Refunded, Reversed:
        return true
    }
    return false
}

iota — генератор чисел в const-блоках

const (
    A = iota // 0
    B        // 1
    C        // 2
)

Полезен для:

  • Перечислений (enum)
  • Побитовых флагов
  • Автоматической генерации чисел

План задач по Go: Недельный модуль

Все задачи должны быть оформлены по базовой структуре проекта Go с соблюдением принципов DRY, KISS, SOLID.

Структура проекта:

project/
├── cmd/             # точка входа в приложение
├── internal/        # внутренняя логика
├── pkg/             # переиспользуемые пакеты
├── Makefile         # команды управления
├── .gitignore       # исключения для Git
├── .env             # переменные окружения (опционально)
├── .env.dist        # шаблон .env

Конвертер валют

Задача:

Создать мини-приложение, которое:

  1. Оформлено по структуре internal/pkg/cmd
  2. Совмещает в себе функционал двух предыдущих задач (в виде функций в отдельных модулях)
  3. Имеет Makefile с командами:
    • make run — запуск приложения
    • make build — сборка в ../bin
    • make link — проверка форматирования и статики (go fmt, go vet)
  4. Содержит .gitignore, .env, .env.dist — для будущих задач

Условия:

  • Использовать fmt для ввода и вывода
  • Использовать пакет shopspring/decimal

💡 Пример:

Вход:

15

Выход:

7800 KZT

Недельный расчет по операциям

Задача:

Обработать массив транзакций, где каждая — это map[int]decimal.Decimal, где:

  • int — номер дня недели (1 = Пн, 7 = Вс)
  • decimal.Decimal — сумма транзакции

Положительная сумма = доход, отрицательная = расход

Входные данные:

transactions := []map[int]decimal.Decimal{
 {1: decimal.NewFromFloat(25000.00)},
 {1: decimal.NewFromFloat(20000.00)},
 {2: decimal.NewFromFloat(-9800.00)},
 {3: decimal.NewFromFloat(-1222.22)},
 {4: decimal.NewFromFloat(-1500.07)},
 {5: decimal.NewFromFloat(1201.37)},
 {6: decimal.NewFromFloat(-100.32)},
 {7: decimal.NewFromFloat(-523.33)},
}

Требования:

  • Применить: for, if / else if / else, switch
  • Итоговый вывод:
    • День недели
    • Доход / Расход за день
    • Общий итог за неделю

Пример вывода:

Понедельник
Поступление: 45000.00

Вторник
Списание: 9800.00

Среда
Списание: 1222.22

Четверг
Списание: 1500.07

Пятница
Поступление: 1201.37

Суббота
Списание: 100.32

Воскресенье
Списание: 523.33

Итог за неделю: -10844.57

Также показать грамотное применение логических конструкций и позитивных практик написания кода.


Неделя 2. Массивы, Слайсы, Мапы, Функции, Указатели, Структуры, Множества


Массивы

Массив — это фиксированная по длине коллекция однотипных элементов. Длина массива является частью его типа.

var a [3]int       // массив из трёх int, по умолчанию [0, 0, 0]
a[0] = 10          // установка значения
fmt.Println(a[1])  // доступ к элементу: 0
  • Тип [3]int и [4]int — разные типы.
  • Массивы копируются по значению.
  • Можно итерировать через for i, v := range a
  • Нельзя изменить длину массива после создания.
  • Внутри range — копия массива (если не использовать указатель).

Слайсы (Slices)

Слайс — это обёртка над массивом, включающая длину и ёмкость.

b := []int{1, 2, 3}
b = append(b, 4)
  • len(b) — текущая длина, cap(b) — ёмкость.
  • append может выделить новую память.
  • Слайсы передаются по ссылке.
  • Срезы: b[1:3], b[:2], b[2:]
  • Можно делать срез от среза: b2 := b[1:]

Map (словари / ассоциативные массивы)

m := make(map[string]int)
m["apple"] = 5
val, ok := m["apple"]
delete(m, "apple")
  • Проверка ключа: val, ok := m["key"]
  • Удаление: delete(m, "key")
  • Порядок итерации не гарантируется.
  • Ключи — сравнимые типы.
  • Значения — любые типы.

Функции

Функции — основной строительный блок в Go.

Объявление функции

func Add(a int, b int) int {
    return a + b
}

Несколько возвращаемых значений

func Swap(x, y string) (string, string) {
    return y, x
}

Именованные возвращаемые значения

func NamedReturn() (a int, b int) {
    a = 1
    b = 2
    return
}

Функции как значения

f := func(x int) int {
    return x * x
}
fmt.Println(f(5))

Анонимные функции

func() {
    fmt.Println("Привет!")
}()

Замыкания

func Counter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

Функции высшего порядка

func Apply(fn func(int) int, val int) int {
    return fn(val)
}

Variadic-функции

func Sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

Рекурсия

func Factorial(n int) int {
    if n == 0 {
        return 1
    }
    return n * Factorial(n-1)
}

Указатели

func AddOne(x *int) {
    *x = *x + 1
}

val := 10
AddOne(&val)
fmt.Println(val) // 11
  • *x — разыменование
  • &val — получение адреса
  • Nil-указатели: var p *int

Структуры (Struct)

type User struct {
    Name string
    Age  int
}

u := User{Name: "Alice", Age: 30}
fmt.Println(u.Name)
  • Можно создавать указатели u := &User{}
  • Сравнимы, если все поля сравнимы

Множества (Set)

set := make(map[string]struct{})
set["apple"] = struct{}{}
_, ok := set["apple"]
delete(set, "apple")
  • Тип struct{} — 0 байт
  • Проверка: _, ok := set[key]

Проект: Сервис учёта транзакций мерчантов

Цель

Разработать Go-сервис, который:

  • Загружает конфигурацию через viper
  • Подключается к PostgreSQL
  • Автоматически мигрирует таблицы
  • Работает с транзакциями и статусами

Конфигурация

  • Порт приложения
  • DEBUG режим
  • Автомиграция
  • Данные БД
  • Docker переменные
  • .env.dist как пример

Структура БД

Таблица: terminals

Поле Тип данных Описание
id bigint (PK) Уникальный ID
client_id TEXT ID клиента
client_secret TEXT Секретный ключ
uuid uuid Уникальный ID терминала в UUID формате

Таблица: transactions

Поле Тип Описание
id bigint ID транзакции
terminal_id UUID FK на терминал
order_id TEXT ID клиента
amount NUMERIC(12,2) Сумма
status TEXT Статус
created_at TIMESTAMP Создана
status_changed TIMESTAMP Статус изменён
code TEXT Код MPS
message TEXT Описание mps кода

Статусы транзакций

Статус Описание
REFUND Возврат суммы
AUTH Блокировка суммы
CANCEL Разблокировка
CHARGE Списание
VERIFIED Проверка карты
CANCEL_OLD Истёк срок CHARGE
FAILED Неуспешно
FINGERPRINT Проверка перед 3D
3D Ошибка 3D
NEW Ожидание
REJECT Отклонено

Логика перехода статусов

  1. NEWAUTH
  2. AUTHCHARGE или CANCEL
  3. CHARGEREFUND

Что реализовать

  • Глобальный конфиг через viper
  • Подключение к БД
  • Автомиграции (по флагу из конфига)
  • Создание транзакций и смена статусов

Неделя 3: Методы, интерфейсы и работа с ошибками.


Методы в Go

Методы — это функции, которые привязаны к определённому типу.

type MFS struct {
    Msisdn      string             `gorm:"column:number;primaryKey"`
    MobileOwner consts.MobileOwner `gorm:"column:id_operator"`
}

func (MFS) TableName() string {
    return "schema.mfs"
}

func (m *MFS) By(msisdn string) error {
    return db.Database.Where(MFS{Msisdn: msisdn}).First(&m).Error
}
  • MFS это структура соответствующая таблице schema.mfs в БД
  • Метод TableName сообщает GORM, какое имя таблицы использовать
  • Метод By выполняет запрос к базе данных по номеру телефона пользователя и загружает результат в текущий экземпляр &m
  • Указатель (*MFS) используется, так как метод изменяет содержимое структуры
  • Вызов Error в конце вернет ошибку если такой записи нет или есть проблемы с подключением к БД

Интерфейсы

Интерфейсы — основной способ абстракции в Go. Go не поддерживает классы и наследование, как в других ООП-языках. Вместо этого используются интерфейсы — они описывают поведение, не привязываясь к конкретной реализации.

Если тип реализует все методы интерфейса — он автоматически считается его реализацией (без implements, extends и т.п.). Это называется duck typing: "если оно выглядит как утка и крякает как утка, значит, это утка".

Интерфейсы позволяют писать обобщённый и расширяемый код.

package main

import (
	"errors"
	"io"
	"log"
	"net/http"
)

type Authenticator interface {
    SetAuth(req *http.Request) error
}

type BasicAuth struct {
    Username string
    Password string
}

type BearerAuth struct {
    Token string
}

func (ba *BasicAuth) SetAuth(req *http.Request) error {
    if ba.Username == "" || ba.Password == "" {
        return errors.New("username or password is not set")
    }
    req.SetBasicAuth(ba.Username, ba.Password)
    return nil
}

func (bt *BearerAuth) SetAuth(req *http.Request) error {
    if bt.Token == "" {
        return errors.New("token is not set")
    }
    req.Header.Set("Authorization", "Bearer "+bt.Token)
    return nil
}

func SendRequest(auth Authenticator, url string) error {
    // собираем запрос
    req, _ := http.NewRequest("GET", url, nil)
    // подключаем авторизацию
    if auth != nil {
        if errAuth := auth.SetAuth(req); errAuth != nil {
            return errAuth
        }
    }
    // отправляем запрос
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()
        if resp.StatusCode != 200 {
            body, _ := io.ReadAll(resp.Body)
            return errors.New(string(body))
    }
    return nil
}

func main() {
    auth := &BearerAuth{Token: "secrettoken"}
    // или используем BasicAuth:
    // auth := &BasicAuth{Username: "admin", Password: "admin"}
    
    err := SendRequest(auth, "https://example.com/")
    if err != nil {
        log.Fatal("Ошибка при отправке:", err)
    }
}
  • Определён интерфейс Authenticator, который требует реализацию одного метода:
SetAuth(req *http.Request) error
  • Реализованы два типа авторизации: BasicAuth — добавляет логин и пароль в заголовок Authorization (в формате Basic). BearerAuth — добавляет токен в заголовок Authorization (в формате Bearer)

  • Функция SendRequest принимает интерфейс Authenticator как параметр, что позволяет использовать любой тип, реализующий нужный метод.

  • Интерфейсы позволяют писать обобщённый код

  • Интерфейс может содержать один или несколько методов

  • Один тип может реализовать сразу несколько интерфейсов

Пустой интерфейс, принимающий любой тип.

func CustomPrint(i interface{}) {
    fmt.Printf("Тип: %T, значение: %v\n", i, i)
}

func main() {
    CustomPrint(322)
    CustomPrint("I like AMFS")
    CustomPrint(MFS{Msisdn: "777777777"})
}

Интерфейс error

error — это встроенный интерфейс:

type error interface {
    Error() string
}

Любой тип, реализующий Error() string, можно использовать как ошибку.


Создание ошибок

error это тоже интерфейс:

type error interface {
    Error() string
}

создаем простую ошибку:

import "errors"

err := errors.New("что-то пошло не так")

Обертываем ошибку

import "fmt"

err := doSomething()
if err != nil {
    return fmt.Errorf("не удалось выполнить doSomething: %w", err)
}

Создаем кастомную ошибку:

var BaseGatewayError *GatewayError

type GatewayError struct {
    Message string
}

func (e *GatewayError) Error() string {
    return e.Message
}

func NewGatewayError(message string) error {
    return &GatewayError{message}
}

Проверка на тип ошибки

if errors.As(err, &BaseGatewayError) {
    fmt.Println("Gateway Error Response:", BaseGatewayError.Message)
}

Обработка ошибок

Go использует явную проверку ошибок:

f, err := os.Open("netflix.txt")
if err != nil {
    log.Fatal(err)
}
defer f.Close()

В Go нет исключений — только проверка через if err != nil.

panic и recover

panic используется в случае фатальных ошибок — когда программа не может продолжать работу:

panic("неожиданная ошибка")

Паника может происходить и неявно:

var input string
switch strings.Split(input, " ")[1] {
    // panic: runtime error: index out of range [1] with length 1
}

Не стоит использовать panic для обычных ошибок:

func divide(a, b int) int {
    if b == 0 {
        panic("division by zero")
    }
    return a / b
}

recover — перехват panic

recover используется внутри defer для перехвата паники и предотвращения аварийного завершения:

func safeCall() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Восстановлено после паники:", r)
        }
    }()
    mightPanic()
}

Когда использовать panic/recover

Используйте panic и recover только в исключительных случаях, например:

  • в middleware для HTTP-серверов (чтобы не упал весь процесс)
  • при защите от неожиданных сбоев на верхнем уровне приложения

Задания:


Расширить сервис учёта транзакций мерчантов

Цель

Усложнить базовую реализацию, добавив поддержку:

  • Моков и интерфейсов для банковских адаптеров
  • Методов бизнес-логики на структурах
  • JWT-авторизации для терминалов
  • Кастомных ошибок и централизованной обработки panic

Интерфейсы и моки

  • Интерфейс BankAdapter с методами:

    • Auth
    • Charge
    • Refund

    Позволяет отделить взаимодействие с банками от основной логики — у каждого банка своя реализация, но используется единый интерфейс.

  • Моковая реализация MockBank

    Используется для тестирования без обращения к настоящему банку.

  • Возможность подключения нескольких адаптеров

    Адаптеры можно регистрировать и подменять, не меняя бизнес-логику. Пример: adapter := adapters["mock"]


Методы

  • Методы для:
    • Обновления статуса транзакции (например, SetStatus)
    • Поиска терминала по client_id и проверки client_secret
    • Генерации JWT-токена по данным терминала

Пример метода:
func (t *Terminal) GenerateJWT() (string, error)


JWT-авторизация

  • Авторизация по client_id и client_secret

  • Генерация JWT с:

    • terminal_id
    • временем жизни (например, 1 час)
  • Middleware:

    • Проверяет наличие и валидность JWT в заголовке Authorization: Bearer <token>
    • Добавляет информацию о терминале в context

Работа с ошибками

  • Обёртки ошибок:
    • fmt.Errorf("ошибка: %w", err)
  • Кастомные ошибки с типами:
    • var ErrUnauthorized = errors.New("unauthorized")
  • Проверка через errors.Is, errors.As
  • Обработка panic через middleware:
    • defer func() { if r := recover(); ... }()

Что реализовать

  • Интерфейс BankAdapter и мок
  • Методы на структурах Terminal, Transaction
  • JWT-авторизацию и middleware
  • Кастомные ошибки и централизованный recover

Неделя 4: Горутины, Каналы и Параллелизм


Когда использовать конкурентность

Конкурентность — это способность программы выполнять несколько задач одновременно.

Используй конкурентность, когда:

  • операция может выполняться параллельно (например, запросы к API, работа с файлами);
  • задача блокирует выполнение (например, ожидание ответа от сети);
  • нужно повысить производительность при работе с множеством однотипных задач.

Горутины

Горутина — это легковесный поток выполнения, основной инструмент конкурентности в Go. Позволяет запускать функции параллельно, не блокируя основной поток.

go func() {
    fmt.Println("Параллельный вывод")
}()
  • Запускается с помощью ключевого слова go
  • Запускается асинхронно
  • Делит память с другими горутинами
  • Не блокирует текущий поток

Каналы

Каналы используются для общения между горутинами.

  • Создаются через make(chan тип)
  • Передача данных: запись ch <- значение, чтение <-ch
  • Бывают буферизированные и небуферизированные
ch := make(chan int) // небуферизированный канал

go func() {
    ch <- 5 // запись в канал
}()

val := <-ch // чтение из канала
fmt.Println(val)

Буферизированные каналы

Создаются с указанием размера буфера:

ch := make(chan int, 2) // буфер на 2 элемента
  • Запись не блокирует, пока буфер не заполнен
  • Чтение не блокирует, пока буфер не пуст
  • Используются для временного хранения данных между горутинами

Пример:

ch := make(chan int, 2)

ch <- 1
ch <- 2
// ch <- 3 // это вызовет блокировку, т.к. буфер заполнен
fmt.Println(<-ch) // 1
fmt.Println(<-ch) // 2

Небуферизированные каналы

Создаются без указания размера:

ch := make(chan int) // небуферизированный канал
  • Запись блокирует, пока кто-то не прочитает из канала
  • Чтение блокирует, пока кто-то не запишет в канал
  • Обеспечивают строгую синхронизацию между отправителем и получателем

Пример:

ch := make(chan int)

go func() {
    ch <- 42 // заблокируется, пока кто-то не прочтёт
}()

val := <-ch // чтение из канала
fmt.Println("Получено:", val)

Цикл for-range и каналы

Цикл for-range позволяет удобно читать данные из канала до его закрытия:

ch := make(chan int)

go func() {
    for i := 0; i < 5; i++ {
        ch <- i
    }
    close(ch) // закрываем канал после записи
}()

for val := range ch { // читаем из канала до его закрытия
    fmt.Println("Получено:", val)
}

Закрытие каналов

Закрытие канала сигнализирует получателям, что больше данных не будет. Канал закрывается с помощью close(ch). После закрытия канала:

  • Запись в закрытый канал вызывает панику
  • Чтение из закрытого канала возвращает нулевое значение типа канала

Проверка закрытия канала:

val, ok := <-ch
if !ok {
    fmt.Println("Канал закрыт, данных нет")
}

Оператор select

Оператор select позволяет работать с несколькими каналами одновременно:

select {
case val := <-ch1:
    fmt.Println("Получено из ch1:", val)
case ch2 <- 10:
    fmt.Println("Отправлено в ch2")
default:
    fmt.Println("Нет доступных операций")
}

Timeout в select

Использование select и time.After

time.After возвращает канал, который отправляет значение через заданное время, позволяя реализовать таймауты в горутинах.

select {
case val := <-ch:
fmt.Println("Получено:", val)
case <-time.After(2 * time.Second):
fmt.Println("Таймаут через 2 секунды")
}

Испольвование context.WithTimeout

  • WithTimeout создаёт контекст с таймаутом

  • <-ctx.Done() сигнализирует о завершении таймаута

  • ctx.Err() возвращает причину завершения (например, context.DeadlineExceeded)

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

select {
case val := <-ch:
    fmt.Println("Получено:", val)
case <-ctx.Done(): // контекст завершён
    fmt.Println("Таймаут:", ctx.Err())
}

Ожидание завершения горутин: sync.WaitGroup

sync.WaitGroup позволяет дождаться завершения всех запущенных горутин. Используйте, когда нужно синхронизировать выполнение и избежать преждевременного выхода из программы.

var wg sync.WaitGroup

wg.Add(3) // ждем 3 горутины

for i := 0; i < 3; i++ {
go func(id int) {
defer wg.Done() // сигнал о завершении горутины
fmt.Println("Горутина", id)
}(i)
}

wg.Wait() // блокируемся пока все Done не вызовутся
  • Add(n) — указывает, сколько горутин ожидается
  • Done() — вызывается в каждой горутине по завершении
  • Wait() — блокирует основной поток до завершения всех горутин

Защита общих данных: sync.Mutex

sync.Mutex обеспечивает эксклюзивный доступ к разделяемым переменным между горутинами. Используйте, если несколько горутин могут одновременно изменять одну переменную

sync.Mutex используется для синхронизации доступа к общим данным между горутинами.
var mu sync.Mutex
var counter int

for i := 0; i < 5; i++ {
    go func() {
        mu.Lock()
        counter++      // безопасное увеличение счётчика
        mu.Unlock()
    }()
}
  • Lock() — захват блокировки
  • Unlock() — освобождение блокировки

Задание: Конвертер валют с конкурентной обработкой


Цель

Реализовать многопоточный сервис конвертации валют, который:

  • имитирует получение курсов валют из внешнего источника

(без реального API, данные — предзаданы или фиксированы)

  • обрабатывает множество запросов параллельно с использованием goroutine

  • использует channel для организации очереди запросов

  • сохраняет полученные курсы в защищённую map с использованием sync.Mutex


Условия

  • Использовать goroutine и sync.WaitGroup для параллельного запуска.

  • Использовать context.WithTimeout для каждого запроса.

  • Обмен результатами — через канал chan Result.

  • Хранение результатов — в общей map[string]decimal.Decimal, доступ к которой регулируется sync.Mutex.

  • Для вычислений и хранения курсов использовать github.com/shopspring/decimal.


Требования:

Валютные пары:

pairs := []string{"USD/KZT", "RUB/KZT", "USD/RUB", "EUR/KZT", "EUR/USD"}

Поведение:

  • Для каждой пары запускается goroutine, которая имитирует получение курса с задержкой time.Sleep (от 0 до 2 секунд).
time.Sleep(time.Duration(rand.Intn(2000)) * time.Millisecond)
  • запрос ограничивается по времени через context.WithTimeout(1.5 секунды)

  • По завершению (или таймауту) каждая goroutine отправляет результат в канал chan Result

type Result struct {
    Pair string
    Rate decimal.Decimal
    Err  error
}
  • Результаты из канала обрабатываются в главной горутине. Если Err == nil, курс сохраняется в общую map[string]decimal.Decimal.

  • Запись в map осуществляется через sync.Mutex.

Пример вывода:

Полученные курсы валют:
USD/KZT: 540,42
RUB/KZT: 6.71
USD/RUB: 80,26
EUR/KZT: запрос превысил таймаут
EUR/USD: 1.15

Неделя 5: Тестирование и отладка (reflect, unsafe)


Зачем это нужно

Для тестирования сценариев:

  • скрытые структуры и приватные поля,

  • код, зависящий от внутреннего состояния,

  • работу с памятью и нестандартными типами данных.

В Go такие задачи можно решать с помощью:

  • Рефлексии (reflect) — для анализа и изменения значений во время выполнения программы;

  • Пакета unsafe — для работы с памятью напрямую и обхода ограничений компилятора (с осторожностью!);

  • Расширенных возможностей тестирования (testing, t.Run, t.Parallel).


Рефлексия (reflect)

Рефлексия позволяет:

  • Узнавать тип переменной во время выполнения;

  • Читать и изменять значения, даже если они неизвестны на этапе компиляции;

  • Доступаться к приватным полям через обход правил.

Пример: чтение и изменение приватного поля структуры.

package main

import (
    "fmt"
    "reflect"
)

type user struct {
    name string
}

func main() {
    u := user{name: "Alice"}

    v := reflect.ValueOf(&u).Elem()
    f := v.FieldByName("name")

    if f.CanSet() {
        f.SetString("Bob") // изменяем напрямую
    } else {
        fmt.Println("Поле недоступно напрямую")
    }

    fmt.Println("Новое имя:", u.name)
}

CanSet() вернёт false для приватных полей (с маленькой буквы). Чтобы их изменить, нужен либо доступ из того же пакета, либо unsafe.


Доступ к приватным полям через unsafe

В unsafe есть возможность получить прямой доступ к памяти по адресу переменной.

package main

import (
    "fmt"
    "reflect"
    "unsafe"
)

type user struct {
    name string
}

func main() {
    u := user{name: "Alice"}

    v := reflect.ValueOf(&u).Elem()
    f := v.FieldByName("name")

    ptr := unsafe.Pointer(f.UnsafeAddr()) // указатель на поле
    realPtr := (*string)(ptr)              // приводим к *string

    *realPtr = "Bob" // меняем напрямую

    fmt.Println("Новое имя:", u.name)
}

unsafe даёт возможность нарушить правила Go и работать с памятью напрямую, но это может привести к непредсказуемому завершению программы или ошибкам времени выполнения, поэтому его используют только в крайних случаях.


Расширенное тестирование в Go

В Go тесты можно организовывать с подзадачами (t.Run) и запускать их параллельно (t.Parallel).

Это полезно, когда:

  • У вас много однотипных тестов, и вы хотите быстро выявить ошибки.

  • Нужно проверить код в условиях конкурентного доступа.

  • Хотите ускорить выполнение тестов на многоядерных системах.

Пример: Параллельные подзадачи

func TestMultiplyByTwo(t *testing.T) {
    cases := []struct {
        name string
        in   int
        want int
    }{
        {"case1", 1, 2},
        {"case2", 2, 4},
    }

    for _, c := range cases {
        c := c // избегаем замыкания на одну переменную
        t.Run(c.name, func(t *testing.T) {
            t.Parallel() // запуск в параллели
            got := c.in * 2
            if got != c.want {
                t.Errorf("got %d, want %d", got, c.want)
            }
        })
    }
}

Особенности:

  • t.Run создаёт независимый тест.

  • t.Parallel позволяет запускать подзадачи одновременно.

  • Нужно избегать замыкания на одну и ту же переменную цикла (c := c).


Отладка с помощью pprof

pprof — встроенный инструмент Go для профилирования производительности и потребления ресурсов.

Когда использовать:

  • Программа работает медленно, и вы хотите понять, где «узкое место».

  • Необходимо отследить утечки памяти или чрезмерное потребление CPU.

  • Нужно оптимизировать горячие участки кода.

package main

import (
    _ "net/http/pprof"
    "net/http"
)

func main() {
    go func() {
        http.ListenAndServe(":6060", nil) // слушаем pprof на порту 6060
    }()

    // основной код программы
    select {}
}

После запуска можно подключиться к профилированию:

go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

Доступные профили:

  • /debug/pprof/profile — CPU-профиль.

  • /debug/pprof/heap — использование памяти.

  • /debug/pprof/goroutine — количество горутин.

  • /debug/pprof/block — блокировки.


Задание: Тестирование приватных структур библиотеки

Цель

Отработать приёмы работы с приватными полями и методами через reflect и unsafe, группировку и параллельный запуск тестов, а также использование pprof для анализа производительности.


Условия:

  • Есть структура с приватными полями и методами.

  • В тестах нужно:

    • С помощью reflect и unsafe изменить приватные поля.

    • Проверить, что методы корректно отрабатывают с изменённым состоянием.

    • Использовать t.Run для группировки тестов.

    • Для тяжёлых тестов — запустить их параллельно (t.Parallel).

  • Собрать и проанализировать профиль памяти (pprof) при массовом выполнении тестов.


Пример структуры:

type cache struct {
    data map[string]string
    hits int
}

func newCache() *cache {
    return &cache{data: make(map[string]string)}
}

func (c *cache) get(key string) string {
    if val, ok := c.data[key]; ok {
        c.hits++
        return val
    }
    return ""
}

Пример модификации приватного поля в тесте:

func TestCacheHits(t *testing.T) {
    c := newCache()

    v := reflect.ValueOf(c).Elem()
    hitsField := v.FieldByName("hits")
    ptr := (*int)(unsafe.Pointer(hitsField.UnsafeAddr()))

    *ptr = 42 // вручную меняем счётчик

    if c.hits != 42 {
        t.Errorf("ожидалось 42, получили %d", c.hits)
    }
}

Ожидаемый результат выполнения тестов:

=== RUN   TestCacheHits
=== RUN   TestCacheHits/manualChange
--- PASS: TestCacheHits (0.00s)
    --- PASS: TestCacheHits/manualChange (0.00s)
PASS
ok      myapp/cache   0.005s

About

Go Course

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 3

  •  
  •  
  •