Олег Марков
Функция setup в Go Golang
Введение
Функция setup в Go — это не встроенная конструкция языка, а распространённый паттерн. Её используют для подготовки окружения: инициализации зависимостей, конфигурации, временных ресурсов, моков и т.д. Чаще всего вы встретите функцию setup в тестах, но она полезна и в боевом коде, если нужно аккуратно собрать сложные объекты перед использованием.
Смотрите, я покажу вам, как на практике функция setup помогает:
- Уменьшить дублирование кода.
- Сделать тесты короче и понятнее.
- Явно описать зависимости компонента.
- Стандартизировать процесс инициализации приложения или подсистемы.
Давайте разберёмся, как устроен этот паттерн, какие варианты реализации бывают, где его стоит применять и как не перегнуть палку.
Что такое функция setup и зачем она нужна
Общая идея
Под функцией setup обычно понимают обычную Go-функцию, которая:
- создаёт и настраивает нужные объекты;
- возвращает эти объекты вызывающему коду;
- иногда дополнительно возвращает функцию teardown (для очистки ресурсов).
Общий смысл: вы выносите повторяющуюся инициализацию в одно место. В тестах это особенно заметно, когда у каждого теста одна и та же подготовка данных, окружения или зависимостей.
Где обычно применяется setup
Чаще всего функция setup встречается в трёх контекстах:
В модульных тестах:
- поднятие моков и фейковых реализаций;
- инициализация in-memory БД;
- подготовка тестовых конфигураций.
В интеграционных тестах:
- создание временных каталогов и файлов;
- запуск тестового HTTP-сервера;
- подключение к тестовой БД или Docker-контейнеру.
В приложении:
- функция setup как часть инициализации приложения (например, в main);
- построение зависимостей (DI без сторонних фреймворков);
- выделенная setup-функция для каждого модуля (setupHTTP, setupDB и т.д.).
Давайте теперь посмотрим, как это выглядит в коде в разных сценариях.
Функция setup в модульных тестах
Базовый вариант setup в тесте
Один из самых частых кейсов — подготовка общего окружения для группы тестов. Пример:
package user_test
import (
"testing"
)
// UserService - простая зависимость, которую нам нужно инициализировать
type UserService struct {
repo UserRepository
}
// UserRepository - интерфейс репозитория пользователей
type UserRepository interface {
Create(name string) error
}
// fakeUserRepo - фейковая реализация репозитория для тестов
type fakeUserRepo struct {
created []string
}
func (f *fakeUserRepo) Create(name string) error {
// Здесь мы просто сохраняем имя в памяти
f.created = append(f.created, name)
return nil
}
// setup - функция, которая готовит UserService и возвращает его
func setup() *UserService {
// Инициализируем фейковый репозиторий
repo := &fakeUserRepo{}
// Собираем сервис с нужными зависимостями
svc := &UserService{
repo: repo,
}
// Возвращаем готовый сервис
return svc
}
func TestUserCreate(t *testing.T) {
// Вызываем setup один раз в начале теста
svc := setup()
// Дальше используем подготовленный сервис
if err := svc.repo.Create("Alice"); err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
Здесь функция setup:
- инкапсулирует детали создания fakeUserRepo;
- собирает UserService;
- возвращает готовый объект для теста.
Это простой, но на практике очень распространённый шаблон.
Добавляем teardown (очистку)
Иногда одного setup мало: после теста нужно очистить ресурсы — закрыть соединения, удалить файлы, остановить сервер и т.д. В таких случаях удобно, чтобы setup возвращала не только подготовленные объекты, но и функцию teardown.
Давайте разберём пример с временным каталогом:
package files_test
import (
"os"
"path/filepath"
"testing"
)
// setup - готовит временный каталог и возвращает путь и функцию очистки
func setup(t *testing.T) (string, func()) {
// t.Helper помечает эту функцию как вспомогательную для теста
t.Helper()
// Создаём временную директорию
dir, err := os.MkdirTemp("", "files_test_*")
if err != nil {
// Если setup не удалось, тест дальше нет смысла выполнять
t.Fatalf("failed to create temp dir: %v", err)
}
// Возвращаем путь и функцию teardown
teardown := func() {
// Удаляем директорию рекурсивно
_ = os.RemoveAll(dir)
}
return dir, teardown
}
func TestWriteFile(t *testing.T) {
dir, teardown := setup(t)
// Обязательно вызываем teardown в конце теста
defer teardown()
// Здесь мы создаём путь к файлу внутри временной директории
filePath := filepath.Join(dir, "test.txt")
// Записываем файл
if err := os.WriteFile(filePath, []byte("hello"), 0o600); err != nil {
t.Fatalf("write failed: %v", err)
}
}
Обратите внимание:
- setup принимает *testing.T, чтобы иметь возможность вызывать t.Helper и t.Fatalf;
- teardown — замыкание, которое знает, что и где нужно удалить;
- в тесте используется defer teardown(), чтобы гарантировать выполнение очистки.
Такой паттерн setup + teardown вы будете встречать очень часто.
Setup для группы тестов (table-driven tests)
Часто вы хотите использовать один и тот же setup для нескольких кейсов. Здесь удобно комбинировать setup с табличными тестами.
Давайте посмотрим на пример:
package math_test
import "testing"
// Calculator - простой тип для примера
type Calculator struct {
base int
}
func (c *Calculator) Add(n int) int {
// Складываем число с базовым значением
return c.base + n
}
// setupCalculator - готовит калькулятор для тестов
func setupCalculator(t *testing.T) *Calculator {
t.Helper()
// Инициализируем калькулятор с базовым значением
return &Calculator{base: 10}
}
func TestCalculator_Add(t *testing.T) {
// Подготовка выполняется один раз в начале теста
calc := setupCalculator(t)
tests := []struct {
name string
in int
want int
}{
{"add positive", 5, 15},
{"add zero", 0, 10},
{"add negative", -3, 7},
}
for _, tt := range tests {
// Запускаем под-тест
t.Run(tt.name, func(t *testing.T) {
// Здесь можно вызывать setup внутри каждого под-теста,
// если нужно независимое состояние
got := calc.Add(tt.in)
if got != tt.want {
t.Errorf("Add(%d) = %d, want %d", tt.in, got, tt.want)
}
})
}
}
Смотрите, я специально вынес инициализацию калькулятора в отдельную setupCalculator, чтобы:
- можно было легко менять базовое значение;
- в других тестах использовать тот же способ инициализации;
- уменьшить шум в теле самого теста.
Если бы калькулятор имел сложные зависимости, выгода была бы ещё более заметна.
Setup в интеграционных тестах
Подготовка HTTP-сервера
В интеграционных тестах очень часто приходится поднимать HTTP-сервер. Давайте создадим функцию setupServer, которая:
- инициализирует роутер;
- запускает тестовый HTTP-сервер;
- возвращает URL и функцию остановки.
package api_test
import (
"net/http"
"net/http/httptest"
"testing"
)
// setupServer - поднимает HTTP-сервер для тестов
func setupServer(t *testing.T) (*httptest.Server, string) {
t.Helper()
// Создаём обработчик для тестов
handler := http.NewServeMux()
// Регистрируем тестовый маршрут
handler.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
// Отвечаем простым текстом
_, _ = w.Write([]byte("pong"))
})
// Создаём тестовый сервер
srv := httptest.NewServer(handler)
// Возвращаем сервер и его URL
return srv, srv.URL
}
func TestPing(t *testing.T) {
srv, baseURL := setupServer(t)
// Закрываем сервер после теста
defer srv.Close()
// Выполняем запрос к тестовому серверу
resp, err := http.Get(baseURL + "/ping")
if err != nil {
t.Fatalf("request failed: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("unexpected status: %d", resp.StatusCode)
}
}
Как видите, этот код:
- прекрасно демонстрирует роль setup как подготовщика окружения;
- скрывает детали создания сервера;
- делает тело теста коротким: только запрос и проверка результата.
Setup с подключением к БД
Сейчас покажу вам ещё один пример — когда setup берёт на себя подготовку соединения с БД. Для простоты рассмотрим in-memory SQLite (через modernc.org/sqlite или любую другую реализацию), но шаблон тот же и для PostgreSQL, MySQL и т.д.
package storage_test
import (
"database/sql"
"testing"
_ "modernc.org/sqlite" // Подключаем драйвер SQLite
)
// setupDB - готовит соединение с тестовой базой
func setupDB(t *testing.T) (*sql.DB, func()) {
t.Helper()
// Создаём in-memory базу SQLite
db, err := sql.Open("sqlite", ":memory:")
if err != nil {
t.Fatalf("failed to open db: %v", err)
}
// Настраиваем соединение (лимиты, опции)
db.SetMaxOpenConns(1)
// Создаём таблицу для тестов
_, err = db.Exec(`CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)`)
if err != nil {
t.Fatalf("failed to create table: %v", err)
}
// Функция очистки
teardown := func() {
// Закрываем соединение
_ = db.Close()
}
return db, teardown
}
func TestInsertUser(t *testing.T) {
db, teardown := setupDB(t)
defer teardown()
// Вставляем данные для проверки
_, err := db.Exec(`INSERT INTO users (name) VALUES (?)`, "Alice")
if err != nil {
t.Fatalf("insert failed: %v", err)
}
// Проверяем, что запись появилась
var count int
if err := db.QueryRow(`SELECT COUNT(*) FROM users`).Scan(&count); err != nil {
t.Fatalf("select failed: %v", err)
}
if count != 1 {
t.Fatalf("unexpected row count: %d", count)
}
}
Здесь функция setupDB:
- создаёт соединение с БД;
- создаёт схему (таблицу);
- возвращает db и teardown.
Такую функцию можно многократно переиспользовать в разных тестах, не повторяя одно и то же.
Setup в боевом коде приложения
Setup как часть инициализации в main
Функция setup полезна не только в тестах. Очень часто её используют в пакете main для подготовки всех зависимостей приложения.
Смотрите пример небольшого HTTP-сервиса:
package main
import (
"database/sql"
"log"
"net/http"
"os"
_ "github.com/lib/pq" // Драйвер PostgreSQL
)
// App - структура, описывающая наше приложение
type App struct {
DB *sql.DB
Logger *log.Logger
Mux *http.ServeMux
}
// setup - готовит экземпляр приложения
func setup() (*App, error) {
// Читаем конфигурацию из переменных окружения
dsn := os.Getenv("DB_DSN")
if dsn == "" {
// Возвращаем ошибку, если конфигурации нет
return nil, ErrMissingDSN
}
// Подключаемся к базе
db, err := sql.Open("postgres", dsn)
if err != nil {
return nil, err
}
// Проверяем подключение
if err := db.Ping(); err != nil {
return nil, err
}
// Настраиваем логгер
logger := log.New(os.Stdout, "app ", log.LstdFlags)
// Создаём роутер
mux := http.NewServeMux()
// Регистрируем обработчики
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
// Возвращаем простой статус
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
})
// Собираем приложение
app := &App{
DB: db,
Logger: logger,
Mux: mux,
}
// Возвращаем готовое приложение
return app, nil
}
// ErrMissingDSN - ошибка отсутствия DSN
var ErrMissingDSN = fmt.Errorf("DB_DSN is required")
func main() {
app, err := setup()
if err != nil {
log.Fatalf("setup failed: %v", err)
}
// Запускаем HTTP-сервер
if err := http.ListenAndServe(":8080", app.Mux); err != nil {
log.Fatalf("server failed: %v", err)
}
}
Покажу вам, почему такой подход удобен:
- настройка зависимостей изолирована от функции main;
- функцию setup можно переиспользовать в тестах (создавать приложение без запуска сервера);
- код main остаётся очень простым и читаемым.
Setup на уровне модуля
Когда проект растёт, бывает полезно иметь несколько отдельных setup-функций:
- setupLogger
- setupDB
- setupHTTP
- setupConfig
Давайте посмотрим, как можно разбить инициализацию на несколько функций, а затем собрать всё в одну:
package main
import (
"database/sql"
"log"
"net/http"
"os"
)
// setupLogger - настраивает логгер
func setupLogger() *log.Logger {
// Создаём логгер, который пишет в stdout
return log.New(os.Stdout, "app ", log.LstdFlags|log.Lshortfile)
}
// setupDB - настраивает подключение к базе
func setupDB(logger *log.Logger) (*sql.DB, error) {
dsn := os.Getenv("DB_DSN")
if dsn == "" {
// Логируем проблему конфигурации
logger.Println("DB_DSN is empty")
return nil, ErrMissingDSN
}
db, err := sql.Open("postgres", dsn)
if err != nil {
return nil, err
}
if err := db.Ping(); err != nil {
return nil, err
}
return db, nil
}
// setupHTTP - готовит HTTP-маршруты
func setupHTTP(db *sql.DB, logger *log.Logger) *http.ServeMux {
mux := http.NewServeMux()
// Пример обработчика, использующего db и logger
mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
// Здесь можно работать с базой данных и логгером
logger.Println("handle /users")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("users list"))
})
return mux
}
// setupApp - собирает всё воедино
func setupApp() (*http.ServeMux, error) {
// Сначала настраиваем логгер
logger := setupLogger()
// Затем настраиваем базу
db, err := setupDB(logger)
if err != nil {
return nil, err
}
// И в конце настраиваем HTTP
mux := setupHTTP(db, logger)
return mux, nil
}
Давайте посмотрим, что даёт такой разнос:
- каждая setup-функция отвечает за свою подсистему;
- зависимости передаются явно (logger → db → http);
- вы легко можете протестировать каждую setup-функцию отдельно.
Таким образом, слово setup в названиях хорошо передаёт идею: “здесь происходит подготовка и конфигурация”.
Паттерны реализации функции setup
Вариант 1. Возврат значения или структуры
Это самый простой и распространённый вариант: setup возвращает один объект или структуру со всеми нужными зависимостями.
Пример: структура TestEnv для тестов.
package service_test
import (
"log"
"testing"
)
// TestEnv - окружение для тестов
type TestEnv struct {
Logger *log.Logger
// Здесь могут быть другие поля - моки, фейки и т.д.
}
// setupEnv - создаёт и настраивает окружение для тестов
func setupEnv(t *testing.T) *TestEnv {
t.Helper()
logger := log.New(testWriter{t}, "test ", log.LstdFlags)
return &TestEnv{
Logger: logger,
}
}
// testWriter - адаптер, пишущий в лог теста
type testWriter struct {
t *testing.T
}
func (w testWriter) Write(p []byte) (int, error) {
// Пишем в лог теста
w.t.Logf("%s", p)
return len(p), nil
}
Такой паттерн особенно удобен, если у вас много зависимостей, и вы хотите всё держать в одной структуре.
Вариант 2. Возврат значения + teardown
Мы уже смотрели такой вариант в тестах. Он особенно полезен, когда:
- вы создаёте ресурсы, требующие освобождения;
- вам нужна гарантия, что очистка выполнится в конце теста или функции.
Форма сигнатуры обычно такая:
func setupSomething(t *testing.T) (*Something, func()) {
// Настраиваем ресурс
// ...
return obj, func() {
// Очищаем ресурс
// ...
}
}
Здесь важно не забывать вызывать teardown:
obj, teardown := setupSomething(t)
defer teardown()
Вариант 3. Setup как метод на структуре
Иногда удобно сделать setup методом на типе, особенно если вы работаете с тестовыми фикстурами.
type TestFixture struct {
DB *sql.DB
// другие поля
}
// Setup - метод, который готовит фикстуру
func (f *TestFixture) Setup(t *testing.T) {
t.Helper()
// Инициализируем DB и другие поля
// ...
}
// Teardown - метод очистки
func (f *TestFixture) Teardown() {
// Закрываем ресурсы
// ...
}
func TestSomething(t *testing.T) {
var fx TestFixture
fx.Setup(t)
defer fx.Teardown()
// Используем fx.DB и другие поля
}
Такой подход хорошо подходит, если у тестов много общего окружения и вы хотите сгруппировать его в один тип.
Рекомендации по дизайну функции setup
Делайте setup максимально явной
Полезно, чтобы функция setup:
- принимала все необходимые параметры явно;
- не прятала важную логику в глобальные переменные;
- не изменяла глобальное состояние без крайней необходимости.
Например, вместо:
var globalDB *sql.DB
func setup() {
// Плохо - записываем в глобальную переменную
globalDB = createDB()
}
лучше:
func setupDB() *sql.DB {
// Хорошо - возвращаем значение вызывающему коду
return createDB()
}
Так вам будет проще тестировать код и понимать, откуда и куда текут зависимости.
Не перегружайте одну setup-функцию всем подряд
Если вы чувствуете, что в setup появилась логика, которая:
- сама по себе сложная;
- содержит условия, циклы, ветвления;
- начинает обрабатывать много разных сценариев,
то стоит вынести часть кода в отдельные функции или типы.
Например, вместо одной огромной setup, которая инициализирует БД, кеш, HTTP, конфиг и очередь, лучше сделать:
- setupConfig
- setupLogger
- setupDB
- setupCache
- setupHTTP
- setupQueue
А затем собрать всё в одной функции, которая вызывает их по очереди.
Используйте t.Helper в тестовых setup
Когда вы пишете setup для тестов, почти всегда стоит вызывать t.Helper() в начале:
func setupSomething(t *testing.T) *Something {
t.Helper()
// дальше обычная логика
}
Это помогает при отладке: стек вызовов в ошибках будет указывать на строку в самом тесте, а не в функции setup.
Следите за временем работы setup
Иногда setup может занимать значительное время (например, запуск Docker-контейнера или миграций БД). В тестах это особенно критично.
Несколько практичных советов:
- По возможности используйте ленивую инициализацию (один setup на пакет, а не на каждый тест).
- Воспользуйтесь TestMain, если нужно настроить окружение один раз перед всем пакетом тестов.
- Для тяжёлых интеграционных тестов заведите отдельный пакет или тег сборки, чтобы не замедлять быстрые модульные тесты.
Распространённые ошибки при использовании setup
Скрытые глобальные зависимости
Проблема: setup записывает данные в глобальные переменные, а тесты не изолированы друг от друга.
Пример проблемы:
var db *sql.DB
func setup(t *testing.T) {
t.Helper()
// Переоткрываем глобальную переменную
db = createTestDB()
}
Разные тесты могут менять одно и то же глобальное состояние, и вы получите нестабильные, “мигающие” тесты.
Лучше:
func setup(t *testing.T) *sql.DB {
t.Helper()
return createTestDB()
}
Так вы явно передаёте db туда, где она нужна.
Отсутствие teardown там, где он нужен
Если setup создаёт внешние ресурсы (каталоги, файлы, соединения, серверы), но вы забываете их закрыть, то:
- тесты могут “протекать” (оставлять мусор);
- в CI могут кончиться дескрипторы файлов или портов;
- локальная машина со временем зарастёт временными файлами.
Всегда, когда вы видите в setup создание чего-то внешнего, задавайте себе вопрос: нужна ли для этого teardown?
Слишком умный setup
Иногда setup пытается угадывать, что от него хотят, и делает слишком много:
- загружает конфиг из 3 разных мест;
- меняет поведение в зависимости от переменных окружения;
- использует флаги командной строки.
Для тестов такой “умный” setup может быть проблемой — вы теряете предсказуемость и контроль. Лучше, чтобы setup был детерминированным и предсказуемым, а всё, что может меняться, передавалось ему явно параметрами.
Смешивание тестовой и боевой логики в одном setup
Ещё одна частая ошибка — использовать одну и ту же функцию setup и для продового кода, и для тестов, при этом внутри делать условия типа “если тест, то вот это, если прод, то другое”.
Например:
func setup() *App {
if isTestEnv() {
// используем in-memory базу
} else {
// подключаемся к настоящей базе
}
}
Лучше разделять:
- setupProdApp
- setupTestApp
или иметь конструктор, который принимает интерфейсы/фабрики зависимостей, а уже в тестах и в проде вы передаёте разные реализации.
Заключение
Функция setup в Go — это паттерн, а не встроенный механизм языка. Именно поэтому у вас есть свобода выбирать форму и уровень абстракции, который лучше всего подходит вашему проекту и конкретной задаче.
Подведём основные выводы:
- setup — это обычная функция, которая готовит окружение: объекты, ресурсы, конфигурацию;
- особенно полезна в тестах, где помогает убрать дублирование и сделать код понятнее;
- в боевом коде setup улучшает структуру инициализации, делает зависимостей явными;
- сочетание setup + teardown — удобный паттерн для ресурсов, требующих очистки;
- важно следить за тем, чтобы setup была:
- предсказуемой;
- явной по зависимостям;
- не слишком перегруженной логикой;
- аккуратно работала с глобальным состоянием.
Теперь, когда вы знаете, как устроена функция setup, вы можете осознанно использовать её в тестах и приложениях, строя более понятную и поддерживаемую архитектуру.
Частозадаваемые технические вопросы по теме статьи и ответы на них
Как сделать один общий setup для всего пакета тестов, а не вызывать его в каждом тесте?
Для этого используйте TestMain:
func TestMain(m *testing.M) {
// Здесь вы делаете общий setup - БД, конфиг, внешние сервисы
code := m.Run() // Запуск всех тестов пакета
// Здесь можно сделать общий teardown
os.Exit(code)
}
Если нужны значения из setup, сохраните их в пакетных переменных и используйте в тестах.
Как передавать t *testing.T в setup, если функция вызывается не напрямую из теста, а глубже?
Просто прокидывайте t по стеку вызовов:
func TestSomething(t *testing.T) {
env := buildEnv(t)
// ...
}
func buildEnv(t *testing.T) *Env {
t.Helper()
return setupEnv(t)
}
Важно, чтобы самая верхняя функция, которая владеет тестом, принимала *testing.T и передавала его дальше.
Можно ли использовать setup в бенчмарках и как это сделать правильно?
Да, но учитывайте, что код внутри цикла b.N должен измеряться, а setup — нет. Делайте подготовку до цикла:
func BenchmarkSomething(b *testing.B) {
env := setupEnvForBench(b)
b.ResetTimer() // Сбрасываем таймер, чтобы не учитывать setup
for i := 0; i < b.N; i++ {
// Здесь замеряется производительность
doWork(env)
}
}
Если нужен teardown, вызовите его после цикла.
Как протестировать саму функцию setup?
Относитесь к setup как к обычной функции. Пишите тесты, которые вызывают её и проверяют результат:
func TestSetupApp(t *testing.T) {
app, err := setupApp()
if err != nil {
t.Fatalf("setupApp failed: %v", err)
}
if app.DB == nil {
t.Fatalf("DB is nil after setup")
}
}
Если setup сложная, имеет смысл разбить её на более мелкие функции и тестировать их отдельно.
Как быть, если setup зависит от переменных окружения, а их нужно менять в разных тестах?
Меняйте переменные окружения в самом тесте перед вызовом setup и очищайте после:
func TestWithCustomEnv(t *testing.T) {
t.Setenv("DB_DSN", "test-dsn") // Go 1.17+
app, err := setupApp()
if err != nil {
t.Fatalf("setupApp failed: %v", err)
}
// Дальше работайте с app
}
t.Setenv автоматически вернёт значение переменной в исходное состояние по окончании теста, что удобно и безопасно.
Постройте личный план изучения Vue до уровня Middle — бесплатно!
Vue — часть карты развития Frontend
100+ шагов развития
30 бесплатных лекций
300 бонусных рублей на счет
Бесплатные лекции
Все гайды по Vue
Лучшие курсы по теме

Vue 3 и Pinia
Антон Ларичев
TypeScript с нуля
Антон Ларичев