Maison  >  Article  >  développement back-end  >  Refactor : GoroutineTracker avec utilisation inutile de Reflect

Refactor : GoroutineTracker avec utilisation inutile de Reflect

PHPz
PHPzoriginal
2024-07-17 04:26:29634parcourir

Refactor: GoroutineTracker with unnecessary usage of reflect

Aujourd'hui, j'ai rencontré ce code dans la base de code de mon entreprise (le code et les commentaires sont réécrits à des fins de démonstration et n'incluent aucun code propriétaire) :

type GoroutineTracker struct {
  wg sync.WaitGroup
  // ... some other fields
}
// Go starts a new goroutine and tracks it with some metrics.
func (g *GoroutineTracker) Go(ctx context.Context, name string, f any, args ...any) {
    fn := reflect.TypeOf(f)
    if fn.Kind() != reflect.Func { panic("must be function") }
    if fn.NumIn() != len(args) { panic("args does not match fn signature") }
    if fn.NumOut() > 0 { panic("output from fn is ignored") }

    g.wg.Add(1)
    id := g.startCaptureTime()
    go func() {
        defer func() {
            r := recover()
            // ... some panic handling code
            g.wg.Done()
            g.endCaptureTime(id)
        }()

        input := typez.MapFunc(args, func(arg any) reflect.Value {
            return reflect.ValueOf(arg)
        })
        _ = reflect.ValueOf(f).Call(input)
    }()
}
// Wait for all goroutines to finished.
func (g *GoroutineTracker) Wait() { g.wg.Wait() }

Le GoroutineTracker est utilisé pour suivre l'utilisation des goroutines dans la base de code, par exemple, le nombre de goroutines, le temps pris par chaque goroutine, etc. La méthode Go est utilisée pour démarrer une nouvelle goroutine et la suivre. La méthode Wait est utilisée pour attendre la fin de toutes les goroutines.

Exemple d'utilisation :

g := NewGoroutineTracker()
g.Go(ctx, "task1", doTask1, arg1, arg2)
g.Go(ctx, "task2", doTask2, arg3)
g.Wait()

Problème : l'utilisation de la réflexion est inutile et peut être évitée

Eh bien, ce code fonctionne, mais il utilise le package Reflect pour vérifier la signature de la fonction puis appeler la fonction. C'est totalement inutile, et nous pouvons l'éviter en changeant l'usage en :

g := NewGoroutineTracker()
g.Go(ctx, "task1", func() error {
    return doTask1(arg1, arg2)
})
g.Go(ctx, "task2", func() error {
    return doTask2(arg3)
})

Le nouveau code sera plus simple et présente de nombreux avantages :

  • Sécurité du type : Pas besoin de vérifier la signature de la fonction à l'aide de Reflect. Le compilateur le fera pour nous. Le code d'origine comporte des erreurs d'exécution potentielles si la signature de la fonction ne correspond pas aux arguments.
  • Gestion des erreurs : Nous pouvons renvoyer une erreur de la fonction et la gérer dans l'appelant. Le code d'origine ignore la sortie de la fonction.
  • Lisibilité : Le nouveau code est plus lisible et plus facile à comprendre. Nous pouvons voir la signature de la fonction et les arguments directement dans le code.

Une meilleure implémentation de GoroutineTracker

Voici le code refactorisé :

func (g *GoroutineTracker) Go(ctx context.Context, fn func() error) {
    g.wg.Add(1)
    id := g.startCaptureTime()
    go func() (err error) {
        defer func() {
            r := recover()
            // capture returned error and panic
            g.endCaptureTime(id, r, err)
            g.wg.Done()
        }()
        // just call the function, no reflect needed
        return fn()
    }()
}

Attendez que toutes les goroutines soient terminées avant de vous arrêter

Un autre cas d'utilisation de GoroutineTracker consiste à attendre la fin de toutes les goroutines avant d'arrêter l'application. On peut donc avoir 2 types d'attente :

  • Dans une fonction : En attente de la fin de toutes les goroutines locales.
  • Lors de l'arrêt de l'application : attente de la fin de toutes les goroutines démarrées par n'importe quel GoroutineTracker.

Nous pouvons l'implémenter en ajoutant un tracker global et en faisant en sorte que n'importe quel tracker enregistre sa fonction auprès du tracker global :

type GlobalTracker struct {
    wg sync.WaitGroup
    // ... some other fields
}
type GoroutineTracker struct {
    parent *GlobalTracker
    wg sync.WaitGroup
    // ... some other fields
}
func (g *GlobalTracker) New() *GoroutineTracker {
    return &GoroutineTracker{parent: g}
}
func (g *GoroutineTracker) Go(ctx context.Context, fn func() error) {
    g.wg.Add(1)            // use both parent and local wg
    g.parent.wg.Add(1)     //   to track the new goroutine
    id := g.startCaptureTime()
    go func() (err error) {
        defer func() {
            // ...
            g.endCaptureTime(id, r, err)
            g.wg.Done()
            g.parent.wg.Done()
        }()

        return fn()
    }()
}
func (g *GlobalTracker) WaitForAll() { g.wg.Wait() }
func (g *GoroutineTracker) Wait()    { g.wg.Wait() }

Et nous pouvons utiliser WaitForAll() pour attendre que toutes les goroutines soient terminées avant de fermer l'application :

type FooService {
    tracker *GlobalTracker
    // ... some other fields
}
func (s *FooService) DoSomething(ctx context.Context) {
    g := s.tracker.New()
    g.Go(ctx, func() error { return s.doTask1(arg1, arg2) })
    g.Go(ctx, func() error { return s.doTask2(arg3) })
    g.Wait()     // wait for local goroutines, this is optional
}

func main() {
    // some initialization, then start the application
    globalTracker := &GlobalTracker{}
    fooService := FooService{tracker: globalTracker, /*...*/}
    application.Start()

    // wait for all goroutines to finish before shutting down
    <-application.Done()
    globalTracker.Wait()
}

Conclusion

En conclusion, bien que l'implémentation originale de GoroutineTracker fonctionne et puisse suivre les goroutines, son utilisation du package Reflect pour vérifier et appeler dynamiquement des fonctions introduit une complexité inutile et des erreurs d'exécution potentielles. En refactorisant le code pour accepter directement les littéraux de fonction, nous obtenons une sécurité de type améliorée, une gestion rationalisée des erreurs et une lisibilité améliorée. Cette approche exploite le système de types vérifiés par le compilateur Go pour garantir la compatibilité entre les signatures de fonction et les arguments, ce qui aboutit à un code plus robuste et plus maintenable. En adoptant ces changements, nous optimisons GoroutineTracker pour plus de clarté et de fiabilité, en nous alignant sur les meilleures pratiques de programmation Go.


Auteur

Je m'appelle Oliver Nguyen. Un éditeur de logiciels travaillant principalement en Go et JavaScript. J'aime apprendre et voir une meilleure version de moi-même chaque jour. Lancez occasionnellement de nouveaux projets open source. Partagez vos connaissances et vos réflexions tout au long de mon voyage.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Article précédent:Golang Web : méthode POSTArticle suivant:Golang Web : méthode POST