Du point de vue de la structure logique du programme, le package est l'unité de base de l'encapsulation logique du programme Go. Chaque package peut être compris comme une unité de base « autonome » bien encapsulée qui expose des interfaces limitées vers l'extérieur. Un programme Go est composé d'un ensemble de packages.
Dans l'unité de base du package Go, il y a des constantes, des variables au niveau du package, des fonctions, des types et méthodes de type, des interfaces, etc. Nous devons nous assurer que ces éléments à l'intérieur du package sont dans un état initial raisonnable et efficace avant d'être utilisés, notamment variables au niveau du package. . Dans le langage Go, nous réalisons généralement ce travail via la fonction init du package.
Comprendre la fonction init
Il existe deux fonctions spéciales dans le langage Go : l'une est la fonction principale du package principal, qui est la fonction d'entrée de tous les programmes exécutables Go ; l'autre est la fonction init du package.
La fonction init est une fonction sans paramètres ni valeur de retour :
func init() { ... } Si un package définit une fonction init, le runtime Go se chargera d'appeler sa fonction init lorsque le package est initialisé. Nous ne pouvons pas appeler explicitement init dans le programme Go , sinon une erreur sera signalée lors de la compilation :
package main
import "fmt"
func init() {
fmt.Println("init invoked")
}
func main() {
init()
}
résultat de l'opération :
undefined: init
Un package Go peut avoir plusieurs fonctions d'initialisation , et plusieurs fonctions d'initialisation peuvent être définies dans chaque fichier source Go qui constitue le package Go. Lors de l'initialisation d'un package Go, le runtime Go appellera la fonction init du package une par une dans un certain ordre. Le runtime Go n'appellera pas la fonction init simultanément.Il attendra qu'une fonction init termine son exécution et revienne avant d'exécuter la fonction init suivante,
et chaque fonction init ne sera exécutée qu'une seule fois pendant tout le cycle de vie du programme Go . Par conséquent, la fonction init est extrêmement adaptée pour initialiser certaines données au niveau du package et vérifier l'état initial.
Quel est l’ordre d’exécution de plusieurs fonctions d’initialisation distribuées dans plusieurs fichiers au sein d’un package ? De manière générale, la fonction init du fichier source transmise au compilateur Go est exécutée en premier, et plusieurs fonctions init du même fichier source sont exécutées séquentiellement dans l'ordre de déclaration. Mais la convention du langage Go nous dit : ne vous fiez pas à l'ordre d'exécution de la fonction init
Séquence d'initialisation du programme
Pourquoi la fonction init est-elle adaptée à l'initialisation des données au niveau du package et à la vérification de l'état initial ? En plus du fait que la fonction init est exécutée séquentiellement et une seule fois, la séquence d'initialisation du programme Go fournit également à la fonction init les conditions préalables pour effectuer le travail.
Les programmes Go sont composés d'un ensemble de packages, et l'initialisation du programme est l'initialisation de ces packages. Chaque package Go aura son propre package de dépendances. Chaque package contient également des constantes, des variables, des fonctions d'initialisation, etc. (le package main a la fonction main). Quel est l'ordre d'initialisation de ces éléments pendant le processus d'initialisation du programme ? Utilisons l’image ci-dessous pour illustrer.
● Le package principal dépend directement des packages pkg1 et pkg4 ;
● Lorsque Go s'exécute, il initialisera d'abord le premier package dépendant pkg1 du package principal en fonction de l'ordre dans lequel les packages sont importés ;
● Le runtime Go suit le principe de « profondeur d'abord » et voit que pkg1 dépend de pkg2, donc le runtime Go initialise pkg2 ;
● pkg2 dépend de pkg3 et Go s'exécute pour initialiser pkg3 ;
● pkg3 n'a pas de packages dépendants, donc le runtime Go est initialisé dans le package pkg3 dans l'ordre constantes → variables → fonction init ;
● Une fois pkg3 initialisé, le runtime Go reviendra à pkg2 et initialisera pkg2, puis reviendra à pkg1 et initialisera pkg1 ;
● Après avoir appelé la fonction init de pkg1, le runtime Go termine l'initialisation du premier package dépendant pkg1 du package principal ;
● Le runtime Go initialisera ensuite le deuxième package dépendant pkg4 du package principal ;
● Le processus d'initialisation de pkg4 est similaire à celui de pkg1. Il initialise également d'abord son package dépendant pkg5, puis s'initialise lui-même ;
● Une fois pkg4 initialisé lors de l'exécution de Go, tous les packages dépendants du package principal sont initialisés, puis le package principal lui-même est initialisé ;
● Dans le package principal, le runtime Go sera initialisé dans l'ordre constantes → variables → fonction init. Après avoir terminé ces initialisations, la fonction principale, la fonction d'entrée du programme, sera officiellement entrée.
À ce stade, nous savons que la condition préalable pour que la fonction init soit adaptée à l'initialisation des données au niveau du package et à la vérification de l'état initial est que l'ordre d'exécution de la fonction init soit classé après les variables au niveau du package du package dans lequel elle se trouve. situé.
Utilisez la fonction init pour vérifier l'état initial des variables au niveau du package
La fonction init est comme le seul « inspecteur de qualité » avant la mise en service effective du package Go. Elle est chargée de vérifier l'état initial des données au niveau du package (principalement des variables au niveau du package) à l'intérieur du package et exposées à l'extérieur. . Dans le runtime Go et la bibliothèque standard, nous pouvons trouver de nombreux exemples d'initialisation vérifiant l'état initial des variables au niveau du package.
- Réinitialiser les valeurs des variables au niveau du package
func init() {
CommandLine.Usage = commandLineUsage
}
CommandLine est une variable exportée au niveau du package du package flag. C'est également une variable qui représente la ligne de commande par défaut (si vous ne créez pas un nouveau FlagSet). Nous pouvons voir à partir de son expression d'initialisation :
var CommandLine = NewFlagSet(os.Args[0], ExitOnError)
Le champ Usage de CommandLine est initialisé avec la valeur de méthode defaultUsage de l’instance FlagSet (c’est-à-dire CommandLine) dans la fonction NewFlagSet. Si cela reste ainsi, les utilisateurs externes utilisant la ligne de commande par défaut de Flag ne pourront pas personnaliser la sortie d'utilisation. Ainsi, dans la fonction init du package flag, le champ Usage de ComandLine est défini sur commandLineUsage, une fonction non exportée dans le package, qui utilise directement
une autre variable de package exportée Utilisation du package flag. De cette manière, CommandLine est associé à la variable du package Usage via la fonction init. Une fois que l'utilisateur a attribué l'utilisation personnalisée à Utilisation, cela équivaut à modifier l'utilisation de la variable CommandLine.
L'exemple suivant provient du package contextuel de la bibliothèque standard :
// closedchan是一个可重用的处于关闭状态的channel
var closedchan = make(chan struct{
})
func init() {
close(closedchan)
}
Le package de contexte a besoin d'un canal fermé et réutilisable dans la méthode Cancel de CancelCtx, de sorte que le package de contexte définit une variable closechan au niveau du package non exportée et l'initialise. Cependant, le closechan initialisé ne répond pas aux exigences du package de contexte.Le seul endroit où son statut peut être vérifié et corrigé est la fonction init du package de contexte, donc le code ci-dessus ferme closechan dans la fonction init.
Initialiser les variables au niveau du package pour garantir leur disponibilité ultérieure
Le processus d'initialisation de certaines variables au niveau du package est relativement complexe et les expressions d'initialisation simples ne peuvent pas répondre aux exigences, et la fonction init est très appropriée pour terminer ce travail. La fonction init du package d'expressions rationnelles de la bibliothèque standard est responsable de l'initialisation du tableau d'octets spécial interne. Ce tableau d'octets spécial est utilisé par la fonction spéciale du package pour déterminer si un certain caractère doit être échappé :
var specialBytes [16]byte
func special(b byte) bool {
return b < utf8.RuneSelf && specialBytes[b%16]&(1<<(b/16)) != 0
}
func init() {
for _, b := range []byte(`\.+*?()|[]{
}^$`) {
specialBytes[b%16] |= 1 << (b / 16)
}
}
Le package net de la bibliothèque standard inverse le tri de la variable non exportée au niveau du package rfc6724policyTable dans la fonction init :
func init() {
sort.Sort(sort.Reverse(byMaskLength(rfc6724policyTable)))
}
Le package http de la bibliothèque standard attribue certaines variables de commutateur au niveau du package en fonction de la valeur de la variable d'environnement GODEBUG dans la fonction init :
var (
http2VerboseLogs bool
http2logFrameWrites bool
http2logFrameReads bool
http2inTests bool
)
func init() {
e := os.Getenv("GODEBUG")
if strings.Contains(e, "http2debug=1") {
http2VerboseLogs = true
}
if strings.Contains(e, "http2debug=2") {
http2VerboseLogs = true
http2logFrameWrites = true
http2logFrameReads = true
}
}
- Mode d'enregistrement dans la fonction init
Voici un exemple de code utilisant le package lib/pq [1] pour accéder à une base de données PostgreSQL :
import (
"database/sql"
_ "github.com/lib/pq"
)
func main() {
db, err := sql.Open("postgres", "user=pqgotest dbname=pqgotest sslmode=verify-full")
if err != nil {
log.Fatal(err)
}
age := 21
rows, err := db.Query("SELECT name FROM users WHERE age = $1", age)
...
}
Pour les Gophers qui débutent sur Go, il s'agit d'un morceau de code magique, car après avoir importé le package lib/pq avec un alias vide, la fonction principale ne semble utiliser aucune variable, fonction ou méthode de pq. Le secret de ce code réside dans la fonction init du package pq :
// github.com/lib/pq/conn.go
...
func init() {
sql.Register("postgres", &Driver{
})
}
...
L'effet secondaire de l'importation de lib/pq avec un alias vide est que le runtime Go utilisera lib/pq comme package dépendant du package principal et initialisera le package pq, afin que la fonction init du package pq puisse être exécutée. Nous voyons que dans la fonction init du package pq, le package pq enregistre son propre pilote SQL (pilote) dans le package sql. De cette façon, tant que le code de la couche application passe au nom du pilote (ici, postgres) lors de l'ouverture de la base de données, le handle d'instance de base de données renvoyé via la fonction sql.Open correspond à l'implémentation correspondante du pilote pq.
Ce mode d'enregistrement de votre propre implémentation dans la fonction init réduit l'exposition directe du package Go au monde extérieur, en particulier l'exposition des variables au niveau du package, et évite les modifications externes de l'état du package via des variables au niveau du package. Du point de vue de base de données/sql, ce mode d'enregistrement est essentiellement l'implémentation d'un modèle de conception d'usine. La fonction sql.Open est la méthode d'usine dans ce mode. Elle produit différents types d'instances de base de données en fonction du nom du pilote transmis par le à l'extérieur.
Ce mode d'enregistrement est également largement utilisé dans d'autres packages de la bibliothèque standard. Par exemple, le package d'images de la bibliothèque standard est utilisé pour obtenir la largeur et la hauteur des images dans différents formats.
package main
import (
"fmt"
"image"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"os"
)
func main() {
// 支持PNG、JPEG、GIF
width, height, err := imageSize(os.Args[1])
if err != nil {
fmt.Println("get image size error:", err)
return
}
fmt.Printf("image size: [%d, %d]\n", width, height)
}
func imageSize(imageFile string) (int, int, error) {
f, _ := os.Open(imageFile)
defer f.Close()
img, _, err := image.Decode(f)
if err != nil {
return 0, 0, err
}
b := img.Bounds()
return b.Max.X, b.Max.Y, nil
}
Ce programme prend en charge les images dans trois formats : PNG, JPEG et GIF. Ceci est obtenu précisément parce que les packages image/png, image/jpeg et image/gif s'enregistrent eux-mêmes dans la liste des formats pris en charge par l'image dans leurs fonctions d'initialisation respectives.
// $GOROOT/src/image/png/reader.go
func init() {
image.RegisterFormat("png", pngHeader, Decode, DecodeConfig)
}
// $GOROOT/src/image/jpeg/reader.go
func init() {
image.RegisterFormat("jpeg", "\xff\xd8", Decode, DecodeConfig)
}
// $GOROOT/src/image/gif/reader.go
func init() {
image.RegisterFormat("gif", "GIF8?a", Decode, DecodeConfig)
}
4. Comment gérer l'échec de la vérification dans la fonction init
La fonction init est une fonction sans paramètres ni valeur de retour. Son objectif principal est de garantir que l'état initial du package dans lequel il se trouve est valide avant qu'il ne soit officiellement utilisé. Une fois que la fonction init rencontre un échec ou une erreur lors de la vérification de l'état initial des données du package (bien que cela se produise rarement), cela signifie que le "contrôle de qualité" du package a un feu rouge. Si le package est "usine", il ne fera que provoquer davantage de changements, avec un impact sérieux.
Donc, échouer rapidement est la meilleure option dans ce cas. Nous recommandons généralement d'appeler
directement panic ou d'enregistrer des journaux d'exceptions via des fonctions telles que log.Fatal, puis de laisser le programme se terminer rapidement.