Blank import в Go: зачем пишут import _

Обновлено 2026-06-15

Иногда в Go можно встретить такой импорт:

import _ "github.com/lib/pq"

У новичков он обычно вызывает справедливый вопрос: зачем импортировать пакет, если мы его нигде не используем?

Ответ простой: пакет нужен не ради функций или типов, а ради побочного эффекта при инициализации.

Что такое init()

В Go у пакета может быть специальная функция init():

func init() {
    sql.Register("postgres", &Driver{})
}

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

  1. Инициализируются импортированные пакеты.
  2. Инициализируются переменные пакета.
  3. Выполняется init().
  4. Запускается main().

init() нельзя вызвать руками. Go сам вызовет ее, когда пакет будет загружен.

Зачем нужен import _

Когда мы пишем:

import (
    "database/sql"

    _ "github.com/lib/pq"
)

мы говорим компилятору: напрямую этот пакет мне не нужен, но его инициализацию выполнить надо.

Драйвер PostgreSQL регистрируется внутри database/sql, и после этого можно писать:

db, err := sql.Open("postgres", dsn)

Без blank import пакет database/sql просто не узнает, что такое драйвер postgres. Сам database/sql дает общий интерфейс, а конкретный драйвер добавляется через регистрацию.

Еще один пример: image/png

Похожий паттерн встречается не только в базах данных:

import (
    "image"

    _ "image/png"
)

Пакет image/png регистрирует PNG-декодер. После этого image.Decode сможет понимать PNG-файлы, хотя напрямую к image/png мы не обращаемся.

Когда init() уместен

init() можно использовать и в своих проектах, но важно держать правило: side effect должен быть маленьким и ожидаемым.

Хороший вариант:

func init() {
    registerDriver()
}

Плохой вариант:

func init() {
    connectToDatabase()
    runMigrations()
    startWorkers()
}

Почему плохо? Потому что импорт пакета внезапно начинает делать слишком много. Ты просто подключил пакет, а он уже полез в базу, запустил горутины и сломал тесты.

Простое правило

import _ и init() очень полезны, когда нужно зарегистрировать драйвер, формат, плагин или другую небольшую интеграцию.

Но если внутри начинается бизнес-логика, подключение к внешним сервисам или запуск процессов, лучше сделать явный вызов из main() или из wiring-слоя. Так код проще читать, тестировать и контролировать.

Оригинальный пост: t.me/go_for_us/38.

Похожие статьи