Go Basics 15: comprobar el estado inicial de las variables a nivel de paquete en la función init()

Desde la perspectiva de la estructura lógica del programa, el paquete es la unidad básica de la encapsulación lógica del programa Go. Cada paquete puede entenderse como una unidad básica "autónoma" y bien encapsulada que expone interfaces limitadas al exterior. Un programa Go se compone de un conjunto de paquetes.

En la unidad básica del paquete Go, hay constantes, variables a nivel de paquete, funciones, tipos y métodos de tipo, interfaces, etc. Debemos asegurarnos de que estos elementos dentro del paquete estén en un estado inicial razonable y efectivo antes de ser utilizados, especialmente variables a nivel de paquete . En el lenguaje Go, generalmente completamos este trabajo mediante la función init del paquete.

Comprender la función de inicio

Hay dos funciones especiales en el lenguaje Go: una es la función principal en el paquete principal, que es la función de entrada de todos los programas ejecutables de Go; la otra es la función de inicio del paquete.

La función init es una función sin parámetros ni valor de retorno:
func init() { ... } Si un paquete define una función init, el tiempo de ejecución de Go será responsable de llamar a su función init cuando se inicialice el paquete. No podemos llamar explícitamente a init en el programa Go ; de lo contrario, se informará un error durante la compilación:


package main
import "fmt"
func init() {
    
    
fmt.Println("init invoked")
}
func main() {
    
    
init()
}

resultado de la operación:

undefined: init

Un paquete Go puede tener múltiples funciones de inicio y se pueden definir múltiples funciones de inicio en cada archivo fuente de Go que conforma el paquete Go. Al inicializar un paquete Go, el tiempo de ejecución de Go llamará a la función init del paquete una por una en un orden determinado. El tiempo de ejecución de Go no llamará a la función init simultáneamente , esperará a que una función init complete su ejecución y regrese antes de ejecutar la siguiente función init,
y cada función init solo se ejecutará una vez durante todo el ciclo de vida del programa Go . Por lo tanto, la función init es extremadamente adecuada para inicializar algunos datos a nivel de paquete y verificar el estado inicial.

¿Cuál es el orden de ejecución de múltiples funciones de inicio distribuidas en múltiples archivos dentro de un paquete? En términos generales, la función init en el archivo fuente pasado al compilador Go primero se ejecuta primero, y varias funciones init en el mismo archivo fuente se ejecutan secuencialmente en el orden de declaración. Pero la convención del lenguaje Go nos dice: no confíe en el orden de ejecución de la función init

Secuencia de inicialización del programa

¿Por qué la función init es adecuada para inicializar datos a nivel de paquete y verificar el estado inicial? Además de que la función init se ejecuta secuencialmente y solo una vez, la secuencia de inicialización del programa Go también proporciona a la función init los requisitos previos para realizar el trabajo.

Los programas Go se componen de un conjunto de paquetes y la inicialización del programa es la inicialización de estos paquetes. Cada paquete Go tendrá su propio paquete de dependencia. Cada paquete también contiene constantes, variables, funciones de inicio, etc. (el paquete principal tiene la función principal). ¿Cuál es el orden de inicialización de estos elementos durante el proceso de inicialización del programa? Usemos la siguiente imagen para ilustrar.

Insertar descripción de la imagen aquí

● El paquete principal depende directamente de los paquetes pkg1 y pkg4;

● Cuando se ejecuta Go, primero inicializará el primer paquete dependiente pkg1 del paquete principal según el orden en que se importan los paquetes;

● El tiempo de ejecución de Go sigue el principio de "primero la profundidad" y ve que pkg1 depende de pkg2, por lo que el tiempo de ejecución de Go inicializa pkg2;

● pkg2 depende de pkg3 y Go se ejecuta para inicializar pkg3;

● pkg3 no tiene paquetes dependientes, por lo que el tiempo de ejecución de Go se inicializa en el paquete pkg3 en el orden de constantes → variables → función de inicio;

● Después de inicializar pkg3, el tiempo de ejecución de Go regresará a pkg2 e inicializará pkg2, y luego regresará a pkg1 e inicializará pkg1;

● Después de llamar a la función init de pkg1, el tiempo de ejecución de Go completa la inicialización del primer paquete dependiente pkg1 del paquete principal;

● El tiempo de ejecución de Go inicializará a continuación el segundo paquete dependiente pkg4 del paquete principal;

● El proceso de inicialización de pkg4 es similar al de pkg1. También inicializa primero su paquete dependiente pkg5 y luego se inicializa él mismo;

● Después de inicializar pkg4 durante el tiempo de ejecución de Go, se inicializan todos los paquetes dependientes del paquete principal y luego se inicializa el paquete principal;

● En el paquete principal, el tiempo de ejecución de Go se inicializará en el orden de constantes → variables → función de inicio. Después de completar estas inicializaciones, se ingresará oficialmente a la función principal, la función de entrada del programa.

En este punto, sabemos que el requisito previo para que la función init sea adecuada para inicializar datos a nivel de paquete y verificar el estado inicial es que el orden de ejecución de la función init esté clasificado después de las variables a nivel de paquete del paquete en el que se encuentra. situado.

Utilice la función init para comprobar el estado inicial de las variables a nivel de paquete

La función init es como el único "inspector de calidad" antes de que el paquete Go se ponga en uso. Es responsable de verificar el estado inicial de los datos a nivel de paquete (principalmente variables a nivel de paquete) dentro del paquete y expuestos al exterior. . En la biblioteca estándar y en tiempo de ejecución de Go, podemos encontrar muchos ejemplos de cómo init verifica el estado inicial de las variables a nivel de paquete.

  1. Restablecer valores de variables a nivel de paquete
func init() {
    
    
	CommandLine.Usage = commandLineUsage
}

CommandLine es una variable exportada a nivel de paquete del paquete de banderas. También es una variable que representa la línea de comando de forma predeterminada (si no crea un nuevo FlagSet). Podemos ver en su expresión de inicialización:

var CommandLine = NewFlagSet(os.Args[0], ExitOnError)

El campo Uso de CommandLine se inicializa con el valor del método defaultUsage de la instancia FlagSet (es decir, CommandLine) en la función NewFlagSet. Si permanece así, los usuarios externos que utilicen la línea de comandos predeterminada de Flag no podrán personalizar la salida de uso. Entonces, en la función de inicio del paquete de bandera, el campo Uso de ComandLine se establece en commandLineUsage, una función no exportada en el paquete, que usa directamente
otra variable de paquete exportada, Uso del paquete de bandera. De esta forma, CommandLine se asocia con la variable del paquete Usage a través de la función init. Después de que el usuario asigna el uso personalizado a Uso, equivale a cambiar el Uso de la variable CommandLine.

El siguiente ejemplo proviene del paquete contextual de la biblioteca estándar:

// closedchan是一个可重用的处于关闭状态的channel
var closedchan = make(chan struct{
    
    })
func init() {
    
    
	close(closedchan)
}

El paquete de contexto necesita un canal cerrado reutilizable en el método de cancelación de cancelCtx, por lo que el paquete de contexto define una variable closechan a nivel de paquete no exportada y la inicializa. Sin embargo, el cerradochan inicializado no cumple con los requisitos del paquete de contexto. El único lugar donde se puede verificar y corregir su estado es la función de inicio del paquete de contexto, por lo que el código anterior cierra el cerradochan en la función de inicio.

Inicialice las variables a nivel de paquete para garantizar su disponibilidad posterior.

El proceso de inicialización de algunas variables a nivel de paquete es relativamente complejo y las expresiones de inicialización simples no pueden cumplir con los requisitos, y la función init es muy adecuada para completar este trabajo. La función init del paquete regexp de la biblioteca estándar es responsable de inicializar la matriz de bytes especial interna. Esta matriz de bytes especial es utilizada por la función especial en el paquete para determinar si es necesario escapar de un determinado carácter:

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

El paquete net de la biblioteca estándar invierte la clasificación de la variable de nivel de paquete no exportada rfc6724policyTable en la función init:

func init() {
    
    
	sort.Sort(sort.Reverse(byMaskLength(rfc6724policyTable)))
}

El paquete http de la biblioteca estándar asigna algunas variables de cambio a nivel de paquete de acuerdo con el valor de la variable de entorno GODEBUG en la función 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
	}
}
  1. Modo de registro en función init

El siguiente es un ejemplo de código que utiliza el paquete lib/pq [1] para acceder a una base de datos 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)
...
}

Para los Gophers que son nuevos en Go, este es un fragmento de código mágico, porque después de importar el paquete lib/pq con un alias vacío, la función principal no parece usar ninguna variable, función o método de pq. El secreto de este código está en la función init del paquete pq:

// github.com/lib/pq/conn.go
...
func init() {
    
    
	sql.Register("postgres", &Driver{
    
    })
}
...

El efecto secundario de importar lib/pq con un alias vacío es que el tiempo de ejecución de Go usará lib/pq como un paquete dependiente del paquete principal e inicializará el paquete pq, de modo que se pueda ejecutar la función init del paquete pq. Vemos que en la función init del paquete pq, el paquete pq registra su propio controlador SQL (controlador) en el paquete sql. De esta manera, siempre que el código de la capa de aplicación pase el nombre del controlador (aquí, postgres) al abrir la base de datos, el identificador de instancia de la base de datos devuelto a través de la función sql.Open corresponde a la implementación correspondiente del controlador pq.

Este modo de registrar su propia implementación en la función init reduce la exposición directa del paquete Go al mundo exterior, especialmente la exposición de las variables a nivel de paquete, y evita cambios externos en el estado del paquete a través de variables a nivel de paquete. Desde la perspectiva de la base de datos/sql, este modo de registro es esencialmente la implementación de un patrón de diseño de fábrica. La función sql.Open es el método de fábrica en este modo. Produce diferentes tipos de instancias de base de datos según el nombre del controlador pasado desde el exterior.mango.

Este modo de registro también se usa ampliamente en otros paquetes de la biblioteca estándar, por ejemplo, el paquete de imágenes de la biblioteca estándar se usa para obtener el ancho y alto de imágenes en varios formatos.

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
}

Este programa soporta imágenes en tres formatos: PNG, JPEG y GIF, esto se logra precisamente porque los paquetes image/png, image/jpeg e image/gif se registran en la lista de formatos soportados de la imagen en sus respectivas funciones de inicio.

// $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. Cómo manejar la falla de verificación en la función init

La función init es una función sin parámetros ni valor de retorno, su objetivo principal es garantizar que el estado inicial del paquete en el que se encuentra sea válido antes de su uso oficial. Una vez que la función init encuentra una falla o error al verificar el estado inicial de los datos del paquete (aunque rara vez ocurre), significa que la "inspección de calidad" del paquete tiene una luz roja. Si el paquete es "de fábrica", solo causará más cambios para un impacto grave.

Entonces, fallar rápido es la mejor opción en este caso. Generalmente recomendamos llamar
a pánico directamente o registrar registros de excepciones mediante funciones como log.Fatal y luego dejar que el programa salga rápidamente.

Supongo que te gusta

Origin blog.csdn.net/hai411741962/article/details/132799918
Recomendado
Clasificación