¿Qué se esconde detrás del guion bajo en import? Guía para quienes quieren entender Go más a fondo
¿Alguna vez has abierto código ajeno en Go y has visto una línea como import _ "github.com/lib/pq"? La primera reacción — «¿Y esto es legal? ¿El compilador no se quejará?». Spoiler: no se quejará. Es más, es una de las características más elegantes del lenguaje, que a menudo genera dudas en los principiantes. Analicemos qué hace realmente este misterioso símbolo de guion bajo antes de la importación en Go, y cómo usarlo con inteligencia.
En Stack Overflow, esta pregunta ha acumulado miles de visitas, y no es de extrañar. La sintaxis parece un error tipográfico, pero detrás hay un concepto profundo de inicialización de paquetes y efectos secundarios. En este artículo no solo daremos una definición seca, sino que mostraremos escenarios reales donde no se puede prescindir de _. ¡Allá vamos!
El problema: «Importé, pero no lo uso»
En Go hay una regla estricta: cada paquete importado debe ser utilizado. De lo contrario, el código no se compilará. Esto se hace por limpieza y rendimiento — sin carga muerta. Pero, ¿qué hacer si necesitamos simplemente ejecutar el código de un paquete al iniciar el programa, sin llamar directamente a sus funciones? Aquí es donde viene al rescate el blank identifier (identificador vacío) — el símbolo _.
Cuando escribes:
import _ "some/package"— le dices al compilador: «Sé que no llamaré a funciones de este paquete, pero, por favor, ejecuta su función init()». Go realizará la inicialización, pero no dará el error «unused import».
Cómo funciona init() y por qué es importante
Cada paquete en Go puede contener una o varias funciones init(). Se ejecutan automáticamente al importar el paquete, antes de la función main(). El orden de ejecución está estrictamente regulado:
- Primero se inicializan las variables del paquete
- Luego se llama a
init() - Si el paquete importa otros paquetes, estos se inicializan antes
Ejemplo para mayor claridad. Creemos un paquete config:
// config/config.gopackage config
var AppName string
func init() { AppName = "MyApp v1.0" println("Config initialized:", AppName)}Ahora en main.go podemos importar este paquete «en silencio»:
// main.gopackage main
import ( _ "example.com/project/config" "fmt")
func main() { fmt.Println("Main function started")}Resultado de la ejecución:
Config initialized: MyApp v1.0Main function startedObserva: no llamamos a config.AppName — simplemente importamos el paquete con guion bajo, y su init() se ejecutó automáticamente. Esta es la mecánica básica sobre la que se construyen muchas soluciones reales.
El escenario principal: registro de controladores de bases de datos
El ejemplo más común del uso de import _ en código de producción es el registro de controladores de bases de datos. El paquete estándar database/sql utiliza el patrón «fábrica»: todos los controladores se registran mediante sql.Register() en sus funciones init().
Mira cómo se conecta PostgreSQL (controlador lib/pq):
import ( "database/sql" _ "github.com/lib/pq" // registra el controlador "postgres")
func main() { db, err := sql.Open("postgres", "user=... dbname=...") // ...}Internamente, en lib/pq, dentro de init() hay algo como:
func init() { sql.Register("postgres", &Driver{})}Sin la importación vacía, el controlador no se habría registrado, y sql.Open("postgres", ...) devolvería un error «unknown driver». Lo mismo funciona para MySQL, SQLite, Oracle y otras bases de datos.