"Si un ouvrier veut bien faire son travail, il doit d'abord affûter ses outils." - Confucius, "Les Entretiens de Confucius. Lu Linggong"
Page de garde > La programmation > Golang Defer : différence allouée au tas, allouée à la pile, à code ouvert

Golang Defer : différence allouée au tas, allouée à la pile, à code ouvert

Publié le 2024-08-19
Parcourir:421

Ceci est un extrait du message ; l'article complet est disponible ici : Golang Defer : From Basic To Trap.

L'instruction defer est probablement l'une des premières choses que nous trouvons assez intéressantes lorsque nous commençons à apprendre le Go, n'est-ce pas ?

Mais il y a bien plus encore qui font trébucher de nombreuses personnes, et il y a de nombreux aspects fascinants que nous n'abordons souvent pas lorsque nous l'utilisons.

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

Allocation par tas, allocation par pile, report à code ouvert

Par exemple, l'instruction defer a en fait 3 types (à partir de Go 1.22, bien que cela puisse changer plus tard) : defer à code ouvert, defer alloué au tas et alloué à la pile. Chacun a des performances différentes et différents scénarios dans lesquels il est préférable de les utiliser, ce qui est bon à savoir si vous souhaitez optimiser les performances.

Dans cette discussion, nous allons tout aborder, des bases à l'utilisation plus avancée, et nous approfondirons même un peu, juste un petit peu, certains détails internes.

Qu’est-ce que le report ?

Jetons un rapide coup d'œil au report avant de plonger trop profondément.

Dans Go, defer est un mot-clé utilisé pour retarder l'exécution d'une fonction jusqu'à ce que la fonction environnante se termine.

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

// Output:
// world
// hello

Dans cet extrait, l'instruction defer planifie l'exécution de fmt.Println("hello") à la toute fin de la fonction principale. Ainsi, fmt.Println("world") est appelé immédiatement et "world" est imprimé en premier. Après cela, parce que nous avons utilisé le report, "hello" est imprimé comme dernière étape avant la fin principale.

C'est comme configurer une tâche à exécuter plus tard, juste avant la fin de la fonction. Ceci est très utile pour les actions de nettoyage, comme fermer une connexion à une base de données, libérer un mutex ou fermer un fichier :

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

  // ...
}

Le code ci-dessus est un bon exemple pour montrer comment fonctionne le report, mais c'est aussi une mauvaise façon d'utiliser le report. Nous y reviendrons dans la section suivante.

"D'accord, bien, mais pourquoi ne pas mettre f.Close() à la fin ?"

Il y a plusieurs bonnes raisons à cela :

  • Nous plaçons l'action de fermeture à proximité de l'action d'ouverture, il est donc plus facile de suivre la logique et d'éviter d'oublier de fermer le fichier. Je ne veux pas faire défiler une fonction pour vérifier si le fichier est fermé ou non ; cela me distrait de la logique principale.
  • La fonction différée est appelée lorsque la fonction revient, même si une panique (erreur d'exécution) se produit.

Lorsqu'une panique se produit, la pile est déroulée et les fonctions différées sont exécutées dans un ordre spécifique, que nous aborderons dans la section suivante.

Les reports sont empilés

Lorsque vous utilisez plusieurs instructions différées dans une fonction, elles sont exécutées dans un ordre « pile », ce qui signifie que la dernière fonction différée est exécutée en premier.

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

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

Chaque fois que vous appelez une instruction defer, vous ajoutez cette fonction en haut de la liste chaînée de la goroutine actuelle, comme ceci :

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

Chaîne différée Goroutine

Et lorsque la fonction revient, elle parcourt la liste chaînée et exécute chacune d'elles dans l'ordre indiqué dans l'image ci-dessus.

Mais rappelez-vous, il n'exécute pas tous les defers dans la liste chaînée de goroutine, il exécute uniquement le defer dans la fonction renvoyée, car notre liste chaînée defer pourrait contenir de nombreux différés de nombreuses fonctions différentes.

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

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

Ainsi, seules les fonctions différées de la fonction actuelle (ou du cadre de pile actuel) sont exécutées.

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

Chaîne différée Goroutine

Mais il existe un cas typique où toutes les fonctions différées de la goroutine actuelle sont tracées et exécutées, et c'est à ce moment-là qu'une panique se produit.

Différer, paniquer et récupérer

Outre les erreurs de compilation, nous avons un tas d'erreurs d'exécution : division par zéro (entier uniquement), hors limites, déréférencement d'un pointeur nul, etc. Ces erreurs provoquent la panique de l'application.

La panique est un moyen d'arrêter l'exécution de la goroutine actuelle, de dérouler la pile et d'exécuter les fonctions différées dans la goroutine actuelle, provoquant le crash de notre application.

Pour gérer les erreurs inattendues et éviter que l'application ne plante, vous pouvez utiliser la fonction de récupération dans une fonction différée pour reprendre le contrôle d'une goroutine paniquée.

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

  panic("This is a panic")
}

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

Habituellement, les gens mettent une erreur dans la panique et l'attrapent avec recovery(..), mais cela peut être n'importe quoi : une chaîne, un int, etc.

Dans l'exemple ci-dessus, à l'intérieur de la fonction différée se trouve le seul endroit où vous pouvez utiliser la récupération. Laissez-moi vous expliquer cela un peu plus.

Il y a quelques erreurs que nous pourrions énumérer ici. J'ai vu au moins trois extraits comme celui-ci dans du vrai code.

La première consiste à utiliser la récupération directement comme fonction différée :

func main() {
  defer recover()

  panic("This is a panic")
}

Le code ci-dessus panique toujours, et cela est dû à la conception du runtime Go.

La fonction de récupération est destinée à détecter une panique, mais elle doit être appelée dans une fonction différée pour fonctionner correctement.

En coulisses, notre appel à récupérer est en fait le runtime.gorecover, et il vérifie que l'appel de récupération se produit dans le bon contexte, en particulier à partir de la fonction différée correcte qui était active lorsque la panique s'est produite.

"Cela signifie-t-il que nous ne pouvons pas utiliser la récupération dans une fonction à l'intérieur d'une fonction différée, comme celle-ci ?"

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

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

  panic("This is a panic")
}

Exactement, le code ci-dessus ne fonctionnera pas comme vous pourriez vous y attendre. En effet, recovery n'est pas appelé directement depuis une fonction différée mais depuis une fonction imbriquée.

Maintenant, une autre erreur consiste à essayer de surprendre la panique d'un autre goroutine :

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
}

C'est logique, non ? Nous savons déjà que les chaînes de report appartiennent à une goroutine spécifique. Il serait difficile si une goroutine pouvait intervenir dans une autre pour gérer la panique puisque chaque goroutine a sa propre pile.

Malheureusement, la seule issue dans ce cas est de planter l'application si nous ne gérons pas la panique dans cette goroutine.

Les arguments différés, y compris le récepteur, sont immédiatement évalués

J'ai déjà rencontré ce problème, où d'anciennes données étaient transmises au système d'analyse, et il était difficile de comprendre pourquoi.

Voici ce que je veux dire :

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

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

  a = 20
}

À votre avis, quel sera le résultat ? Il est 10 heures, pas 20 heures.

C'est parce que lorsque vous utilisez l'instruction defer, elle récupère les valeurs immédiatement. C'est ce qu'on appelle la « capture par valeur ». Ainsi, la valeur de a qui est envoyée à pushAnalytic est définie sur 10 lorsque le report est planifié, même si a change plus tard.

Il existe deux façons de résoudre ce problème.

...

L'article complet est disponible ici : Golang Defer : From Basic To Trap.

Déclaration de sortie Cet article est reproduit sur : https://dev.to/func25/golang-defer-heap-allocated-stack-allocated-open-coded-defer-1h9o?1 En cas de violation, veuillez contacter [email protected] pour le supprimer
Dernier tutoriel Plus>

Clause de non-responsabilité: Toutes les ressources fournies proviennent en partie d'Internet. En cas de violation de vos droits d'auteur ou d'autres droits et intérêts, veuillez expliquer les raisons détaillées et fournir une preuve du droit d'auteur ou des droits et intérêts, puis l'envoyer à l'adresse e-mail : [email protected]. Nous nous en occuperons pour vous dans les plus brefs délais.

Copyright© 2022 湘ICP备2022001581号-3