> 백엔드 개발 > Golang > Go 루프 탐색의 함정을 기록하세요.

Go 루프 탐색의 함정을 기록하세요.

藏色散人
풀어 주다: 2021-02-19 17:38:23
앞으로
2370명이 탐색했습니다.

튜토리얼 칼럼에서는 GO의 루프를 통해 작은 구덩이를 사용하는 방법을 공유할 예정이니, 필요한 친구들에게 도움이 되길 바랍니다!

Go 루프 탐색의 함정을 기록하세요.Golang의 프로세스 제어에는 for와 range라는 두 가지 유형의 루프 문이 있습니다.

for 문

1. 대입식, 관계식 또는 논리식, 대입식 { }
for i := 0; i < 10; i++ {}
로그인 후 복사
2. 관계식 또는 논리식 수식 { }</ code></h4><div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">n := 10for n &gt; 0 {  n--}</pre><div class="contentsignin">로그인 후 복사</div></div> <p><code>3.for { }1.for 赋值表达式; 关系表达式或逻辑表达式; 赋值表达式 { }

for {
 fmt.Println("hello world")
}
// 等价于
// for true {
//     fmt.Println("hello world")
// }
로그인 후 복사

2.for 关系表达式或逻辑表达式 { }

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
로그인 후 복사

3.for { }

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
로그인 후 복사

range语句

Golang range类似迭代器操作,可以对 slice、map、数组、字符串等进行迭代循环。在字符串、数组和切片中它返回 (索引, 值) ,在集合中返回 (键, 值),但若当只有一个返回值时,第一个参数是索引或键。

func main() {
    a1 := []int{1, 2, 3}
    a2 := make([]*int, len(a1))

    for i, v := range a1 {
        a2[i] = &v
    }

    fmt.Println("值:", *a2[0], *a2[1], *a2[2])
    fmt.Println("地址:", a2[0], a2[1], a2[2])
}
// 输出结果
// 值: 3 3 3
// 地址: 0xc000012090 0xc000012090 0xc000012090
로그인 후 복사

for循环尤其是range语句,在平时开发过程中频繁使用,但很多开发者(本人算一个)经常会在以下场景中踩坑。

场景一,使用循环迭代器的变量

先来看一个明显的错误:

func main() {
    var out [][]int
    for _, i := range [][1]int{{1}, {2}, {3}} {
        out = append(out, i[:])
    }
    fmt.Println("Values:", out)}// 输出结果// [[3] [3] [3]]
로그인 후 복사

分析

out是一个整型指针数组变量,在for循环中,声明了一个i变量,每次循环将i的地址追加到out切片中,但是每次追加的其实都是i变量,因此我们追加的是一个相同的地址,而该地址最终的值是3。

正确做法

解开代码中的注释// i := i,每次循环时都重新创建一个新的i变量。


再看一个比较隐秘的错误:

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
로그인 후 복사

分析

大多数人就是在range这里给变量赋值的时候踩坑,因为比较隐秘,其实情况和上面的一样,range在遍历值类型时,其中的v是一个局部变量,只会声明初始化一次,之后每次循环时重新赋值覆盖前面的,所以给a2[i]赋值的时候其实都是同一个地址&v,而v最终的值为a1最后一个元素的值,也就是3。

正确做法

a2[i]赋值时传递原始指针,即a2[i] = &a1[i]
②创建临时变量t := va2[i] = &t
③闭包(与②原理一样),func(v int) { a2[i] = &v }(v)


更为隐秘的还有:

for _, val := range values {
    wg.Add(1)
    val := val    go func() {
        fmt.Println(val)
        wg.Done()
    }()}
로그인 후 복사

原理也是一样的,不论遍历多少次,i[:]总是被本次遍历的值所覆盖

场景二,在循环体内使用goroutines

for _, val := range values {
    wg.Add(1)
    go func(val int) {
        fmt.Println(val)
        wg.Done()
    }(val)}
로그인 후 복사

分析

对于主协程来讲,循环是很快就跑完的,而这个时候各个协程可能才开始跑,此时val的值已经遍历到最后一个了,所以各协程都输出了3。(如果遍历数据庞大,主协程遍历耗时较久的话,goroutine的输出会根据当时候的valrrreee

range 문

Golang 범위는 반복자 작업과 유사하며 슬라이스, 맵, 배열, 문자열 등을 반복할 수 있습니다. 문자열, 배열 및 슬라이스에서는 (인덱스, 값)을 반환하고 컬렉션에서는 (키, 값)을 반환하지만 반환 값이 하나만 있는 경우 첫 번째 인수는 인덱스 또는 키입니다.

rrreee
for 루프, 특히 range 문은 일상적인 개발 과정에서 자주 사용되지만, 저를 포함한 많은 개발자들은 다음과 같은 시나리오에서 실수를 자주 저지릅니다.

시나리오 1, 루프 반복기의 변수 사용🎜🎜먼저 명백한 오류를 살펴보겠습니다. 🎜rrreee🎜🎜Analytic🎜🎜🎜out은 for에서 정수 포인터 배열 변수입니다. loop 에서는 i 변수가 선언되고 각 루프는 i의 주소를 out 슬라이스에 추가하지만 매번 추가되는 것은 실제로는 i 변수이므로 동일한 주소를 추가하고 주소의 최종 값은 3입니다. 🎜🎜🎜올바른 접근 방식🎜🎜🎜코드에서 // i := i 주석을 잠금 해제하고 반복될 때마다 새 i 변수를 다시 만듭니다. 🎜
🎜좀 더 비밀스러운 실수를 살펴보겠습니다: 🎜rrreee🎜🎜Analytics🎜🎜🎜대부분의 사람들은 여기 범위의 변수에 값을 할당할 때 실수를 합니다. 사실 상황은 위와 같습니다. range가 값 유형을 순회할 때 v는 지역 변수이며 한 번만 선언되고 초기화된 후 다시 할당됩니다. 이전 항목을 덮어쓰기 위해 반복할 때마다 a2[i]에 값을 할당할 때 실제로는 모두 동일한 주소 &v를 가지며 최종 값을 갖습니다. va1입니다. 마지막 요소의 값은 3입니다. 🎜🎜🎜올바른 접근 방식🎜🎜🎜1a2[i] 할당 시 원래 포인터를 전달합니다. 즉, a2[i] = &a1[i]
②임시 변수 t := v 생성 a2[i] = &t
③ 클로저(2번과 같은 원리), func(v int) { a2 [i] = &v }(v)🎜
🎜더 비밀스러운 점은: 🎜rrreee🎜원칙은 동일합니다. 몇 번을 탐색하더라도 i[ :] 항상 이 순회 값으로 덮어쓰기🎜🎜🎜🎜시나리오 2, goroutines 사용🎜rrreee🎜🎜analytic🎜🎜🎜메인 코루틴의 경우 루프는 빠르게 실행되며 이때 각 코루틴은 방금 실행을 시작했습니다. 이때 val의 값은 마지막 값으로 순회되었으므로 각 코루틴은 3을 출력합니다. (순회 데이터가 크고 메인 코루틴 순회 시간이 오래 걸리는 경우, 고루틴의 출력은 그때의 val 값을 기준으로 하게 되므로 출력 결과가 동일하지 않을 수 있습니다. 매번.) 🎜🎜🎜 해결책🎜🎜🎜①임시 변수를 사용하세요🎜rrreee🎜②클로저를 사용하세요🎜rrreee

위 내용은 Go 루프 탐색의 함정을 기록하세요.의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
원천:learnku.com
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿