golang では、関数ポインタは重要な型であり、その主な用途にはコールバック関数、ライブラリ関数の動的ロードなどが含まれます。
しかし、実際の開発環境では、関数ポインタが無効で、プログラム内で奇妙なエラーが発生する状況によく遭遇します。この記事では、特定のケースを例として、関数ポインターが失敗する理由を分析し、この状況を回避する方法について説明します。
ケース分析
GraphQL API サービスでは、サードパーティのライブラリを使用して GraphQL クエリ ステートメントを解析し、カスタム関数を使用して一部のフィールドを処理しました。このライブラリは関数ポインター型を提供します。これにカスタム関数を渡すだけです。
具体的には、コード スニペットは次のとおりです。
type fieldResolver func(ctx context.Context, obj interface{}, args map[string]interface{}) (interface{}, error) type Resolver struct { // ... fieldResolvers map[string]fieldResolver // ... } func (r *Resolver) AddFieldResolver(fieldName string, fr fieldResolver) { r.fieldResolvers[fieldName] = fr }
AddFieldResolver メソッドは、フィールド解析関数ポインターをリゾルバー型構造体に追加するために使用されます。同時に、この Resolver 型構造は GraphQL Resolver インターフェイスも実装します。
私の実装では、このリゾルバー型構造体に 2 つのフィールド解決関数ポインターを追加しました。これら 2 つの関数ポインターは、それぞれ 2 つのフィールド名 name と created_at によって決定されます。
func (r *Resolver) BaseQuery() QueryResolver { return &queryResolver{r} } func (r *Resolver) name(ctx context.Context, obj interface{}, f graphql.ResolveFieldParams) (interface{}, error) { // handler for name field } func (r *Resolver) created_at(ctx context.Context, obj interface{}, f graphql.ResolveFieldParams) (interface{}, error) { // handler for created_at field } func (r *Resolver) initFieldResolvers() { r.fieldResolvers = map[string]fieldResolver{ "name": r.name, "created_at": r.created_at, } }
これら 2 つのフィールド解決関数ポインターの初期化は、Resolver 型構造体のコンストラクターで呼び出される initFieldResolvers メソッドで完了します。
また、特定の Resolver インターフェイスを Resolver 型構造に実装する必要があります。その具体的なメソッドは次のとおりです:
type queryResolver struct{ *Resolver } func (r *queryResolver) Name(ctx context.Context, obj *types.User) (string, error) { resolver, ok := r.fieldResolvers["name"] if !ok { return "", fmt.Errorf("resolver not found") } result, err := resolver(ctx, obj, nil) if err != nil { return "", err } return result.(string), nil } func (r *queryResolver) CreatedAt(ctx context.Context, obj *types.User) (string, error) { resolver, ok := r.fieldResolvers["created_at"] if !ok { return "", fmt.Errorf("resolver not found") } result, err := resolver(ctx, obj, nil) if err != nil { return "", err } return result.(string), nil }
これは、前に登録した解析関数ポインタ、つまり name を動的に呼び出します。と created_at の 2 つの関数。
しかし、テスト プロセス中に、この実装が非常に不安定であることがわかりました。正常に動作する場合もありますが、「リゾルバーが見つかりません」というエラーが報告される場合もありました。
原因分析
この場合、私はまず関数ポインターの失敗の可能性を考えました。実際の開発では、関数ポインタを構造体変数に格納し、その変数の値を別の場所で呼び出したところ、ポインタの有効期限が切れていたという同様の問題がよく発生します。
Go 言語では、他の言語と同様、関数は第一級市民であり、関数ポインタも変数として格納されます。通常の状況では、関数ポインタの有効期限は切れません。理論上、メモリに格納されるのは呼び出すことができるポインタ アドレスです。このポインタはプログラムが終了するまで再利用されません。
ただし、このシナリオでは、この Resolver 型構造が複数のコルーチン間で共有されるため、同時アクセスが発生し、コルーチンが終了してメモリを解放する状況も発生します。これにより、関数ポインタが無効になる可能性があります。
解決策
関数ポインターの失敗の問題を解決するには、ポインターが失敗する状況を回避することが重要です。 golang の同時プログラミングでは、特定のデータが同時にアクセスされたときに問題が発生しないことを保証するための技術的手段がいくつかあります。次に、無効な関数ポインターを回避するための 2 つの一般的なテクニックを紹介します。
上記のコードの Resolver 型構造では、sync.RWMutex 型を使用して fieldResolvers フィールドの同時読み取りを保護できます。そして書くこと。こうすることで、fieldResolvers フィールドの読み取り時に競合状態が発生しないようにすることができます。
同時に、マップ タイプの代わりに関数ポインタのスライスを使用することもでき、関数ポインタを読み取るときにマップ タイプにアクセスする必要がないため、競合状態の発生が回避されます。
具体的なコードは次のとおりです。
type Resolver struct { sync.RWMutex fieldResolvers []*fieldResolver } func (r *Resolver) AddFieldResolver(fr *fieldResolver) { r.Lock() defer r.Unlock() r.fieldResolvers = append(r.fieldResolvers, fr) } func (r *Resolver) name(ctx context.Context, obj interface{}, f graphql.ResolveFieldParams) (interface{}, error) { // handler for name field } func (r *Resolver) created_at(ctx context.Context, obj interface{}, f graphql.ResolveFieldParams) (interface{}, error) { // handler for created_at field } func (r *Resolver) initFieldResolvers() { r.AddFieldResolver(&r.name) r.AddFieldResolver(&r.created_at) }
ここでは、fieldResolver のタイプを map[string]fieldResolver から []*fieldResolver に変更しました。ポインタ型を使用すると、冗長なメモリ割り当てとデータのコピーが回避されます。
無効な関数ポインタを回避するもう 1 つの方法は、関数ポインタをチャネルに保存することです。具体的には、関数ポインタを呼び出す必要がある場合、関数ポインタをチャネルに送信し、呼び出しを行う前にチャネルが関数ポインタを返すのを別のコルーチンで待機することができます。
これにより、関数ポインタが期限切れにならないことが保証され、コルーチン終了時のメモリ解放などの問題を回避できます。
具体的なコードは次のとおりです。
type Resolver struct { fieldResolvers chan *fieldResolver // ... } func (r *Resolver) AddFieldResolver(fr *fieldResolver) { r.fieldResolvers <- fr } func (r *Resolver) initFieldResolvers() { // ... go func() { for fr := range r.fieldResolvers { if fr != nil { // call the function pointer } } }() }
ここでは、fieldResolvers のタイプを chan *fieldResolver に変更し、コルーチンを通じてこのチャネルの関数ポインターを呼び出しました。
結論
golang で発生する関数ポインタの失敗の問題については、プログラムの同時実行性とメモリ解放の問題に注意を払う必要があります。競合状態やメモリ管理の問題を回避するには、RWMutex や chan などの golang の強力な同時プログラミング機能を利用できます。
同時に、ポインター型の使用にも注意を払い、不必要なメモリ割り当てやデータのコピーを回避して、関数ポインターが失敗する可能性をできる限り減らす必要があります。
以上がgolang 関数ポインタが無効ですの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。