Что скрывается за подчеркиванием в 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() функции выполнятся. Главное — не допускайте цикли