Menu

Golang 클로저와 익명 함수: 예제로 이해하기

Go의 익명 함수는 주변 스코프의 변수를 캡처할 수 있습니다. 이것이 클로저이며, 자기 상태를 가진 카운터, 제너레이터, 미들웨어, 콜백을 만듭니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

익명 함수

함수 리터럴은 이름이 없는 함수입니다. 표현식이므로 값이 들어갈 수 있는 곳이면 어디든 쓸 수 있습니다. 변수, 인자, 반환 값, 구조체 필드 모두 가능합니다.

Go에는 화살표나 람다 축약 문법이 없습니다. 온전한 func(params) results { body } 형태가 유일한 작성법입니다.

변수 캡처

함수 리터럴은 감싸는 함수의 변수를 쓸 수 있습니다. 변수를 복사하지 않습니다. 클로저와 주변 코드가 변수 하나를 공유합니다.

캡처된 변수는 그것을 선언한 함수가 반환한 뒤에도, 어떤 클로저가 참조하는 동안은 살아 있습니다. 필요하면 컴파일러가 변수를 힙으로 옮기며, 이 부분을 직접 관리할 일은 없습니다.

제너레이터와 카운터

함수에서 클로저를 반환하면 반환된 함수마다 자기만의 상태를 갖게 됩니다.

counter()를 호출할 때마다 새 n이 만들어지므로 c1c2는 독립적으로 셉니다. 바깥에서는 n을 읽거나 초기화할 수 없으니, 작은 형태의 캡슐화인 셈입니다.

반복문 안의 클로저 (Go 1.22 이상)

Go 1.22 이전에는 for 반복문 전체에 반복 변수가 하나뿐이었습니다. 반복문 안에서 만든 클로저는 모두 그 변수 하나를 캡처했고 마지막 값을 봤습니다. Go에서 가장 흔한 클로저 버그였습니다.

Go 1.22부터는 반복마다 자기 복사본을 가지므로 아래 코드는 0 1 2를 출력합니다:

Go 1.21 이하에서는 같은 반복문을 for i := 0; i < 3; i++로 쓰면(정수에 대한 range도 1.22에서 새로 생겼습니다) 3 3 3이 출력되었습니다. 새 규칙은 go.modgo 줄을 기준으로 모듈 단위로 적용되므로, 오래된 모듈은 go 줄을 1.22 이상으로 올리기 전까지 예전 동작을 유지합니다. 오래된 코드의 반복문 안에서 i := iv := v를 여전히 볼 수 있습니다. 예전의 수동 해결책이었고 지금은 불필요합니다.

반복마다 새 변수를 만드는 규칙은 for 문 자체가 선언한 변수에만 해당합니다. 반복문 앞에서 선언하고 본문에서 갱신하는 변수는 여전히 공유되는 변수 하나입니다:

var last string
for _, s := range items {
	last = s
	handlers = append(handlers, func() { use(last) }) // every closure sees the final value
}

클로저와 고루틴

함수 리터럴로 시작한 고루틴도 클로저입니다. 같은 공유 규칙이 적용되고, 여기에 일반적인 동시성 규칙이 더해집니다. 여러 고루틴이 캡처된 변수에 쓴다면 뮤텍스나 채널이 필요합니다.

이 코드는 항상 5050을 출력합니다. 각 고루틴은 자기 i를 읽고(Go 1.22 반복문 의미론) 공유 변수 total에는 잠금을 건 상태로 씁니다. 뮤텍스를 없애면 결과를 예측할 수 없게 되고, go run -race가 데이터 레이스로 보고합니다. WaitGroup과 레이스 탐지기는 고루틴 페이지를 참고하세요.

실제 코드에서 클로저가 쓰이는 곳

  • 정렬과 검색: slices.SortFunc, slices.IndexFunc, sort.Slice는 모두 함수 리터럴을 받습니다.
  • 지연 정리: defer func() { ... }()는 함수가 끝날 때 블록을 실행하고, 함수의 변수를 읽을 수 있습니다. defer를 참고하세요.
  • HTTP 미들웨어: 핸들러를 받아서 그것을 감싸는 새 http.HandlerFunc 리터럴을 반환하는 함수입니다.
  • 설정: 함수형 옵션(func WithTimeout(d time.Duration) Option { return func(c *Config) { c.Timeout = d } })은 인자를 캡처한 클로저입니다.

매개변수와 카운터를 모두 캡처하는 미들웨어 예제입니다:

흔한 실수

  • 스냅숏을 기대함. 클로저는 만들어질 때의 값이 아니라 실행될 때 변수의 현재 값을 읽습니다. 스냅숏이 필요하면 값을 매개변수로 넘기세요: go func(v int) { ... }(x).
  • 재귀 리터럴. 함수 리터럴은 같은 :=에서 자신이 대입되는 변수로 자기 자신을 참조할 수 없습니다. 변수를 먼저 선언하세요: var walk func(n int); walk = func(n int) { ... walk(n-1) }.
  • 고루틴에서 동기화 없이 쓰기. 캡처는 동기화가 아닙니다. 공유 변수에 쓸 때는 보호하세요.

자주 묻는 질문

Go에서 익명 함수란 무엇인가요?

이름이 없는 함수 리터럴입니다: func(x int) int { return x * 2 }. 변수에 대입하거나, 인자로 넘기거나, 반환하거나, 닫는 중괄호 뒤에 ()를 붙여 바로 호출할 수 있습니다. Go에는 별도의 람다 문법이 없고, 함수 리터럴이 곧 람다입니다.

Go에서 클로저란 무엇인가요?

바깥에서 선언된 변수를 참조하는 함수 리터럴입니다. 함수는 그 변수들을 살려 두고 감싸는 스코프와 공유하므로, 클로저 안에서 바꾼 값은 밖에서도 보이고 그 반대도 마찬가지입니다. 변수는 복사되지 않고 참조로 캡처됩니다.

Go 클로저는 변수를 값으로 캡처하나요, 참조로 캡처하나요?

참조로 캡처합니다. 클로저와 주변 코드가 같은 변수를 씁니다. 스냅숏이 필요하면 클로저를 만들기 전에 값을 새 변수에 복사하거나 인자로 넘기세요.

Go의 클로저 반복 변수 버그는 고쳐졌나요?

네, Go 1.22부터입니다. 이제 for 반복문은 반복마다 새 반복 변수를 선언하므로, 반복문 안에서 만든 클로저와 고루틴은 해당 반복의 값을 봅니다. go.modgo 1.22 이상이 적힌 모듈에 적용됩니다. 오래된 코드에는 우회책으로 반복문 안에 i := i가 있는 경우가 많은데, 해는 없지만 더는 필요하지 않습니다.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기