ホームページ  >  記事  >  バックエンド開発  >  Go のループトラバーサルの落とし穴を記録する

Go のループトラバーサルの落とし穴を記録する

藏色散人
藏色散人転載
2021-02-19 17:38:232293ブラウズ

次のチュートリアル コラムでは、Go のループ トラバーサルの落とし穴について説明します。必要な友人の役に立てば幸いです。

Go のループトラバーサルの落とし穴を記録するGolang のフロー制御には、for と range の 2 種類のループ ステートメントがあります。

for ステートメント

1.for 代入式、関係式または論理式、代入式{}

for i := 0; i ef9e3ef6040f5b2c62734f0aa4b71b16 0 {
 n--}

3.for { }

for {
 fmt.Println("hello world")
}
// 等价于
// for true {
//     fmt.Println("hello world")
// }

range ステートメントGolang rangeこれはイテレータ操作に似ており、スライス、マップ、配列、文​​字列などを反復処理できます。文字列、配列、スライスでは (インデックス、値) を返し、コレクションでは (キー、値) を返しますが、戻り値が 1 つだけの場合、最初の引数はインデックスまたはキーになります。

str := "abc"
for i, char := range str {
    fmt.Printf("%d => %s\n", i, string(char))
}
for i := range str { //只有一个返回值
    fmt.Printf("%d\n", i)
}
nums := []int{1, 2, 3}
for i, num := range nums {
    fmt.Printf("%d => %d\n", i, num)
}
kvs := map[string]string{"a": "apple", "b": "banana"}
for k, v := range kvs {
    fmt.Printf("%s => %s\n", k, v)
}
for k := range kvs { //只有一个返回值
    fmt.Printf("%s\n", k)
}
// 输出结果
// 0 => a
// 1 => b
// 2 => c
// 0
// 1
// 2
// 0 => 1
// 1 => 2
// 2 => 3
// a => apple
// b => banana
// a
// b

for ループ、特に range ステートメントは日常の開発プロセスで頻繁に使用されますが、多くの開発者 (私を含む) が次のようなシナリオで罠に陥ることがよくあります。

シナリオ 1、ループ反復子の変数を使用する

最初に明らかな間違いを見てみましょう:
func main() {
    var out []*int
    for i := 0; i < 3; i++ {
        // i := i
        out = append(out, &i)
    }
    fmt.Println("值:", *out[0], *out[1], *out[2])
    fmt.Println("地址:", out[0], out[1], out[2])
}
// 输出结果
// 值: 3 3 3
// 地址: 0xc000012090 0xc000012090 0xc000012090

分析

out

は整数ポインタ配列変数であり、for ループ内で
i

変数が宣言され、i のアドレスが格納されますout スライスに追加しますが、各追加は実際には i 変数であるため、追加するものは同じアドレスであり、アドレスの最終値は 3 です。 正しいアプローチ

コード内のコメントのロックを解除します

// i := i

、ループを通過するたびに新しい # を作成します ## i
変数。

より微妙な間違いを見てみましょう: <pre class="brush:php;toolbar:false">func main() { a1 := []int{1, 2, 3} a2 := make([]*int, len(a1)) for i, v := range a1 { a2[i] = &amp;v } fmt.Println(&quot;值:&quot;, *a2[0], *a2[1], *a2[2]) fmt.Println(&quot;地址:&quot;, a2[0], a2[1], a2[2]) } // 输出结果 // 值: 3 3 3 // 地址: 0xc000012090 0xc000012090 0xc000012090</pre>

分析

ほとんどの人がここにいます
range

これは比較的秘密であるため、変数に値を代入する際の落とし穴です。実際、状況は上記と同じです。

range
が値の型をトラバースしているとき、

v はローカルです。変数であり、初期化されるだけです。一度実行すると、ループするたびに前の変数が再割り当てされるため、a2[i] に値を割り当てると、実際には同じアドレス &v になります。 、および v 最終値は、a1 の最後の要素の値、つまり 3 です。 正しいアプローチ

a2[i]

代入時に元のポインタを渡します。つまり、

a2[i] = &a1 [i]

②一時変数作成t := v;a2[i] = &t
③クロージャ(原理は②と同じ)、func (v int) { a2[i] = &v }(v)
さらに秘密的なことは次のとおりです: <pre class="brush:php;toolbar:false">func main() {     var out [][]int     for _, i := range [][1]int{{1}, {2}, {3}} {         out = append(out, i[:])     }     fmt.Println(&quot;Values:&quot;, out)}// 输出结果// [[3] [3] [3]]</pre>原理は、いくつになっても同じです。走査されるたびに、

i[:]
は常にこの走査の値で上書きされます。

シナリオ 2、ループ本体でゴルーチンを使用します。

func main() {
    values := []int{1, 2, 3}
    wg := sync.WaitGroup{}
    for _, val := range values {
        wg.Add(1)
        go func() {
            fmt.Println(val)
            wg.Done()
        }()
    }
    wg.Wait()}// 输出结果// 3// 3// 3

分析

メイン コルーチンの場合、ループはすぐに完了し、各コルーチンはこの時点でのみ実行を開始する可能性があります。このとき、
val## の値は# はすでにトラバーサルが最後のトラバーサルに到達しているため、各コルーチンは

3

を出力します。 (トラバーサルデータが巨大でメインコルーチンのトラバーサルに時間がかかる場合、ゴルーチンの出力はその時点の
val

の値に基づいて行われるため、それぞれの出力結果が同じになるとは限りません。 ) 解決策①一時変数を使用する

for _, val := range values {
    wg.Add(1)
    val := val    go func() {
        fmt.Println(val)
        wg.Done()
    }()}
②クロージャを使用する

for _, val := range values {
    wg.Add(1)
    go func(val int) {
        fmt.Println(val)
        wg.Done()
    }(val)}

以上がGo のループトラバーサルの落とし穴を記録するの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はlearnku.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。