Comprendre init in Go

introduction

Dans Go, la fonction prédéfinieinit() déclenche un morceau de code à exécuter avant toute autre partie de votre package. Ce code s'exécutera dès que lespackage is imported et peut être utilisé lorsque vous avez besoin de votre application pour s'initialiser dans un état spécifique, par exemple lorsque vous avez une configuration spécifique ou un ensemble de ressources avec lequel votre application doit démarrer. Il est également utilisé lorsqueimporting a side effect, une technique utilisée pour définir l'état d'un programme en important un package spécifique. Ceci est souvent utilisé pourregister un package avec un autre pour s'assurer que le programme considère le code correct pour la tâche.

Bien queinit() soit un outil utile, il peut parfois rendre le code difficile à lire, car une instance deinit() difficile à trouver affectera considérablement l'ordre dans lequel le code est exécuté. Pour cette raison, il est important que les développeurs novices dans Go comprennent les facettes de cette fonction, afin de s'assurer d'utiliserinit() de manière lisible lors de l'écriture du code.

Dans ce didacticiel, vous apprendrez commentinit() est utilisé pour la configuration et l’initialisation de variables de package spécifiques, les calculs ponctuels et l’enregistrement d’un package à utiliser avec un autre package.

Conditions préalables

Pour certains exemples de cet article, vous aurez besoin des éléments suivants:

.
├── bin
│
└── src
    └── github.com
        └── gopherguides

Déclaration deinit()

Chaque fois que vous déclarez une fonctioninit(), Go la chargera et l'exécutera avant tout autre élément de ce package. Pour illustrer cela, cette section explique comment définir une fonctioninit() et montre les effets sur le fonctionnement du package.

Prenons d'abord ce qui suit comme exemple de code sans la fonctioninit():

main.go

package main

import "fmt"

var weekday string

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

Dans ce programme, nous avons déclaré unvariable global appeléweekday. Par défaut, la valeur deweekday est une chaîne vide.

Lançons ce code:

go run main.go

Comme la valeur deweekday est vide, lorsque nous exécutons le programme, nous obtiendrons la sortie suivante:

OutputToday is

Nous pouvons remplir la variable vide en introduisant une fonctioninit() qui initialise la valeur deweekday au jour courant. Ajoutez les lignes en surbrillance suivantes à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)
}

Dans ce code, nous avons importé et utilisé le packagetime pour obtenir le jour actuel de la semaine (Now().Weekday().String()), puis utiliséinit() pour initialiserweekday avec cette valeur.

Maintenant, lorsque nous exécuterons le programme, il affichera le jour de la semaine en cours:

OutputToday is Monday

Bien que cela illustre le fonctionnement deinit(), un cas d'utilisation beaucoup plus typique pourinit() est de l'utiliser lors de l'importation d'un package. Cela peut être utile lorsque vous devez effectuer des tâches d’installation spécifiques dans un package avant de l’utiliser. Pour illustrer cela, créons un programme nécessitant une initialisation spécifique pour que le package fonctionne comme prévu.

Initialisation des packages à l'importation

Tout d'abord, nous allons écrire un code qui sélectionne une créature aléatoire à partir d'unslice et l'imprime. Cependant, nous n'utiliserons pasinit() dans notre programme initial. Cela montrera mieux le problème que nous avons et commentinit() résoudra notre problème.

À partir de votre répertoiresrc/github.com/gopherguides/, créez un dossier appelécreature avec la commande suivante:

mkdir creature

Dans le dossiercreature, créez un fichier appelécreature.go:

nano creature/creature.go

Dans ce fichier, ajoutez le contenu suivant:

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]
}

Ce fichier définit une variable appeléecreatures qui a un ensemble de créatures marines initialisées en tant que valeurs. Il a également une fonctionexportedRandom qui retournera une valeur aléatoire à partir de la variablecreatures.

Enregistrez et quittez ce fichier.

Ensuite, créons un packagecmd que nous utiliserons pour écrire notre fonctionmain() et appeler le packagecreature.

Au même niveau de fichier à partir duquel nous avons créé le dossiercreature, créez un dossiercmd avec la commande suivante:

mkdir cmd

Dans le dossiercmd, créez un fichier appelémain.go:

nano cmd/main.go

Ajoutez le contenu suivant au fichier:

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())
}

Ici, nous avons importé le packagecreature, puis dans la fonctionmain(), nous avons utilisé la fonctioncreature.Random() pour récupérer une créature aléatoire et l'imprimer quatre fois.

Enregistrez et quittezmain.go.

Nous avons maintenant tout notre programme écrit. Cependant, avant de pouvoir exécuter ce programme, nous devons également créer quelques fichiers de configuration pour que notre code fonctionne correctement. Go utiliseGo Modules pour configurer les dépendances de package pour l'importation de ressources. Ces modules sont des fichiers de configuration placés dans votre répertoire de packages qui indiquent au compilateur d'où importer les packages. Bien que l’apprentissage des modules dépasse le cadre de cet article, nous ne pouvons écrire que quelques lignes de configuration pour que cet exemple fonctionne localement.

Dans le répertoirecmd, créez un fichier nommégo.mod:

nano cmd/go.mod

Une fois le fichier ouvert, placez-le dans le contenu suivant:

cmd/go.mod

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

La première ligne de ce fichier indique au compilateur que le packagecmd que nous avons créé est en faitgithub.com/gopherguides/cmd. La deuxième ligne indique au compilateur quegithub.com/gopherguides/creature peut être trouvé localement sur le disque dans le répertoire../creature.

Enregistrez et fermez le fichier. Ensuite, créez un fichiergo.mod dans le répertoirecreature:

nano creature/go.mod

Ajoutez la ligne de code suivante au fichier:

creature/go.mod

 module github.com/gopherguides/creature

Cela indique au compilateur que le packagecreature que nous avons créé est en fait le packagegithub.com/gopherguides/creature. Sans cela, le packagecmd ne saurait pas d'où importer ce package.

Enregistrez et quittez le fichier.

Vous devriez maintenant avoir la structure de répertoire et la disposition de fichier suivants:

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

Maintenant que toute la configuration est terminée, nous pouvons exécuter le programmemain avec la commande suivante:

go run cmd/main.go

Cela donnera:

Outputjellyfish
squid
squid
dolphin

Lorsque nous avons exécuté ce programme, nous avons reçu quatre valeurs et les avons imprimées. Si nous exécutons le programme plusieurs fois, nous remarquerons que nousalways obtenons la même sortie, plutôt qu'un résultat aléatoire comme prévu. Cela est dû au fait que le packagerand crée des nombres pseudo-aléatoires qui généreront systématiquement la même sortie pour un seul état initial. Pour obtenir un nombre plus aléatoire, nous pouvonsseed le paquet, ou définir une source changeante de sorte que l'état initial soit différent à chaque fois que nous exécutons le programme. Dans Go, il est courant d'utiliser l'heure actuelle pour amorcer le packagerand.

Puisque nous voulons que le packagecreature gère la fonctionnalité aléatoire, ouvrez ce fichier:

nano creature/creature.go

Ajoutez les lignes en surbrillance suivantes au fichiercreature.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]
}

Dans ce code, nous avons importé le packagetime et utiliséSeed() pour amorcer l'heure actuelle. Enregistrez et quittez le fichier.

Maintenant, lorsque nous lancerons le programme, nous obtiendrons un résultat aléatoire:

go run cmd/main.go
Outputjellyfish
octopus
shark
jellyfish

Si vous continuez à exécuter le programme encore et encore, vous continuerez à obtenir des résultats aléatoires. Cependant, ce n'est pas encore une implémentation idéale de notre code, car à chaque fois quecreature.Random() est appelé, il réinitialisera également le packagerand en appelant à nouveaurand.Seed(time.Now().UnixNano()). Le réensemencement augmentera les chances de réensemencer avec la même valeur initiale si l'horloge interne n'a pas changé, ce qui entraînera de possibles répétitions du motif aléatoire ou augmentera le temps de traitement de la CPU en laissant votre programme attendre que l'horloge change.

Pour résoudre ce problème, nous pouvons utiliser une fonctioninit(). Mettons à jour le fichiercreature.go:

nano creature/creature.go

Ajoutez les lignes de code suivantes:

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]
}

L'ajout de la fonctioninit() indique au compilateur que lorsque le packagecreature est importé, il doit exécuter la fonctioninit() une fois, en fournissant une seule graine pour la génération de nombres aléatoires. Cela garantit que nous n'exécutons pas de code plus que nécessaire. Maintenant, si nous exécutons le programme, nous continuerons à obtenir des résultats aléatoires:

go run cmd/main.go
Outputdolphin
squid
dolphin
octopus

Dans cette section, nous avons vu comment l'utilisation deinit() peut garantir que les calculs ou initialisations appropriés sont effectués avant l'utilisation du package. Ensuite, nous verrons comment utiliser plusieurs instructionsinit() dans un package.

Instances multiples deinit()

Contrairement à la fonctionmain() qui ne peut être déclarée qu'une seule fois, la fonctioninit() peut être déclarée plusieurs fois dans un package. Cependant, plusieurs instructionsinit()+`s can make it difficult to know which one has priority over the others. In this section, we will show how to maintain control over multiple `+init().

Dans la plupart des cas, les fonctionsinit() s'exécuteront dans l'ordre dans lequel vous les rencontrez. Prenons comme exemple le code suivant:

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 nous exécutons le programme avec la commande suivante:

go run main.go

Nous recevrons le résultat suivant:

OutputFirst init
Second init
Third init
Fourth init

Notez que chaqueinit() est exécuté dans l'ordre dans lequel le compilateur le rencontre. Cependant, il n'est pas toujours aussi facile de déterminer l'ordre dans lequel la fonctioninit() sera appelée.

Examinons une structure de package plus compliquée dans laquelle nous avons plusieurs fichiers contenant chacun leur propre fonctioninit() déclarée. Pour illustrer cela, nous allons créer un programme qui partage une variable appeléemessage et l'imprime.

Supprimez les répertoirescreature etcmd et leur contenu de la section précédente, et remplacez-les par les répertoires et la structure de fichiers suivants:

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

Ajoutons maintenant le contenu de chaque fichier. Dansa.go, ajoutez les lignes suivantes:

cmd/a.go

package main

import (
    "fmt"

    "github.com/gopherguides/message"
)

func init() {
    fmt.Println("a ->", message.Message)
}

Ce fichier contient une seule fonctioninit() qui imprime la valeur demessage.Message du packagemessage.

Ensuite, ajoutez le contenu suivant àb.go:

cmd/b.go

package main

import (
    "fmt"

    "github.com/gopherguides/message"
)

func init() {
    message.Message = "Hello"
    fmt.Println("b ->", message.Message)
}

Dansb.go, nous avons une seule fonctioninit() qui définit la valeur demessage.Message surHello et l’imprime.

Ensuite, créezmain.go pour ressembler à ce qui suit:

cmd/main.go

package main

func main() {}

Ce fichier ne fait rien, mais fournit un point d'entrée pour que le programme s'exécute.

Enfin, créez votre fichiermessage.go comme suit:

message/message.go

package message

var Message string

Notre packagemessage déclare la variableMessage exportée.

Pour exécuter le programme, exécutez la commande suivante à partir du répertoirecmd:

go run *.go

Comme nous avons plusieurs fichiers Go dans le dossiercmd qui composent le packagemain, nous devons dire au compilateur que tous les fichiers.go du dossiercmd doivent être compilé. L'utilisation de*.go indique au compilateur de charger tous les fichiers du dossiercmd qui se terminent par.go. Si nous émettions la commande dego run main.go, le programme échouerait à se compiler car il ne verrait pas le code dans les fichiersa.go etb.go.

Cela donnera la sortie suivante:

Outputa ->
b -> Hello

Selon la spécification de langue Go pourPackage Initialization, lorsque plusieurs fichiers sont rencontrés dans un package, ils sont traités par ordre alphabétique. Pour cette raison, la première fois que nous avons imprimémessage.Message à partir dea.go, la valeur était vide. La valeur n'a pas été initialisée tant que la fonctioninit() deb.go n'a pas été exécutée.

Si nous devions changer le nom de fichier dea.go enc.go, nous obtiendrions un résultat différent:

Outputb -> Hello
a -> Hello

Maintenant, le compilateur rencontre d'abordb.go, et en tant que tel, la valeur demessage.Message est déjà initialisée avecHello lorsque la fonctioninit() dansc.go est rencontrée.

Ce comportement pourrait créer un problème possible dans votre code. Il est courant dans le développement logiciel de changer les noms de fichiers, et en raison de la façon dontinit() est traité, changer les noms de fichiers peut changer l'ordre dans lequelinit() est traité. Cela pourrait avoir l’effet indésirable de modifier la sortie de votre programme. Pour assurer un comportement d'initialisation reproductible, les systèmes de génération sont encouragés à présenter plusieurs fichiers appartenant au même paquet dans l'ordre du nom de fichier lexical à un compilateur. Une façon de s'assurer que toutes les fonctionsinit() sont chargées dans l'ordre est de les déclarer toutes dans un seul fichier. Cela empêchera l'ordre de changer même si les noms de fichiers sont modifiés.

En plus de vous assurer que l'ordre de vos fonctionsinit() ne change pas, vous devriez également essayer d'éviter de gérer l'état dans votre package en utilisantglobal variables, c'est-à-dire des variables accessibles de n'importe où dans le package. Dans le programme précédent, la variablemessage.Message était disponible pour l'ensemble du package et conservait l'état du programme. Grâce à cet accès, les instructionsinit() ont pu modifier la variable et déstabiliser la prévisibilité de votre programme. Pour éviter cela, essayez de travailler avec des variables dans des espaces contrôlés ayant le moins d'accès possible tout en permettant au programme de fonctionner.

Nous avons vu que vous pouvez avoir plusieurs déclarationsinit() dans un même package. Cependant, cela pourrait créer des effets indésirables et rendre votre programme difficile à lire ou à prédire. Éviter plusieurs instructionsinit() ou les conserver toutes dans un seul fichier garantira que le comportement de votre programme ne change pas lorsque des fichiers sont déplacés ou que les noms sont modifiés.

Ensuite, nous examinerons commentinit() est utilisé pour importer avec des effets secondaires.

Utilisation deinit() pour les effets secondaires

Dans Go, il est parfois souhaitable d’importer un package non pour son contenu, mais pour les effets secondaires qui se produisent lors de l’importation du package. Cela signifie souvent qu'il y a une instructioninit() dans le code importé qui s'exécute avant tout autre code, permettant au développeur de manipuler l'état dans lequel son programme démarre. Cette technique s'appelleimporting for a side effect.

Un cas d'utilisation courant pour l'importation d'effets secondaires est la fonctionnalitéregister de votre code, qui permet à un package de savoir quelle partie du code votre programme doit utiliser. Dans lesimage package, par exemple, la fonctionimage.Decode a besoin de savoir quel format d'image elle tente de décoder (jpg,png,gif, etc. .) avant de pouvoir s'exécuter. Vous pouvez accomplir cela en important d'abord un programme spécifique qui a un effet secondaire de l'instructioninit().

Supposons que vous essayez d'utiliserimage.Decode sur un fichier.png avec l'extrait de code suivant:

Extrait de décodage

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

Un programme avec ce code sera toujours compilé, mais chaque fois que nous essayons de décoder une imagepng, nous obtiendrons une erreur.

Pour résoudre ce problème, nous devons d'abord enregistrer un format d'image pourimage.Decode. Heureusement, le packageimage/png contient l'instructioninit() suivante:

image/png/reader.go

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

Par conséquent, si nous importonsimage/png dans notre extrait de code de décodage, la fonctionimage.RegisterFormat() dansimage/png s'exécutera avant l'un de nos codes:

Extrait de décodage

. . .
import _ "image/png"
. . .

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

Cela définira l'état et le registre dont nous avons besoin pour la versionpng deimage.Decode(). Cet enregistrement se produira comme un effet secondaire de l'importation deimage/png.

Vous avez peut-être remarqué lesblank identifier (_) avant"image/png". Cela est nécessaire car Go ne vous permet pas d'importer des packages qui ne sont pas utilisés tout au long du programme. En incluant l'identificateur vide, la valeur de l'importation elle-même est ignorée, de sorte que seul l'effet secondaire de l'importation se manifeste. Cela signifie que, même si nous n'appelons jamais le packageimage/png dans notre code, nous pouvons toujours l'importer pour l'effet secondaire.

Il est important de savoir quand vous devez importer un package pour son effet secondaire. Sans l'enregistrement approprié, il est probable que votre programme compilera, mais ne fonctionnera pas correctement lorsqu'il sera exécuté. Les packages de la bibliothèque standard déclareront la nécessité de ce type d'importation dans leur documentation. Si vous écrivez un package qui nécessite une importation pour effet secondaire, vous devez également vous assurer que l'instructioninit() que vous utilisez est documentée afin que les utilisateurs qui importent votre package puissent l'utiliser correctement.

Conclusion

Dans ce didacticiel, nous avons appris que la fonctioninit() se charge avant que le reste du code de votre package ne soit chargé, et qu'elle peut effectuer des tâches spécifiques pour un package comme l'initialisation d'un état souhaité. Nous avons également appris que l'ordre dans lequel le compilateur exécute plusieurs instructionsinit() dépend de l'ordre dans lequel le compilateur charge les fichiers source. Si vous souhaitez en savoir plus surinit(), consultez lesGolang documentation officiels ou lisez lesthe discussion in the Go community about the function.

Vous pouvez en savoir plus sur les fonctions avec notre article surHow To Define and Call Functions in Go ou explorerthe entire How To Code in Go series.