문제
for i := 0; i < 8; i++ {
CmbGateName[i].OnChanged = func(s string) {
structs.G_Config.StrGateName[i] = s
}
}
위의 코드를 실행 중, 예상치 못한 결과가 나왔다.
CmbGateName는 사이즈가 8인 Fyne의 widget.Select 배열로 선언되어 있다.
또 structs.G_Config.StrGateName 는 사이즈가 8인 스트링 배열이다.
OnChange 함수를 등록해서 해당 셀렉트 위젯이 변경될 때 실행될 함수를 넣어주고 있다.
-> CmbGateName[i] 가 변경되면 골라진 값을 struct.G_Config.StrGateName [i]에 넣어준다.
하지만 OnChange가 동작해야 할 때
panic: runtime error: index out of range [8] with length 8
실제로는 위와같은 에러를 뜨며 프로그램이 종료되는 것이었다;;
콜 스택의 꼭대기에선 이곳을 가리키고 있었다.
for i := 0; i < 8; i++ {
CmbGateName[i].OnChanged = func(s string) {
===> structs.G_Config.StrGateName[i] = s <===
}
}
for문의 조건이 i < 8이므로 for문 내에서 i == 8인 경우는 절대로 없을 거라고 생각했다.
CmbGateName[0].OnChanged = func(s string) {
structs.G_Config.StrGateName[0] = s
}
CmbGateName[1].OnChanged = func(s string) {
structs.G_Config.StrGateName[1] = s
}
CmbGateName[2].OnChanged = func(s string) {
structs.G_Config.StrGateName[2] = s
}
CmbGateName[3].OnChanged = func(s string) {
structs.G_Config.StrGateName[3] = s
}
...
CmbGateName[7].OnChanged = func(s string) {
structs.G_Config.StrGateName[7] = s
}
for문의 예상 동작은 위와 같았다.
-> CmbGateName[i] 가 변경되면 골라진 값을 struct.G_Config.StrGateName[i]에 넣어준다.
당연해 보인다..
왜 패닉되는걸까..?
해결
한참 디버깅을 하다가 클로저 때문이었다는걸 알게 되었다.
Go functions may be closures. A closure is a function value that references variables from outside its body. The function may access and assign to the referenced variables; in this sense the function is "bound" to the variables.
-A Tour of Go
Tour of Go의 예시처럼 함수를 선언할 때 함수 밖의 내용을 참조할 수 있다는 것이다.
일반적으로 함수가 호출되고 종료되면, 그 함수(스코프?) 내의 지역 변수들은 스택 메모리에서 제거된다.
하지만 클로저가 해당 변수들을 참조하고 있다면 이 변수들은 힙 메모리에 유지되며,
클로저가 더 이상 사용되지 않거나 참조하지 않을 때까지 메모리에서 해제되지 않는다.
클로즈는 알고 있었는데 막상 클로저를 만나니 못 알아본 것이었다...
for i := 0; i < 8; i++ {
CmbGateName[i].OnChanged = func(s string) {
structs.G_Config.StrGateName[i] = s
}
}
다시 보니 당연해 보인다.
OnChange에 들어갈 func(s string) 내부를 보면 i는 선언되어 있지 않다.
i는 함수의 바깥에서 가져오므로 해당 익명 함수는 클로저가 되는 것이었다.
(변수 쉐도잉도 조심해야 할 듯)
for문은 i를 증가시키다가 i == 8 이 되었을 때, i < 8 조건에 의해 루프를 그만두지만
i는 클로저에서 참조하고 있기 때문에 사라지지 않게 된다.
따라서 i == 8 인 상태로 계속 참조되는 것이다.
그러므로 for문을 전부 돌고 나면
CmbGateName[0].OnChanged = func(s string) {
structs.G_Config.StrGateName[8] = s
}
CmbGateName[1].OnChanged = func(s string) {
structs.G_Config.StrGateName[8] = s
}
CmbGateName[2].OnChanged = func(s string) {
structs.G_Config.StrGateName[8] = s
}
CmbGateName[3].OnChanged = func(s string) {
structs.G_Config.StrGateName[8] = s
}
...
CmbGateName[7].OnChanged = func(s string) {
structs.G_Config.StrGateName[8] = s
}
위와 같은 결과가 남게 되는 것이다..
생각대로 동작하기 위해선
for i := 0; i < 8; i++ {
CmbGateName[i].OnChanged = func(s string) {
index := i
structs.G_Config.StrGateName[index] = s
}
}
새로운 변수를 만들고 거기에 i를 넣어줌으로써 더 이상 바깥쪽의 변수들을 직접적으로 참조하지 않도록 할 수 있다.
이제는 이런 코드가 어떻게 동작할지 예상할 수 있다!!
package main
import (
"fmt"
)
func main() {
var funcs []func()
for i := 0; i < 3; i++ {
funcs = append(funcs, func() {
fmt.Println(i)
})
}
for _, f := range funcs {
f()
}
}
(Go playground에서 실행해 볼 때랑 컴파일 해서 실행해 볼 때랑 출력이 다른데 왜 그런지는 잘 모르겠다)
'Go' 카테고리의 다른 글
[Go] 컨트리뷰트 도전 실패 (0) | 2024.06.10 |
---|---|
[Go] multipart/form-data 전송, 수신 (0) | 2024.06.10 |
[Go] 테스트가능한 예시 (Testable Examples) (0) | 2024.06.05 |
[Go] 빈 구조체의 의미와 활용 struct{} (0) | 2024.06.04 |