web-dev-qa-db-ja.com

forループでのポインターの使用

ある状態でコードにバグがあり、他の状態ではない理由を理解するのに苦労しています。ポインタをカバーしてから久しぶりなので、さびたかも!

基本的に、オブジェクトをメモリに格納するために使用しているリポジトリ構造があり、Store関数があります。

type chartsRepository struct {
    mtx    sync.RWMutex
    charts map[ChartName]*Chart
}

func (r *chartsRepository) Store(c *Chart) error {
    r.mtx.Lock()
    defer r.mtx.Unlock()
    r.charts[c.Name] = c
    return nil
}

したがって、RWミューテックスロックをオンにし、識別子によって参照されるマップへのポインターを追加するだけです。

次に、基本的にこれらのオブジェクトのスライスをループして、すべてをリポジトリに格納する関数を用意しました。

type service struct {
    charts Repository
}

func (svc *service) StoreCharts(arr []Chart) error {
    hasError := false
    for _, chart := range arr {
        err := svc.repo.Store(&chart)
        // ... error handling
    }
    if hasError {
        // ... Deals with the error object
        return me
    }
    return nil
}

上記は機能しません。最初はすべて正常に機能しているように見えますが、後でデータにアクセスしようとすると、キーが異なるにもかかわらず、マップのエントリはすべて同じChartオブジェクトを指しています。

次のようにしてポインター参照を別の関数に移動すると、すべてが期待どおりに機能します。

func (svc *service) StoreCharts(arr []Chart) error {
    // ...
    for _, chart := range arr {
        err := svc.storeChart(chart)
    }
    // ...
}

func (svc *service) storeChart(c Chart) error {
    return svc.charts.Store(&c)
}

この問題は、ループがchartループ内のforへの参照を上書きするため、ポインターの参照も変更されることを想定しています。ポインターが独立した関数で生成されると、その参照は上書きされません。そうですか?

ばかげているように感じますが、&chartによってポインタを生成するべきではありません。これはchart参照とは無関係ですか?また、forループでポインターp := &chartの新しい変数を作成しようとしましたが、それも機能しませんでした。

ループ内でポインタを生成することを避けるべきですか?

17
Simon

これは、単一のループ変数chartがあり、各反復で新しい値がそれに割り当てられるだけだからです。したがって、ループ変数のアドレスを取得しようとすると、各反復で同じになるため、同じポインターを格納し、指定されたオブジェクト(ループ変数)が各反復(およびループの後)で上書きされます。最後の反復で割り当てられた値を保持します)。

これは で説明されています。仕様:ステートメントの場合:range句を含むステートメントの場合:

反復変数は、 short変数宣言:=)。この場合、それらのタイプはそれぞれの反復値のタイプに設定され、それらの scope は「for」ステートメントのブロックです。 これらは各反復で再利用されます。反復変数が「for」ステートメントの外側で宣言されている場合、実行後の値は最後の反復の値になります。

2番目のバージョンは機能します。ループ変数を関数に渡すため、そのコピーが作成され、コピーのアドレス(ループ変数から切り離されています)を格納します。

ただし、関数なしでも同じ効果を得ることができます。ローカルコピーを作成し、そのアドレスを使用するだけです。

for _, chart := range arr {
    chart2 := chart
    err := svc.repo.Store(&chart2) // Address of the local var
    // ... error handling
}

また、スライス要素のアドレスも保存できることに注意してください。

for i := range arr {
    err := svc.repo.Store(&arr[i]) // Address of the slice element
    // ... error handling
}

これの欠点は、スライス要素へのポインターを格納するため、ポインターのいずれかを保持している限り、スライスのバッキング配列全体をメモリーに保持する必要があることです(配列はガベージコレクションできません)。さらに、保存したポインターはスライスと同じChart値を共有するため、渡されたスライスのチャート値を誰かが変更した場合、保存したポインターを持つチャートに影響を与えます。

関連する質問を参照してください:

Golang:ループスライスの範囲を使用して複数のルートを登録/マップ

なぜこれら2つのforループのバリエーションが異なる動作をするのですか?

27
icza