Зачем подчеркивание перед import в Go? Разбираем на примерах

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Что скрывается за подчеркиванием в import? Гайд для тех, кто хочет понять Go глубже

Вы когда-нибудь открывали чужой код на Go и видели строку вроде import _ "github.com/lib/pq"? Первая реакция — «А это вообще законно? Компилятор не ругнется?». Спойлер: не ругнется. Более того, это одна из самых элегантных фич языка, которая часто вызывает вопросы у новичков. Давайте разберемся, что на самом деле делает этот загадочный символ подчеркивания перед импортом в Go, и как его использовать с умом.

На Stack Overflow этот вопрос собрал тысячи просмотров — и неудивительно. Синтаксис выглядит как опечатка, но за ним стоит глубокая концепция инициализации пакетов и побочных эффектов. В этой статье мы не просто дадим сухое определение, а покажем реальные сценарии, где без _ не обойтись. Поехали!

Проблема: «Импортировал, но не использую»

В Go есть строгое правило: каждый импортированный пакет должен быть использован. Иначе код не скомпилируется. Это сделано для чистоты и производительности — никакого мертвого груза. Но что делать, если нам нужно просто выполнить код пакета при старте программы, не вызывая его функций напрямую? Тут и приходит на помощь blank identifier (пустой идентификатор) — символ _.

Когда вы пишете:

import _ "some/package"

— вы говорите компилятору: «Я знаю, что не буду вызывать функции из этого пакета, но, пожалуйста, выполни его init() функцию». Go выполнит инициализацию, но при этом не даст ошибку «unused import».

Как работает init() и почему это важно

Каждый пакет в Go может содержать одну или несколько функций init(). Они выполняются автоматически при импорте пакета, до функции main(). Порядок выполнения строго регламентирован:

  • Сначала инициализируются переменные пакета
  • Затем вызывается init()
  • Если пакет импортирует другие пакеты — они инициализируются раньше

Пример для наглядности. Создадим пакет config:

// config/config.go
package config

var AppName string

func init() {
    AppName = "MyApp v1.0"
    println("Config initialized:", AppName)
}

Теперь в main.go мы можем импортировать этот пакет «молча»:

// main.go
package main

import (
    _ "example.com/project/config"
    "fmt"
)

func main() {
    fmt.Println("Main function started")
}

Результат выполнения:

Config initialized: MyApp v1.0
Main function started

Обратите внимание: мы не вызывали config.AppName — мы просто импортировали пакет с подчеркиванием, и его init() сработал автоматически. Это базовая механика, на которой строятся многие реальные решения.

Главный сценарий: регистрация драйверов баз данных

Самый частый пример использования import _ в production-коде — это регистрация драйверов баз данных. Стандартный пакет database/sql использует паттерн «фабрика»: все драйверы регистрируются через sql.Register() в своих init() функциях.

Взгляните, как подключается PostgreSQL (драйвер lib/pq):

import (
    "database/sql"
    _ "github.com/lib/pq" // регистрирует драйвер "postgres"
)

func main() {
    db, err := sql.Open("postgres", "user=... dbname=...")
    // ...
}

Внутри lib/pq в init() написано что-то вроде:

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

Без пустого импорта драйвер не был бы зарегистрирован, и sql.Open("postgres", ...) вернул бы ошибку «unknown driver». То же самое работает для MySQL, SQLite, Oracle и других БД. Это стандартный индустриальный паттерн, и каждый Go-разработчик должен его знать.

Другие реальные примеры

  • HTTP-серверы: пакет net/http/pprof регистрирует обработчики для профилирования. Просто импортируйте его пустым — и эндпоинты /debug/pprof/ появятся автоматически.
  • Тестирование: библиотеки вроде gomega или testify могут регистрировать глобальные хелперы через init().
  • Метрики и логирование: некоторые системы мониторинга требуют пустой импорт для включения сбора метрик.

Подводные камни: что нужно знать

Несмотря на элегантность, у import _ есть особенности, которые стоит учитывать:

Порядок инициализации может быть неочевидным

Если у вас несколько пакетов с init(), порядок их выполнения зависит от графа зависимостей. В сложных проектах это может привести к неожиданным багам. Совет: не полагайтесь на порядок вызовов init() между несвязанными пакетами.

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

init() выполняется один раз при старте программы. Если вам нужно что-то загружать по требованию — используйте обычные функции или sync.Once.

Следите за ошибками в init()

Если init() вызывает panic — программа упадет сразу при старте. Отладить это сложнее, чем обычную ошибку.

Частые вопросы новичков

Давайте разберем несколько вопросов, которые чаще всего задают на форумах:

«Можно ли использовать подчеркивание для обычных пакетов?»

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

«Чем отличается import _ от import . (точка)?»

import . "pkg" импортирует все экспортируемые имена в текущее пространство имен — вы можете вызывать функции без префикса. import _ не импортирует вообще никаких имен, только запускает init(). Это две совершенно разные вещи.

«Можно ли импортировать несколько пакетов с подчеркиванием?»

Да, сколько угодно. Все их init() функции выполнятся. Главное — не допускайте цикли

Блоги

Рекомендаций по книгам