"Si un trabajador quiere hacer bien su trabajo, primero debe afilar sus herramientas." - Confucio, "Las Analectas de Confucio. Lu Linggong"
Página delantera > Programación > Aplazamiento de Golang: Aplazamiento de código abierto, asignado por pila y asignado por montón

Aplazamiento de Golang: Aplazamiento de código abierto, asignado por pila y asignado por montón

Publicado el 2024-08-19
Navegar:505

Este es un extracto de la publicación; la publicación completa está disponible aquí: Golang Defer: From Basic To Trap.

La declaración de aplazar es probablemente una de las primeras cosas que encontramos bastante interesantes cuando empezamos a aprender Go, ¿verdad?

Pero hay mucho más que hace tropezar a muchas personas, y hay muchos aspectos fascinantes que a menudo no tocamos cuando lo usamos.

Golang Defer: Heap-allocated, Stack-allocated, Open-coded Defer

Asignación de montón, asignación de pila, aplazamiento de código abierto

Por ejemplo, la declaración de aplazamiento en realidad tiene 3 tipos (a partir de Go 1.22, aunque eso podría cambiar más adelante): aplazamiento de código abierto, aplazamiento de asignación de montón y asignación de pila. Cada uno tiene un rendimiento diferente y diferentes escenarios donde se utilizan mejor, lo cual es bueno saber si desea optimizar el rendimiento.

En esta discusión, cubriremos todo, desde lo básico hasta el uso más avanzado, e incluso profundizaremos un poco, solo un poquito, en algunos de los detalles internos.

¿Qué es aplazar?

Echemos un vistazo rápido al aplazamiento antes de profundizar demasiado.

En Go, aplazar es una palabra clave que se utiliza para retrasar la ejecución de una función hasta que finalice la función circundante.

func main() {
  defer fmt.Println("hello")
  fmt.Println("world")
}

// Output:
// world
// hello

En este fragmento, la instrucción defer programa fmt.Println("hello") para que se ejecute al final de la función principal. Por lo tanto, fmt.Println("world") se llama inmediatamente y "world" se imprime primero. Después de eso, debido a que usamos aplazar, "hola" se imprime como último paso antes de que finalice el proceso principal.

Es como configurar una tarea para que se ejecute más tarde, justo antes de que salga la función. Esto es realmente útil para acciones de limpieza, como cerrar una conexión a una base de datos, liberar un mutex o cerrar un archivo:

func doSomething() error {
  f, err := os.Open("phuong-secrets.txt")
  if err != nil {
    return err
  }
  defer f.Close()

  // ...
}

El código anterior es un buen ejemplo para mostrar cómo funciona el aplazamiento, pero también es una mala forma de utilizarlo. Hablaremos de eso en la siguiente sección.

"Está bien, bien, pero ¿por qué no poner f.Close() al final?"

Hay un par de buenas razones para esto:

  • Colocamos la acción de cerrar cerca de la de apertura, para que sea más fácil seguir la lógica y evitar olvidar cerrar el archivo. No quiero desplazarme hacia abajo en una función para verificar si el archivo está cerrado o no; me distrae de la lógica principal.
  • La función diferida se llama cuando la función regresa, incluso si ocurre un pánico (error de tiempo de ejecución).

Cuando ocurre un pánico, la pila se desenrolla y las funciones diferidas se ejecutan en un orden específico, que cubriremos en la siguiente sección.

Los aplazamientos están apilados

Cuando utilizas varias declaraciones diferidas en una función, se ejecutan en un orden de "pila", lo que significa que la última función diferida se ejecuta primero.

func main() {
  defer fmt.Println(1)
  defer fmt.Println(2)
  defer fmt.Println(3)
}

// Output:
// 3
// 2
// 1

Cada vez que llamas a una declaración de aplazamiento, agregas esa función a la parte superior de la lista vinculada de la rutina actual, como esta:

Golang Defer: Heap-allocated, Stack-allocated, Open-coded Defer

Cadena de aplazamiento de rutinas

Y cuando la función regresa, recorre la lista vinculada y ejecuta cada una en el orden que se muestra en la imagen de arriba.

Pero recuerde, no ejecuta todos los aplazamientos en la lista enlazada de goroutine, solo ejecuta el aplazamiento en la función devuelta, porque nuestra lista enlazada de aplazamientos podría contener muchos aplazamientos de muchas funciones diferentes.

func B() {
  defer fmt.Println(1)
  defer fmt.Println(2)
  A()
}

func A() {
  defer fmt.Println(3)
  defer fmt.Println(4)
}

Por lo tanto, solo se ejecutan las funciones diferidas en la función actual (o marco de pila actual).

Golang Defer: Heap-allocated, Stack-allocated, Open-coded Defer

Cadena de aplazamiento de rutinas

Pero hay un caso típico en el que todas las funciones diferidas en la rutina actual se rastrean y ejecutan, y es entonces cuando ocurre el pánico.

Aplazar, entrar en pánico y recuperarse

Además de los errores en tiempo de compilación, tenemos un montón de errores en tiempo de ejecución: dividir por cero (solo números enteros), fuera de límites, desreferenciar un puntero nulo, etc. Estos errores hacen que la aplicación entre en pánico.

El pánico es una forma de detener la ejecución de la rutina actual, desenrollar la pila y ejecutar las funciones diferidas en la rutina actual, lo que provoca que nuestra aplicación falle.

Para manejar errores inesperados y evitar que la aplicación falle, puede usar la función de recuperación dentro de una función diferida para recuperar el control de una gorutina que entra en pánico.

func main() {
  defer func() {
    if r := recover(); r != nil {
      fmt.Println("Recovered:", r)
    }
  }()

  panic("This is a panic")
}

// Output:
// Recovered: This is a panic

Por lo general, la gente pone un error en el pánico y lo detecta con recovery(..), pero puede ser cualquier cosa: una cadena, un int, etc.

En el ejemplo anterior, dentro de la función diferida es el único lugar donde puede usar la recuperación. Déjame explicarte esto un poco más.

Hay un par de errores que podríamos enumerar aquí. He visto al menos tres fragmentos como este en código real.

La primera es usar la recuperación directamente como función diferida:

func main() {
  defer recover()

  panic("This is a panic")
}

El código anterior todavía genera pánico, y esto se debe al diseño del tiempo de ejecución de Go.

La función de recuperación está destinada a detectar el pánico, pero debe llamarse dentro de una función diferida para que funcione correctamente.

Detrás de escena, nuestra llamada de recuperación es en realidad runtime.gorecover, y verifica que la llamada de recuperación se realice en el contexto correcto, específicamente desde la función diferida correcta que estaba activa cuando ocurrió el pánico.

"¿Eso significa que no podemos usar la recuperación en una función dentro de una función diferida, como esta?"

func myRecover() {
  if r := recover(); r != nil {
    fmt.Println("Recovered:", r)
  }
}

func main() {
  defer func() {
    myRecover()
    // ...
  }()

  panic("This is a panic")
}

Exactamente, el código anterior no funcionará como cabría esperar. Esto se debe a que la recuperación no se llama directamente desde una función diferida sino desde una función anidada.

Ahora, otro error es tratar de detectar el pánico con una rutina diferente:

func main() {
  defer func() {
    if r := recover(); r != nil {
      fmt.Println("Recovered:", r)
    }
  }()

  go panic("This is a panic")

  time.Sleep(1 * time.Second) // Wait for the goroutine to finish
}

Tiene sentido, ¿verdad? Ya sabemos que las cadenas diferidas pertenecen a una rutina específica. Sería difícil si una gorutina pudiera intervenir en otra para controlar el pánico, ya que cada gorutina tiene su propia pila.

Desafortunadamente, la única salida en este caso es bloquear la aplicación si no manejamos el pánico en esa rutina.

Los argumentos diferidos, incluido el receptor, se evalúan inmediatamente

Me he encontrado con este problema antes, donde los datos antiguos se enviaban al sistema de análisis y era difícil descubrir por qué.

Esto es lo que quiero decir:

func pushAnalytic(a int) {
  fmt.Println(a)
}

func main() {
  a := 10
  defer pushAnalytic(a)

  a = 20
}

¿Cuál crees que será el resultado? Son 10, no 20.

Eso se debe a que cuando usas la declaración aplazar, toma los valores en ese momento. Esto se llama "captura por valor". Por lo tanto, el valor de a que se envía a pushAnalytic se establece en 10 cuando se programa el aplazamiento, aunque a cambie más adelante.

Hay dos formas de solucionar este problema.

...

La publicación completa está disponible aquí: Golang Defer: From Basic To Trap.

Declaración de liberación Este artículo se reproduce en: https://dev.to/func25/golang-defer-heap-allocated-stack-allocated-open-coded-defer-1h9o?1 Si hay alguna infracción, comuníquese con [email protected] para borrarlo
Último tutorial Más>

Descargo de responsabilidad: Todos los recursos proporcionados provienen en parte de Internet. Si existe alguna infracción de sus derechos de autor u otros derechos e intereses, explique los motivos detallados y proporcione pruebas de los derechos de autor o derechos e intereses y luego envíelos al correo electrónico: [email protected]. Lo manejaremos por usted lo antes posible.

Copyright© 2022 湘ICP备2022001581号-3