Avatar ·

Что означает символ подчеркивания перед импортом пакета в Go?

Всем привет! Я только начал изучать Go и столкнулся с непонятной для меня конструкцией. В чужом коде я увидел импорт пакета с символом подчеркивания (underscore) перед именем пакета: ```go import ( "fmt" _ "github.com/lib/pq" "database/sql" ) ``` Я понимаю, что `_` в Go используется как пустой идентификатор (blank identifier), когда мы хотим проигнорировать возвращаемое значение функции. Но что он делает в импорте? Ведь если мы импортируем пакет, то обычно используем его в коде, а тут явно написано, что мы его не используем (иначе компилятор выдаст ошибку). Я попробовал убрать этот импорт, и код перестал работать — компилятор выдал ошибку, что драйвер для PostgreSQL не найден. Но при этом сам пакет `pq` нигде в коде не используется. Как такое возможно? Зачем нужен такой импорт и как он работает? Объясните, пожалуйста, простыми словами. Я привык к Python, где импорт всегда означает явное использование модуля, а тут какая-то магия. <<>>

Что такое пустой импорт в Go?

Символ подчеркивания _ перед именем пакета в Go — это специальный синтаксис, который называется пустым импортом (blank import) или импортом ради побочного эффекта (import for side effect).

Как это работает?

В отличие от Python, в Go импорт пакета без его использования — это ошибка компиляции. Но есть ситуации, когда пакет нужно импортировать только ради его инициализации, не используя его функции или типы явно. Для этого и используется _.

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

Реальный пример: драйверы баз данных

Самый частый случай — это регистрация драйверов баз данных. Рассмотрим ваш пример с PostgreSQL:

package main

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

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

Пакет github.com/lib/pq в своей функции init() вызывает:

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

Это регистрирует драйвер в глобальном реестре пакета database/sql. После этого вы можете использовать строку "postgres" в sql.Open(). Без пустого импорта драйвер не будет зарегистрирован, и база данных не подключится.

Другие случаи использования

  • Регистрация HTTP-хендлеров — пакеты вроде net/http/pprof добавляют свои обработчики в стандартный HTTP-сервер
  • Автоматическая инициализация — загрузка конфигураций, подключение к сервисам
  • Тестирование — пакеты, которые должны инициализироваться для тестов, но не используются напрямую

Важные моменты

  • Функция init() выполняется один раз при первом импорте пакета
  • Порядок выполнения init() гарантирован: сначала в импортированных пакетах, потом в текущем
  • Пустой импорт — это единственный легальный способ импортировать пакет без его использования

Проверка: что произойдет?

Если вы напишете:

import (
    "fmt"
    _ "fmt" // Ошибка! Пакет уже импортирован
)

Компилятор выдаст ошибку, потому что один пакет нельзя импортировать дважды (даже с пустым идентификатором).

Альтернатива: точка-импорт

Существует похожий синтаксис import . "package", который делает все экспортируемые имена пакета доступными без префикса. Но это совсем другая история и обычно не рекомендуется.

<<>>

Дополнение: как найти, что делает пустой импорт

Если вы встретили пустой импорт и хотите понять, зачем он нужен, сделайте следующее:

  1. Найдите пакет в go.mod или в папке vendor
  2. Откройте файлы пакета и найдите функцию init() (их может быть несколько)
  3. Посмотрите, что регистрируется или инициализируется

Пример поиска

Для пакета github.com/lib/pq функция init() находится в файле conn.go:

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

Как видите, регистрируются два имени драйвера: "postgres" и "pq". Оба можно использовать в sql.Open().

Когда пустой импорт НЕ нужен

Если вы используете функции или типы из пакета напрямую, пустой импорт не нужен. Просто импортируйте пакет обычным способом:

import "fmt" // Нормальный импорт, используем fmt.Println()

import _ "fmt" // Ошибка компиляции: пакет импортирован, но не используется

Пустой импорт — это осознанное решение разработчика пакета, который хочет, чтобы его код выполнялся при импорте, но не хотел бы навязывать пользователям конкретные функции.