· 10 мин 👁 1.4k Начинающий

Язык Go — пустой идентификатор

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

blank identifierимпортинтерфейсыside effectошибки
Содержание

Пустой идентификатор _ мы уже встречали в циклах for range и при работе с картами. Но его применение шире. _ можно присвоить или объявить с любым значением любого типа — значение молча отбрасывается. Это аналог /dev/null в Unix: место, куда можно «записать» значение, которое не нужно, но синтаксис требует переменную.

Пустой идентификатор при множественном присваивании

Цикл for range — частный случай общей ситуации: множественного присваивания.

Если функция возвращает несколько значений, но одно из них не нужно — _ позволяет явно его отбросить, не создавая лишнюю переменную. Это делает намерение очевидным: значение не забыли использовать, а сознательно игнорируют.

Например, os.Stat возвращает информацию о файле и ошибку. Если нужна только ошибка:

if _, err := os.Stat(path); os.IsNotExist(err) {
    fmt.Printf("%s does not exist\n", path) // файл не существует
}

Игнорировать ошибку — плохая практика

Иногда встречается код, где ошибка отбрасывается через _. Это опасно:

// Плохо! Программа упадёт, если path не существует.
fi, _ := os.Stat(path)
if fi.IsDir() { // fi == nil при ошибке — паника
    fmt.Printf("%s is a directory\n", path)
}

Ошибки возвращаются не случайно. Всегда проверяйте их.

Неиспользуемые импорты и переменные

В Go неиспользованный импорт или необъявленная переменная — это ошибка компиляции. Неиспользуемые импорты раздувают бинарник и замедляют компиляцию; переменная, которую инициализировали но не используют — как минимум лишняя работа, как максимум признак более серьёзного бага.

Но при активной разработке бывает неудобно: импорт или переменная нужны, просто ещё не дописан код. Удалять их только чтобы скомпилироваться — а потом добавлять снова — раздражает. _ решает эту проблему.

Вот программа, которая не компилируется: два неиспользованных импорта (fmt и io) и неиспользованная переменная fd:

package main

import (
    "fmt"
    "io"
    "log"
    "os"
)

func main() {
    fd, err := os.Open("test.go")
    if err != nil {
        log.Fatal(err)
    }
    // TODO: использовать fd
}

Временное решение — заглушить ошибки компилятора через _:

package main

import (
    "fmt"
    "io"
    "log"
    "os"
)

var _ = fmt.Printf // Для отладки; удалить когда будет готово.
var _ io.Reader    // Для отладки; удалить когда будет готово.

func main() {
    fd, err := os.Open("test.go")
    if err != nil {
        log.Fatal(err)
    }
    // TODO: использовать fd
    _ = fd
}

var _ = fmt.Printf обращается к символу из пакета fmt — компилятор считает импорт использованным. var _ io.Reader делает то же для io. _ = fd подавляет ошибку о неиспользованной переменной.

По соглашению такие глобальные заглушки размещают сразу после блока import и снабжают комментарием — чтобы их было легко найти и не забыть убрать.

Импорт ради побочного эффекта

Иногда пакет нужен не ради его API, а ради того, что происходит при его инициализации. Например, пакет net/http/pprof в своей функции init регистрирует HTTP-хендлеры с отладочной информацией. Большинству программ не нужен его API — нужна только эта регистрация.

Чтобы импортировать пакет только ради побочного эффекта, его переименовывают в _:

import _ "net/http/pprof"

Такой импорт явно говорит: пакет нужен исключительно ради побочного эффекта. У пакета нет имени в этом файле — и использовать его по имени невозможно. Если бы имя было, но не использовалось, компилятор отверг бы программу.

Другой распространённый пример — регистрация драйверов баз данных:

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

func main() {
    db, err := sql.Open("postgres", "...") // драйвер уже зарегистрирован
    // ...
}

Проверка реализации интерфейса

Как мы уже видели, тип в Go реализует интерфейс неявно — достаточно иметь нужные методы. Большинство проверок соответствия интерфейсу происходят статически, на этапе компиляции: например, если передать *os.File в функцию, ожидающую io.Reader, компилятор проверит, что *os.File реализует io.Reader.

Но бывают проверки в рантайме. Пакет encoding/json определяет интерфейс json.Marshaler. Когда JSON-энкодер встречает значение, реализующее этот интерфейс, он вызывает его метод маршалинга вместо стандартного преобразования. Проверка происходит через type assertion:

m, ok := val.(json.Marshaler)

Если нужно только проверить, реализует ли тип интерфейс — не используя само значение — результат type assertion тоже можно отбросить через _:

if _, ok := val.(json.Marshaler); ok {
    fmt.Printf("значение %v типа %T реализует json.Marshaler\n", val, val)
}

Статическая гарантия реализации интерфейса

Есть тонкая ловушка: если тип должен реализовывать интерфейс, но в коде нет ни одного статического преобразования — компилятор не проверит это автоматически. Если тип случайно перестанет реализовывать интерфейс (например, из-за изменения сигнатуры метода), компилятор промолчит. Ошибка проявится только в рантайме.

Например, json.RawMessage должен реализовывать json.Marshaler для кастомного JSON-представления. Если он вдруг перестанет это делать, JSON-энкодер просто будет использовать стандартное преобразование — молча, без ошибки.

Чтобы получить гарантию на этапе компиляции, используют глобальное объявление с _:

// Эта строка не создаёт переменную — она существует только для проверки типов.
// Если *RawMessage не реализует json.Marshaler — программа не скомпилируется.
var _ json.Marshaler = (*RawMessage)(nil)

Разберём по частям:

  • (*RawMessage)(nil) — nil-указатель типа *RawMessage; значение не нужно, нужен только тип
  • присваивание в json.Marshaler заставляет компилятор проверить соответствие
  • var _ — результат отбрасывается, переменная не создаётся

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

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

Итоги

  • _ отбрасывает ненужные значения при множественном присваивании — вместо создания фиктивной переменной
  • Игнорировать ошибку через _ — плохая практика; ошибки нужно обрабатывать
  • var _ = pkg.Symbol и _ = variable временно подавляют ошибки компилятора при активной разработке; такие заглушки нужно удалить, когда код будет готов
  • import _ "pkg" импортирует пакет только ради побочного эффекта его init-функции — без доступа к API
  • var _ InterfaceName = (*Type)(nil) — статическая проверка реализации интерфейса на этапе компиляции; используется, когда в коде нет других статических преобразований