Loading
2022. 3. 26. 22:31 - lazykuna

[Golang] defer과 value capture에 대해서

짤은 본문이랑은 별 상관은 없다...

오늘도 어김없이 삽질하다가 흥미로운 글을 보아서 이에 대해서 적는다. 제목 그대로 Golang의 value capture 정책에 대한 이야기이다.

Defer과 value capture이란?

본격적으로 내용을 설명하기 전에 개념 설명을 짤막하게 하면,

  • Defer — 어떤 함수가 종료될 때 마지막으로 수행하는 작업이다. C++의 destructor과 비슷하기도 하지만, 단위가 block이 아니라 함수의 종료라는 점이 차이점이다.
  • value capture — 함수형 프로그래밍에서 많이 쓰이는 개념인데, 보통 어떤 클로저(lambda 함수)가 선언되었을 때, 외부 변수 값을 참조하고 있을 경우, 그 시점에서의 값(또는 상태)을 저장해두어 수행될 때 사용하는 것이다.

여기에서 value capture이 들어가는 이유는, defer된 로직은 바로 수행되는 것이 아니고 함수가 종료될 때 수행되기 때문이고, 여기서 인자로 사용되는 변수를 capture하는 과정이 동반된다.

Golang의 defer에서의 value capture

그런데 Golang은 value capture에 대한 명시적인 선언을 하지 않는다. 대신 이에 대해 암묵적인 규칙이 있다.

  • defer되는 함수는 바로 호출되지 않는다 (= 계산되지 않는다)
  • 다만, defer되는 함수에 들어가는 값은 저장된다. (= parameter은 capture된다)
  • 다만, defer되는 함수의 인자는 계산된다.
  • defer되는 함수들은 LIFO 순서를 따른다. 즉 가장 마지막에 defer된 함수가 먼저 계산된다.

참조 링크

몇 가지 모호한 예제들

그럼 이제 아래의 예제를 보자.

package main

import "fmt"

func main() {
    var whatever [5]struct{}

    for i := range whatever {
        fmt.Println(i)
    } // part 1

    for i := range whatever {
        defer func() { fmt.Println(i) }()
    } // part 2

    for i := range whatever {
        defer func(n int) { fmt.Println(n) }(i)
    } // part 3
}

첫번째 for은 defer 없이 함수를 그대로 수행하고 있다. 따라서 결과값이 0~4까지 그대로 나올 것이다.

두번째 for은 간단한 함수로 하나 감싸져서 함수가 수행된다. 그런데, value capture은 인자로 들어가는 값에 대해서만 수행된다. 따라서 i값과 상관없이 range의 마지막 값인 4가 5개 print 될 것이다.

세번째 for의 경우에는 인자를 받는 closure이 있다. 인자를 capture 시키고, defer된 순서와 반대로 숫자가 출력될 것이기 때문에 4에서부터 0까지 숫자가 출력될 것이다.

여기서 조심할 점이 있다. defer은 실행한 역순으로 결과가 나온다는 점이다. 그러므로, 처음 5개가 첫번째 for 구문, 그 다음 5개는 세번째 for 구문, 마지막 5개는 두번째 for 구문이라는 점에 유의해야 한다.

결과를 보면 실제로 그러하다.

0
1
2
3
4
4
3
2
1
0
4
4
4
4
4

그럼, Closure에서의 value capture은?

여기까지 오니 궁금한 점이 생겼다. 그럼 일반 closure에서 value capture은 어떻게 하려나?

이에 관해서 (역시나) stackoverflow에 재미난 실험이 있었다.

package main

import "fmt"

func VariableLoop() {
    f := make([]func(), 3)
    for i := 0; i < 3; i++ {
        // closure over variable i
        f[i] = func() {
            fmt.Println(i)
        }
    }
    fmt.Println("VariableLoop")
    for _, f := range f {
        f()
    }
}

func ValueLoop() {
    f := make([]func(), 3)
    for i := 0; i < 3; i++ {
        i := i
        // closure over value of i
        f[i] = func() {
            fmt.Println(i)
        }
    }
    fmt.Println("ValueLoop")
    for _, f := range f {
        f()
    }
}

func VariableRange() {
    f := make([]func(), 3)
    for i := range f {
        // closure over variable i
        f[i] = func() {
            fmt.Println(i)
        }
    }
    fmt.Println("VariableRange")
    for _, f := range f {
        f()
    }
}

func ValueRange() {
    f := make([]func(), 3)
    for i := range f {
        i := i
        // closure over value of i
        f[i] = func() {
            fmt.Println(i)
        }
    }
    fmt.Println("ValueRange")
    for _, f := range f {
        f()
    }
}

func main() {
    VariableLoop()
    ValueLoop()
    VariableRange()
    ValueRange()
}

이 코드의 결과는,

VariableLoop
3
3
3
ValueLoop
0
1
2
VariableRange
2
2
2
ValueRange
0
1
2

어... 이거 뭔가 이상하다. 이거 value capture을 하고 싶으면, 매 루프마다 값을 만들어 주어야 하는 것 같은데..?

실제로 그러하다. 공식 스펙이 아래와 같다. (다른 방법으로는 인자로 변수를 넘기면 capture 된다는 듯)

The Go Programming Language Specification

Function literals
Function literals are closures: they may refer to variables defined in a surrounding function. Those variables are then shared between the surrounding function and the function literal, and they survive as long as they are accessible.
Function literals are closures: they may refer to variables defined in a surrounding function. Those variables are then shared between the surrounding function and the function literal, and they survive as long as they are accessible.

Go FAQ: What happens with closures running as goroutines?
To bind the current value of v to each closure as it is launched, one must modify the inner loop to create a new variable each iteration. One way is to pass the variable as an argument to the closure.
Even easier is just to create a new variable, using a declaration style that may seem odd but works fine in Go.

허허... 괴상한 거 같기도 한데, 어떻게 보면 value capture을 위해 새로운 문법을 굳이 쓸 필요가 없다는 점에서는 상당히 직관적인 듯.

출처

'개발 > Developing' 카테고리의 다른 글

.git의 내부 구조, 그리고 브랜치 이름  (0) 2022.04.27
[Golang] Context 패키지  (0) 2022.03.27
[Golang] struct, tag, 그리고 reflect  (0) 2022.03.21
[Linux] rsync  (0) 2022.03.17
함수형 프로그래밍과 멀티 코어  (0) 2022.03.17