Go (или Golang) — это компилируемый, строго типизированный язык программирования от Google, созданный для разработки быстрых, надёжных и масштабируемых приложений.
💡 Особенности:
- Простота синтаксиса, как у Python
- Производительность на уровне C
- Встроенная конкурентность через
goroutines
- Идеально для серверной разработки, микросервисов, сетевых программ, CLI-инструментов
- Официальный сайт: https://go.dev/
- Установка: https://go.dev/doc/install
- Архив всех версий: https://go.dev/dl/
- Онлайн-песочница: https://go.dev/play/
Go использует модули для управления зависимостями и сборкой. Каждый проект — это отдельный модуль.
Пример структуры:
amfs/
├── cmd/
│ └── amfs/ ← точка входа (main.go) для приложения
├── internal/ ← внутренние пакеты (нельзя импортировать извне)
├── pkg/ ← переиспользуемые публичные пакеты
├── go.mod ← описание модуля и зависимостей
├── main.go ← стартовый файл
└── other.go ← дополнительные исходники
Функция 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.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 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
.
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
}
const (
A = iota // 0
B // 1
C // 2
)
Полезен для:
- Перечислений (
enum
) - Побитовых флагов
- Автоматической генерации чисел
Все задачи должны быть оформлены по базовой структуре проекта Go с соблюдением принципов DRY, KISS, SOLID.
Структура проекта:
project/
├── cmd/ # точка входа в приложение
├── internal/ # внутренняя логика
├── pkg/ # переиспользуемые пакеты
├── Makefile # команды управления
├── .gitignore # исключения для Git
├── .env # переменные окружения (опционально)
├── .env.dist # шаблон .env
Создать мини-приложение, которое:
- Оформлено по структуре
internal/pkg/cmd
- Совмещает в себе функционал двух предыдущих задач (в виде функций в отдельных модулях)
- Имеет
Makefile
с командами:make run
— запуск приложенияmake build
— сборка в../bin
make link
— проверка форматирования и статики (go fmt
,go vet
)
- Содержит
.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
Также показать грамотное применение логических конструкций и позитивных практик написания кода.
Массив — это фиксированная по длине коллекция однотипных элементов. Длина массива является частью его типа.
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
— копия массива (если не использовать указатель).
Слайс — это обёртка над массивом, включающая длину и ёмкость.
b := []int{1, 2, 3}
b = append(b, 4)
len(b)
— текущая длина,cap(b)
— ёмкость.append
может выделить новую память.- Слайсы передаются по ссылке.
- Срезы:
b[1:3]
,b[:2]
,b[2:]
- Можно делать срез от среза:
b2 := b[1:]
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)
}
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
type User struct {
Name string
Age int
}
u := User{Name: "Alice", Age: 30}
fmt.Println(u.Name)
- Можно создавать указатели
u := &User{}
- Сравнимы, если все поля сравнимы
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
как пример
Поле | Тип данных | Описание |
---|---|---|
id | bigint (PK) | Уникальный ID |
client_id | TEXT | ID клиента |
client_secret | TEXT | Секретный ключ |
uuid | uuid | Уникальный ID терминала в UUID формате |
Поле | Тип | Описание |
---|---|---|
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 |
Отклонено |
NEW
→AUTH
AUTH
→CHARGE
илиCANCEL
CHARGE
→REFUND
- Глобальный конфиг через
viper
- Подключение к БД
- Автомиграции (по флагу из конфига)
- Создание транзакций и смена статусов
Методы — это функции, которые привязаны к определённому типу.
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
— это встроенный интерфейс:
type error interface {
Error() string
}
Любой тип, реализующий Error() string
, можно использовать как ошибку.
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("неожиданная ошибка")
Паника может происходить и неявно:
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
используется внутри defer
для перехвата паники и предотвращения аварийного завершения:
func safeCall() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Восстановлено после паники:", r)
}
}()
mightPanic()
}
Используйте 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)
-
Авторизация по
client_id
иclient_secret
-
Генерация JWT с:
terminal_id
- временем жизни (например, 1 час)
-
Middleware:
- Проверяет наличие и валидность JWT в заголовке
Authorization: Bearer <token>
- Добавляет информацию о терминале в
context
- Проверяет наличие и валидность JWT в заголовке
- Обёртки ошибок:
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
Конкурентность — это способность программы выполнять несколько задач одновременно.
Используй конкурентность, когда:
- операция может выполняться параллельно (например, запросы к 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 позволяет удобно читать данные из канала до его закрытия:
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 {
case val := <-ch1:
fmt.Println("Получено из ch1:", val)
case ch2 <- 10:
fmt.Println("Отправлено в ch2")
default:
fmt.Println("Нет доступных операций")
}
time.After возвращает канал, который отправляет значение через заданное время, позволяя реализовать таймауты в горутинах.
select {
case val := <-ch:
fmt.Println("Получено:", val)
case <-time.After(2 * time.Second):
fmt.Println("Таймаут через 2 секунды")
}
-
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 позволяет дождаться завершения всех запущенных горутин. Используйте, когда нужно синхронизировать выполнение и избежать преждевременного выхода из программы.
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 используется для синхронизации доступа к общим данным между горутинами.
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
Для тестирования сценариев:
-
скрытые структуры и приватные поля,
-
код, зависящий от внутреннего состояния,
-
работу с памятью и нестандартными типами данных.
В Go такие задачи можно решать с помощью:
-
Рефлексии (
reflect
) — для анализа и изменения значений во время выполнения программы; -
Пакета
unsafe
— для работы с памятью напрямую и обхода ограничений компилятора (с осторожностью!); -
Расширенных возможностей тестирования (
testing, t.Run, t.Parallel
).
Рефлексия позволяет:
-
Узнавать тип переменной во время выполнения;
-
Читать и изменять значения, даже если они неизвестны на этапе компиляции;
-
Доступаться к приватным полям через обход правил.
Пример: чтение и изменение приватного поля структуры.
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
есть возможность получить прямой доступ к памяти по адресу переменной.
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
тесты можно организовывать с подзадачами (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 — встроенный инструмент 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