Información sobre init en Go

Introducción

En Go, la función init() predeterminada establece una porción de código que debe ejecutarse antes que cualquier otra parte de su paquete. Este código se ejecutará tan pronto como se importe el paquete y puede usarse cuando necesite que su aplicación se inicie en un estado específico; por ejemplo, cuando requiera que la aplicación se inicie con una configuración o un conjunto de recursos específicos. También se utiliza al importar un efecto secundario, una técnica que se utiliza para establecer el estado de un programa al importar un paquete específico. Esto se suele utilizar para registrar un paquete con otro a fin de garantizar que el programa considere el código correcto para la tarea.

Si bien init() es una herramienta útil, a veces puede dificultar la lectura del código, dado que una instancia init() difícil de encontrar afectará en gran medida el orden en el que se ejecuta el código. Debido a esto, es importante que los desarrolladores que comienzan a usar Go comprendan las facetas de esta función, para poder asegurarse de utilizar init() de forma legible al escribir código.

A través de este tutorial, aprenderá a usar init() para configurar e inicializar variables de paquetes específicas, cálculos por única vez y registros de un paquete para su uso con otro.

Requisitos previos

Para algunos de los ejemplos que se incluyen en este artículo, necesitará lo siguiente:

  • Un espacio de trabajo de Go configurado conforme a Cómo instalar Go y configurar un entorno de programación local. En este tutorial, se usará la siguiente estructura de archivos:
. ├── bin │ └── src     └── github.com         └── gopherguides 

Declarar init()

Siempre que declare una función init(), Go la cargará antes que a cualquier otra cosa de ese paquete. Para demostrarlo, en esta sección se explicará la manera de definir una función init() y se mostrarán los efectos sobre la forma en que se ejecuta el paquete.

Primero, tomemos lo siguiente como ejemplo de código sin la función init():

main.go

package main  import "fmt"  var weekday string  func main() {     fmt.Printf("Today is %s", weekday) } 

En este programa, declaramos una variable global llamada weekday. De forma predeterminada, el valor de weekday es una cadena vacía.

Ejecutaremos este código:

  • go run main.go

Debido a que el valor de weekday está vacío, al ejecutar el programa, obtendremos el siguiente resultado:

OutputToday is 

Podemos completar la variable en blanco introduciendo una función init() que inicialice el valor de weekday en el día actual. Añada las siguientes líneas resaltadas a main.go:

main.go

package main  import (     "fmt"     "time" )  var weekday string  func init() {     weekday = time.Now().Weekday().String() }  func main() {     fmt.Printf("Today is %s", weekday) } 

En este código, importamos y usamos el paquete time para obtener el día actual de la semana (Now(). Weekday(). String()) y, luego, utilizamos init() para inicializar weekday con ese valor.

Ahora, cuando ejecutemos el programa, imprimirá el día actual de la semana:

OutputToday is Monday 

Aunque esto ilustra la forma en que init() funciona, es mucho más común usar init() al importar un paquete. Esto puede ser útil cuando necesita realizar tareas de configuración específicas en un paquete antes de que se utilice. Para demostrarlo, crearemos un programa que requerirá una inicialización específica a fin de que el paquete funcione como se indica.

Inicializar paquetes en la importación

Primero, escribiremos código para que se seleccione e imprima un animal al azar de un segmento, pero no usaremos init() en nuestro programa inicial. Esto indicará mejor el problema que tenemos y la manera en que init() lo resolverá.

Desde su directorio src/github.com/gopherguides/, cree una carpeta llamada creatrue con el siguiente comando:

  • mkdir creature

Dentro de la carpeta creature, cree un archivo llamado creature:

  • nano creature/creature.go

En este archivo, añada el siguiente contenido:

creature.go

package creature  import (     "math/rand" )  var creatures = []string{"shark", "jellyfish", "squid", "octopus", "dolphin"}  func Random() string {     i := rand.Intn(len(creatures))     return creatures[i] } 

Este archivo define una variable llamada creatures que tiene un conjunto de animales marinos que se inicializan como valores. También tiene una función Random exportada que mostrará un valor al azar de la variable creatures.

Guarde y cierre este archivo.

A continuación, crearemos un paquete cmd que usaremos para escribir nuestra función main() e invocar el paquete creature.

En el mismo nivel de archivo desde el que creamos la carpeta creature, cree una carpeta cmd con el siguiente comando:

  • mkdir cmd

Dentro de la carpeta cmd, cree un archivo llamado main.go:

  • nano cmd/main.go

Añada el siguiente contenido al archivo:

cmd/main.go

package main  import (     "fmt"      "github.com/gopherguides/creature" )  func main() {     fmt.Println(creature.Random())     fmt.Println(creature.Random())     fmt.Println(creature.Random())     fmt.Println(creature.Random()) } 

Aquí, importamos el paquete creature y luego, en la función main(), usamos la función creature.Random() para obtener un animal al azar e imprimirlo cuatro veces.

Guarde y cierre main.go.

Ahora, tenemos todo nuestro programa escrito. Sin embargo, para poder ejecutar este programa, también debemos crear algunos archivos de configuración a fin de que nuestro código funcione correctamente. Go utiliza Go Modules para configurar las dependencias de paquetes e importar recursos. Estos módulos son archivos de configuración que se disponen en su directorio de paquetes e indican al compilador el punto desde el cual se deben importar los paquetes. Si bien en este artículo no obtendrá información sobre los módulos, podemos escribir algunas líneas de configuración para que este ejemplo funcione a nivel local.

En el directorio cmd, cree un archivo llamado go.mod:

  • nano cmd/go.mod

Una vez que el archivo esté abierto, disponga el siguiente contenido:

cmd/go.mod

module github.com/gopherguides/cmd  replace github.com/gopherguides/creature => ../creature 

La primera línea de este archivo indica al compilador que el paquete cmd que creamos es, de hecho, github.com/gopherguides/cmd. La segunda línea indica al compilador que github.com/gopherguides/creature se encuentra a nivel local en el disco, en el directorio ../creature.

Guarde y cierre el archivo. A continuación, cree un archivo go.mod en el directorio creature:

  • nano creature/go.mod

Añada la siguiente línea de código al archivo:

creature/go.mod

 module github.com/gopherguides/creature 

Esto indica al compilador que el paquete creature que creamos, en realidad, es el paquete github.com/gopherguides/creature. Sin esto, el paquete cmd no tendría registro del punto desde el cual debería importar este paquete.

Guarde y cierre el archivo.

Ahora, debería contar con esta estructura de directorios y distribución de archivos:

├── cmd │   ├── go.mod │   └── main.go └── creature     ├── go.mod     └── creature.go 

Ahora que completamos toda la configuración, podemos ejecutar el programa main con el siguiente comando:

  • go run cmd/main.go

Esto proporcionará lo siguiente:

Outputjellyfish squid squid dolphin 

Cuando ejecutamos este programa, recibimos cuatro valores y los imprimimos. Si ejecutamos el programa varias veces, observaremos que siempre obtenemos el mismo resultado, en vez de un resultado al azar como se espera. Esto se debe a que el paquete rand crea números pseudoaleatorios que generarán de forma sistemática el mismo resultado para un único estado inicial. Para lograr un número más aleatorio, podemos propagar el paquete o establecer un origen cambiante para que el estado inicial sea diferente cada vez que ejecutemos el programa. En Go, es habitual usar la hora actual para propagar el paquete rand.

Dado que queremos que el paquete creature maneje la funcionalidad aleatoria, abra este archivo:

  • nano creature/creature.go

Añada las siguientes líneas al archivo creature.go:

creature/creature.go

package creature  import (     "math/rand"     "time" )  var creatures = []string{"shark", "jellyfish", "squid", "octopus", "dolphin"}  func Random() string {     rand.Seed(time.Now().UnixNano())     i := rand.Intn(len(creatures))     return creatures[i] } 

En este código, importamos el paquete time y usamos Seed() para propagar la hora actual. Guarde el archivo y ciérrelo.

Ahora, cuando ejecutemos el programa, obtendremos un resultado aleatorio:

  • go run cmd/main.go
Outputjellyfish octopus shark jellyfish 

Si continúa ejecutando el programa una y otra vez, seguirá obteniendo resultados aleatorios. Sin embargo, esta todavía no es una implementación ideal de nuestro código, porque cada vez qye se invoca creature.Random() también se vuelve a propagar el paquete rand invocando rand.Seed(time.Now(). UnixNano()) de nuevo. La repetición de la propagación aumentará la probabilidad de realizar la propagación con el mismo valor inicial si el reloj interno no se ha modificado, lo cual posiblemente generará repeticiones del patrón aleatorio o aumentará el tiempo de procesamiento de la CPU al hacer que el programa espere el cambio del reloj.

Para solucionar esto, podemos usar una función init(). Actualizaremos el archivo creature.go:

  • nano creature/creature.go

Añada las siguientes líneas de código:

creature/creature.go

package creature  import (     "math/rand"     "time" )  var creatures = []string{"shark", "jellyfish", "squid", "octopus", "dolphin"}  func init() {     rand.Seed(time.Now().UnixNano()) }  func Random() string {     i := rand.Intn(len(creatures))     return creatures[i] } 

La adición de la función init() indica al compilador que, al importar el paquete creature, debe ejecutar la función init() una vez con una sola propagación para la generación de un número al azar. Esto garantiza que no ejecutemos código más de lo necesario. Ahora, si ejecutamos el programa, continuaremos obteniendo resultados aleatorios:

  • go run cmd/main.go
Outputdolphin squid dolphin octopus 

En esta sección, vimos que el uso de init() puede garantizar que se realicen inicializaciones o cálculos adecuados antes de usar un paquete. A continuación, veremos la forma de usar varias instrucciones init() en un paquete.

Varias instancias de init()

A diferencia de la función main(), que solo se puede declarar una vez, la función init() puede declararse varias veces en un paquete. Sin embargo, varias funciones init() pueden hacer que resulte difícil determinar la que tiene prioridad sobre las demás. En esta sección, se mostrará la manera de mantener el control sobre varias instrucciones init().

En la mayoría de los casos, las funciones init() se ejecutarán en el orden en el que las encuentre. Tomemos el siguiente código como ejemplo:

main.go

package main  import "fmt"  func init() {     fmt.Println("First init") }  func init() {     fmt.Println("Second init") }  func init() {     fmt.Println("Third init") }  func init() {     fmt.Println("Fourth init") }  func main() {} 

Si ejecutamos el programa con el siguiente comando:

  • go run main.go

Obtendremos el siguiente resultado:

OutputFirst init Second init Third init Fourth init 

Observe que cada función init() se ejecuta en el orden en el que el compilador la encuentra. Sin embargo, es posible que no siempre sea tan fácil determinar el orden de invocación de las funciones init().

Veamos una estructura de paquetes más complicada en la que tenemos varios archivos, cada uno con su propia función init() declarada en su interior. Para ilustrar esto, crearemos un programa que comparta una variable llamada message y la imprima.

Elimine los directorios creature y cmd y su contenido de la sección anterior, y sustitúyalos por los directorios y la estructura de archivos que se indican a continuación:

├── cmd │   ├── a.go │   ├── b.go │   └── main.go └── message     └── message.go 

Ahora, agregaremos el contenido de cada archivo. En a.go, añada las siguientes líneas:

cmd/a.go

package main  import (     "fmt"      "github.com/gopherguides/message" )  func init() {     fmt.Println("a ->", message.Message) } 

Este archivo contiene una función init() única que imprime el valor de message.Message del paquete message.

A continuación, añada el siguiente contenido a b.go:

cmd/b.go

package main  import (     "fmt"      "github.com/gopherguides/message" )  func init() {     message.Message = "Hello"     fmt.Println("b ->", message.Message) } 

En b.go, hay una función init() única que fija el valor de message.Message en Hello y lo imprime.

A continuación, cree main.go para que tenga el siguiente aspecto:

cmd/main.go

package main  func main() {} 

Este archivo no hace más que simplemente ofrecer un punto de entrada para que se ejecute el programa.

Por último, cree su archivo message.go de la siguiente manera:

message/message.go

package message  var Message string 

Nuestro paquete messages declara la variable Message exportada.

Para iniciar el programa, ejecute el siguiente comando desde el directorio cmd:

  • go run *.go

Debido a que hay varios archivos de Go en la carpeta cmd que conforman el paquete main, debemos indicar al compilador que todos los archivos .go de la carpeta cmd deben compilarse. Usar *.go indica al compilador que cargue todos los archivos que terminan en .go de la carpeta cmd. Si emitiéramos el comando go main.go, el programa no se compilaría porque no detectaría el código en los archivos a.go y b.go.

Esto generará el siguiente resultado:

Outputa -> b -> Hello 

De acuerdo con la especificación del lenguaje de Go para Inicialización de paquetes, cuando se encuentran varios archivos en un paquete se procesan en orden alfabético. Es por esto que la primera vez que imprimimos message.Message desde a.go, el valor estaba vacío. El valor no se inicializó hasta que se ejecutó la función init() desde b.go.

Si cambiáramos el nombre del archivo de a.go a c.go, obtendríamos un resultado diferente:

Outputb -> Hello a -> Hello 

Ahora, el compilador encuentra b.go primero y, por lo tanto, el valor de message.Message ya está inicializado con Hello cuando se encuentra la función init() en c.go.

Este comportamiento podría generar un problema en su código. En el ámbito del desarrollo de software, es habitual cambiar los nombres de los archivos y, por la forma en que se procesa init(), hacer este cambio puede modificar el orden en el que se procesa init(). Esto podría tener el efecto no deseado de cambiar el resultado de su programa. Para garantizar un comportamiento de inicialización reproducible, se recomienda que los sistemas de compilación presenten a un compilador varios archivos pertenecientes al mismo paquete en orden de nombre de archivo léxico. Una forma de garantizar que se carguen todas las funciones init() en orden es declararlas en su totalidad en un único archivo. Esto impedirá que el orden cambie, incluso si se cambian los nombres de los archivos.

Además de garantizar que el orden de sus funciones init() no cambie, también debe intentar evitar la administración del estado en su paquete usando variables globales; es decir, variables accesibles desde cualquier punto del paquete. En el programa anterior, la variable message.Message estaba disponible para todo el paquete y mantuvo el estado del programa. Debido a este acceso, las instrucciones init() pudieron cambiar la variable y desestabilizar la previsibilidad de su programa. Para evitar esto, intente trabajar con variables en espacios controlados que tengan el menor nivel de acceso posible y, al mismo tiempo, permitan que el programa funcione.

Vimos que puede tener varias declaraciones init() en un único paquete. Sin embargo, esto puede crear efectos no deseados y hacer que su programa sea difícil de leer o predecir. Evitar tener varias instrucciones init() o mantenerlas en un único archivo garantizará que el comportamiento de su programa no cambie al mover los archivos o modificar su nombre.

A continuación, veremos cómo se utiliza init() para la importación con efectos secundarios.

Usar init() para efectos secundarios

En Go, a veces es conveniente importar un paquete no por su contenido, sino por los efectos secundarios que se producen al importarlo. Esto suele significar que hay una instrucción init() en el código importado que se ejecuta antes de cualquier otro código, lo cual permite que el desarrollador manipule el estado en el que se inicia el programa. La técnica se denomina importación para efectos secundarios.

Un caso de uso común para realizar una importación para obtener efectos secundarios tiene que ver con registrar la funcionalidad en su código, lo cual permite que un paquete registre la parte del código que necesita usar su programa. En el paquete image, por ejemplo, la función image.Decode debe registrar el formato de la imagen que intenta decodificar (jpg, png y gif, entre otros) para poder ejecutarse. Puede realizar esto importando, primero, un programa específico que tenga un efecto secundario de instrucción init().

Supongamos que intenta usar image.Decode en un archivo .png con el siguiente fragmento de código:

Sample Decoding Snippet

. . . func decode(reader io.Reader) image.Rectangle {     m, _, err := image.Decode(reader)     if err != nil {         log.Fatal(err)     }     return m.Bounds() } . . . 

De todos modos, se compilará un programa con este código, pero cada vez que intentemos decodificar una imagen png, obtendremos un error.

Para solucionar esto, primero, debemos registrar un formato de imagen para image.Decode. Afortunadamente, el paquete image/png contiene la siguiente instrucción init():

image/png/reader.go

func init() {     image.RegisterFormat("png", pngHeader, Decode, DecodeConfig) } 

Por lo tanto, si importamos image/png en nuestro fragmento de decodificación, la función image.RegisterFormat() en image/png se ejecutará antes que cualquier parte de nuestro código:

Sample Decoding Snippet

. . . import _ "image/png" . . .  func decode(reader io.Reader) image.Rectangle {     m, _, err := image.Decode(reader)     if err != nil {         log.Fatal(err)     }     return m.Bounds() } 

Con esto, se establecerá el estado y se registrará que necesitamos la versión png de image.Decode(). El registro se realizará como efecto secundario de la importación de image/png.

Posiblemente, haya observado el identificador en blanco (_) antes de "image/png". Esto es necesario porque Go no le permite importar paquetes que no se utilicen en todo el programa. Cuando se incluye el identificador en blanco, el valor de la importación se descarta para que solo se produzca el efecto secundario de la importación. Esto significa que, aunque nunca invoquemos el paquete image/png en nuestro código, podemos importarlo para obtener el efecto secundario.

Es importante conocer el momento en que se debe importar un paquete debido a su efecto secundario. Sin el registro adecuado, es probable que su programa se compile y no funcione correctamente cuando se ejecute. Los paquetes de la biblioteca estándar declararán la necesidad de este tipo de importación en su documentación. Si escribe un paquete que requiere una importación para obtener efectos secundarios, también debe asegurarse de que la instrucción init() que esté usando se documente para que los usuarios que importen su paquete puedan utilizarlo correctamente.

Conclusión

A través de este tutorial, aprendió que la función init() se carga antes que el resto del código de su paquete y que puede realizar tareas específicas para un paquete, como inicializar un estado deseado. También aprendió que el orden en el que el compilador ejecuta varias instrucciones init() depende del orden en el que carga los archivos de origen. Si desea obtener más información sobre init(), consulte la documentación oficial de Golang o lea los comentarios acerca de la función en la comunidad de Go.

Puede leer más sobre funciones en nuestro artículo Cómo definir e invocar funciones en Go o consultar toda la serie Cómo programar en Go.