CodeGym /Cursos /Go SELF /init() y efectos secundarios

init() y efectos secundarios

Go SELF
Nivel 10, Lección 4
Disponible

1. init() en Go: que es y por que hay que ser prudente con ella

Cuando escribes tus primeros programas, quieres que todo arranque “solo”: importas un paquete y, de alguna forma, se prepara, se configura y empieza a funcionar. En Go existe para eso una funcion especial llamada init(). Se invoca automaticamente, sin tu participacion directa, y se ejecuta antes de main(). Es comoda, pero tambien peligrosa: init() convierte facilmente el programa en un conjunto de sorpresas.

La firma de init() es muy estricta: sin parametros y sin valores de retorno. Es decir, se ve asi:

package main

func init() {
	// automatic initialization
}

Precisamente por su caracter “automatico” se mira con cautela a init(): hace el trabajo de forma implicita. Y esa es la palabra clave de la leccion: implícito.

Mecanicamente, init() forma parte del proceso de inicializacion de paquetes: el runtime de Go ejecuta la inicializacion del paquete (incluida la asignacion de variables globales y las llamadas a init()) y solo despues pasa a main().

Orden de ejecucion de init(): una linea temporal sencilla

A uno le gustaria pensar que Go lee los archivos de arriba abajo y hace exactamente lo que has escrito. Dentro de un mismo archivo esa sensacion muchas veces es real, pero a nivel de paquetes y varios archivos el panorama es un poco mas complejo. Por eso conviene tener en la cabeza una linea temporal sencilla: que se ejecuta antes, que despues y que expectativas se pueden considerar seguras.

De forma simplificada, el orden es este:

  1. primero se inicializan los paquetes importados (sus variables y sus init()),
  2. despues se inicializa el paquete actual,
  3. y al final se ejecuta main() de package main.

Esta es la ilustracion (el esquema es a proposito muy tosco, para poder recordarlo sin apuntes):

flowchart TD
    A[Paquetes importados] --> B[Inicializacion de variables a nivel de package]
    B --> C["Llamadas a init() en el paquete actual"]
    C --> D["main()"]

El hecho mas fiable que necesitas al empezar es: init() siempre se ejecuta antes de main() (si el paquete participa en la compilacion).

Mini demostracion:

package main

import "fmt"

func init() {
	fmt.Println("init") // init
}

func main() {
	fmt.Println("main") // main
}

Eso es predecible y util para entender la mecanica. Pero a partir de aqui empiezan los matices.

Si tienes varios init() (y Go lo permite), todos se ejecutaran antes de main(). Pero el orden exacto entre lineas y entre archivos ya no es algo en lo que convenga apoyarse al disenar la logica. Si construyes el programa de manera que “en el init() del archivo A debe ejecutarse necesariamente antes que el init() del archivo B”, casi seguro estas sembrando un bug futuro; simplemente aun es pequeno y simpatico.

Efectos secundarios y por que init() los potencia

La expresion “efecto secundario” suena como una advertencia de medicamento: “puede causar somnolencia, ganas repentinas de reescribir el proyecto en Rust e intolerancia a las variables globales”. Pero en esencia es mas simple: un efecto secundario aparece cuando una funcion hace algo ademas de calcular un resultado. Por ejemplo, escribe en un archivo, modifica una variable global, imprime por pantalla o registra un handler “dentro del paquete”.

init() casi siempre trata sobre efectos secundarios, porque no devuelve nada. No puede decir “me he inicializado correctamente” o “no he podido leer la config, aqui tienes el error”. Por eso el estilo tipico de “haz algo importante y, si no sale bien, devuelve error” no encaja en init().

De aqui salen dos caminos tipicos, ambos incomodos.

Primer camino: “tragarse el problema en silencio”. Por ejemplo, intentaste preparar un valor, no salio bien y dejaste todo como estaba. Luego en main() aparece de repente "" o 0, y tu te quedas pensando quien te robo los datos.

Segundo camino: “entrar en panico”. Es decir, en init() se hace panic(...), para que el programa simplemente caiga al arrancar. A veces es aceptable, pero muchas veces convierte la experiencia del usuario (y la tuya al depurar) en un drama sin intermedio.

Comparemos dos enfoques con un ejemplo muy simple. Supongamos que queremos calcular una vez un “prefijo” para la salida.

Version con init() (inicializacion oculta):

package main

import "fmt"

var prefix string

func init() {
	prefix = ">> "
}

func main() {
	fmt.Println(prefix + "hello") // >> hello
}

Y ahora una version con inicializacion explicita (se ve enseguida donde ocurre):

package main

import "fmt"

func main() {
	prefix := ">> "
	fmt.Println(prefix + "hello") // >> hello
}

En un ejemplo de juguete como este la diferencia parece ridicula. Pero en un proyecto real “prefijo” pasa rapidamente a ser “config”, “conexion”, “registro”, “cache”, “tabla de codificacion”... y ahi es donde init() empieza a volver el codigo “magico”.

2. Cuando init() es aceptable y con que sustituirlo

Cuando init() realmente es aceptable

Prohibir por completo init() seria raro: el lenguaje lo tiene por algo. La cuestion no es “usar o no usar”, sino “que poner exactamente ahi para no llorar despues”.

La zona normal de uso de init() en tu nivel actual es una pequena configuracion que:

  1. no pueda “fallar” con un error normal (o el fallo sea realmente correcto),
  2. no dependa de la entrada del usuario, de archivos ni de la red,
  3. no oculte la logica de negocio del arranque de la aplicacion.

El ejemplo mas claro es preparar un generador de numeros pseudoaleatorios para algunas necesidades internas (no para criptografia, solo para crear un “ID aleatorio” o “barajar una lista”).

Vamos a integrarlo en una aplicacion de consola de aprendizaje. Supongamos que tenemos un mini programa que lee numeros e imprime estadisticas basicas (suma y maximo). Añadiremos un “ID aleatorio de ejecucion” para distinguir los arranques (solo a modo de demostracion).

package main

import (
	"fmt"
	"math/rand"
	"time"
)

var runID int

func init() {
	runID = rand.New(rand.NewSource(time.Now().UnixNano())).Intn(1000)
}

func main() {
	fmt.Println("run id:", runID) // run id: 123 (example)
}

Esto es aceptable como demostracion de la mecanica, pero observa: ya hemos traido time, ya hemos creado una variable global y ya hemos ocultado el sentido de “de donde sale runID”. No es un crimen, pero si una “deuda” que habra que recordar.

Si notas que init() empieza a crecer (condiciones, manejo de errores, lectura de algun sitio), casi siempre es una señal: toca parar y hacer una inicializacion explicita.

Preferimos la inicializacion explicita: initApp() se llama desde main

Cuando el programa es pequeno, init() parece comodo: “bueno, lo configuro una vez y ya”. Pero en cuanto aparecen varios pasos de arranque, quieres tener una ruta obvia: que ocurre primero, que despues, donde tratar el error y donde mostrar un mensaje claro.

Por eso un buen patron de aprendizaje —y en proyectos reales tambien aparece continuamente— es crear una funcion de inicializacion explicita, por ejemplo initApp(), y llamarla desde main().

Imaginemos que nuestro “mini calculador estadistico” debe tener un prefijo de salida y una hora de inicio. Hagamoslo de forma explicita:

package main

import (
	"fmt"
	"time"
)

func initApp() (string, time.Time) {
	prefix := "stats: "
	startedAt := time.Now()
	return prefix, startedAt
}

func main() {
	prefix, startedAt := initApp()
	fmt.Println(prefix, "started at", startedAt.Format(time.Kitchen))
}

Si, el codigo ha crecido unas pocas lineas. Pero ahora:

  • se ve que exactamente se inicializa,
  • se ve donde ocurre,
  • puedes añadir facilmente el manejo de errores (mediante (T, error)) sin convertirlo todo en panic.

Si initApp() pudiera fallar, lo hariamos asi:

package main

import (
	"errors"
	"fmt"
)

func initApp() (string, error) {
	prefix := "stats: "
	if prefix == "" {
		return "", errors.New("empty prefix")
	}
	return prefix, nil
}

func main() {
	prefix, err := initApp()
	if err != nil {
		fmt.Println("init error:", err)
		return
	}
	fmt.Println(prefix + "ready") // stats: ready
}

Fijate: esto es muy “de nuestro estilo” dentro del material ya visto — una funcion normal, un if normal err != nil { ... }, nada de magia. Y precisamente esa es la razon por la que la inicializacion explicita casi siempre gana a init() en el codigo de aplicacion.

3. Analogia con las clases

Si vienes a Go desde Java/Kotlin/C#/C++, el cerebro casi automaticamente intenta mapear las construcciones conocidas. Y eso es normal: asi se aprende mas rapido. La analogia mas util (y bastante honesta) en esta etapa es esta: un paquete en Go se lee a menudo como “una clase con un conjunto de static”.

Por que se parece.

  1. La sintaxis “nombre.puntoNombre” se parece a Class.method.
    Escribimos fmt.Println, json.Marshal, http.ListenAndServe. En lenguajes con clases eso recuerda mucho a Console.WriteLine(...) o Math.Max(...) — “llamada a una funcion estatica”.
  2. Dentro del paquete pueden vivir “cosas comunes” a nivel de paquete.
    Un paquete tiene funciones y variables a nivel de package. Se parece a campos/metodos static: “una entidad comun para todo el modulo”.
  3. init() recuerda a un constructor estatico.
    En Java puedes encontrar static { ... }, en C# un constructor estatico, en Kotlin init { ... } para un object/clase. El sentido es parecido: “en cuanto se conecto el modulo/clase, algo se ejecuto automaticamente”.

Aqui esta el limite importante: es una analogia para leer codigo, pero no una igualdad literal.

La diferencia principal: un paquete no es una clase

Un paquete no tiene instancias, no tiene this, no tiene “objeto de paquete”. Un paquete es, sobre todo:

  • un espacio de nombres (un grupo de nombres relacionados),
  • un limite de visibilidad (que se ve dentro del paquete y que se exporta hacia fuera),
  • un limite de inicializacion (precisamente a nivel de paquetes funciona init()).

Y el “verdadero comportamiento OO” en Go aparece no a traves de paquetes, sino a traves de tipos (struct) y sus metodos. Por eso el modelo mental correcto es este:

  • pkg.Name es “aqui tienes una cosa publica del espacio de nombres pkg”;
  • los metodos “como en las clases” no son pkg.method, sino value.Method() (y llegaremos a eso en el tema de metodos).

Es decir, el paquete ayuda a organizar el codigo, pero la “objetualidad” en Go vive en los tipos, no en los paquetes.

Por que precisamente en el tema init() esta analogia es especialmente util

Porque init() es el momento en el que alguien que viene de lenguajes OO espera un comportamiento familiar: “si lo he importado, significa que el paquete lo ha preparado todo por si mismo, y eso es normal”. Y si, Go realmente lo permite. Pero aqui aparece enseguida una diferencia clave del estilo Go: en Go no gusta la magia oculta de arranque, porque empeora la previsibilidad.

Imagina dos mundos:

  • El mundo “como en un libro de OO”: ves Class.doSomething(), y en algun lugar “dentro” la clase se ha preparado sola, lo cual a menudo se acepta como norma.
  • El mundo Go: cuando ves import "pkg", quieres estar seguro de que importar significa “voy a usar los nombres de este paquete”, no “voy a lanzar logica oculta”.

init() precisamente rompe esa expectativa: el simple hecho de importar puede cambiar el comportamiento del programa. Y esto empieza a ser un problema cuando el proyecto crece un poco.

init() como static constructor: semejanza y peligro

Si mantenemos la analogia, init() es algo asi como un “static init”, pero con un amplificador de efectos secundarios:

  • se ejecuta automaticamente,
  • se ejecuta antes de main,
  • se ejecuta en el momento de inicializar el paquete (y el paquete puede importarse “simplemente porque se ha arrastrado una dependencia”).

Y por eso los desarrolladores de Go lo tratan con cautela: si haces trabajo importante en init(), estas creando un escenario de arranque oculto que no se ve desde main.

4. Errores tipicos al trabajar con init() y efectos secundarios

Error n. 1: meter en init() la “logica principal de arranque”.
A veces los principiantes (y tambien desarrolladores experimentados cansados, seamos sinceros) empiezan a arrastrar a init() de todo: imprimir un saludo, configurar variables, leer datos e incluso mini algoritmos. Al final main() se convierte en un “conector” vacio, y entender que pasa al arrancar solo es posible excavando todo el paquete. Mucho mas fiable es mantener la secuencia importante de arranque en main() y llamar a funciones de inicializacion explicitas.

Error n. 2: intentar manejar errores en init() como en una funcion normal.
init() no devuelve error, asi que la gente o bien ignora el error (y obtiene unos raros ceros y cadenas vacias), o monta un panic en cualquier sitio. Si la inicializacion puede fallar, mejor crear initApp() (T, error) y tratar err con el habitual if err != nil { ... }, para que el programa termine de forma clara y predecible.

Error n. 3: depender del orden de init() en distintos archivos de un mismo paquete.
Aunque “a mi me funciona”, eso no significa que sea un buen contrato. El orden de inicializacion entre archivos no es algo sobre lo que convenga construir el sentido del programa. Si necesitas un orden, hazlo explicito: llama a initA(), luego a initB() desde un unico lugar, y entonces el orden sera exactamente el que has escrito.

Error n. 4: usar blank import sin entender que efecto exacto quieres.
import _ "..." parece pequeno, pero puede activar un mecanismo grande y oculto. Si no puedes explicar en dos frases que aporta exactamente ese import, significa que aun no deberias escribirlo. Para tareas de aprendizaje y aplicaciones sencillas, casi siempre bastan los imports normales y la inicializacion explicita.

Error n. 5: guardar estado mutable a nivel de paquete porque “es comodo para init”.
Con frecuencia init() arrastra variables globales: “en init() lleno var cfg”. Luego cfg se modifica desde distintas funciones, y el programa se parece a una pelicula de misterio: “quien cambio cfg.Timeout y por que ahora es 0?”. Mejor mantener el estado local y pasarlo mediante parametros y resultados de funciones: asi ves tu mismo el flujo de datos, en vez de perseguirlo con una linterna.

1
Tarea
Go SELF, nivel 10, lección 4
Bloqueada
Orden de ejecución
Orden de ejecución
1
Tarea
Go SELF, nivel 10, lección 4
Bloqueada
Prefijo en init
Prefijo en init
1
Tarea
Go SELF, nivel 10, lección 4
Bloqueada
Inicialización explícita
Inicialización explícita
1
Tarea
Go SELF, nivel 10, lección 4
Bloqueada
Corchetes desde archivos
Corchetes desde archivos
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION