ホームページ  >  記事  >  バックエンド開発  >  リファクタリング: 不要なリフレクトを使用したGoroutineTracker

リファクタリング: 不要なリフレクトを使用したGoroutineTracker

PHPz
PHPzオリジナル
2024-07-17 04:26:29737ブラウズ

Refactor: GoroutineTracker with unnecessary usage of reflect

今日、会社のコードベースでこのコードに遭遇しました(コードとコメントはデモ目的で書き直されており、独自のコードは含まれていません):

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

GoroutineTracker は、コードベース内のゴルーチンの使用状況 (ゴルーチンの数、各ゴルーチンにかかった時間など) を追跡するために使用されます。 Go メソッドは、新しいゴルーチンを開始して追跡するために使用されます。 Wait メソッドは、すべてのゴルーチンが終了するのを待つために使用されます。

使用例:

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

問題: リフレクトの使用は不要であり、回避できます。

そのコードは機能しますが、reflect パッケージを使用して関数の署名を確認してから関数を呼び出します。これはまったく不要であり、使用法を次のように変更することで回避できます。

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

新しいコードはよりシンプルになり、多くの利点があります:

  • タイプセーフティ: リフレクトを使用して関数のシグネチャをチェックする必要はありません。コンパイラがそれをやってくれます。関数のシグネチャが引数と一致しない場合、元のコードにはランタイム エラーが発生する可能性があります。
  • エラー処理: 関数からエラーを返し、呼び出し元で処理できます。元のコードは関数の出力を無視します。
  • 可読性: 新しいコードはより読みやすく、理解しやすくなっています。コード内で関数のシグネチャと引数を直接確認できます。

GoroutineTracker のより良い実装

リファクタリングされたコードは次のとおりです:

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

すべてのゴルーチンが完了するまで待ってからシャットダウンします

GoroutineTracker のもう 1 つの使用例は、アプリケーションをシャットダウンする前にすべての goroutine が終了するのを待つことです。したがって、2 種類の待機が可能です:

  • 関数内: すべてのローカル goroutine が終了するのを待機しています。
  • アプリケーションのシャットダウン時: いずれかの GoroutineTracker によって開始されたすべての goroutine が終了するのを待機しています。

グローバル トラッカーを追加し、任意のトラッカーにその機能をグローバル トラッカーに登録させることで、これを実装できます。

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

そして、WaitForAll() を使用して、アプリケーションをシャットダウンする前にすべてのゴルーチンが完了するのを待つことができます。

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

結論

結論として、GoroutineTracker の元の実装は動作し、ゴルーチンを追跡できますが、関数を動的にチェックして呼び出すためにリフレクト パッケージを使用すると、不必要な複雑さと潜在的なランタイム エラーが発生します。関数リテラルを直接受け入れるようにコードをリファクタリングすることで、型の安全性が向上し、エラー処理が合理化され、可読性が向上します。このアプローチでは、Go のコンパイラでチェックされた型システムを活用して、関数のシグネチャと引数の間の互換性を確保し、より堅牢で保守しやすいコードを実現します。これらの変更を採用することで、Go プログラミングのベスト プラクティスに合わせて、GoroutineTracker を最適化して明確さと信頼性を実現します。


著者

私はオリバー・グエンです。主に Go と JavaScript を扱うソフトウェア メーカー。私は毎日学び、より良い自分を見ることを楽しんでいます。時々、新しいオープンソース プロジェクトをスピンオフします。私の旅中の知識や考えを共有してください。

以上がリファクタリング: 不要なリフレクトを使用したGoroutineTrackerの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。