Язык Go — пустой идентификатор
Пустой идентификатор _ в Go: множественное присваивание, заглушки для неиспользуемых импортов, импорт ради побочного эффекта и проверка реализации интерфейса на этапе компиляции.
Содержание
Пустой идентификатор _ мы уже встречали в циклах 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-функции — без доступа к APIvar _ InterfaceName = (*Type)(nil)— статическая проверка реализации интерфейса на этапе компиляции; используется, когда в коде нет других статических преобразований