panic: send on closed channel - 채널을 잘 닫자 🕵🏼♂️
고루틴과 채널은 golang에서 가장 핵심적인 기능 중 하나입니다.
다만 꼼꼼하게 체크하고 사용하지 않으면 여러가지 문제들을 만들어낼 수 있습니다. 그 중 하나는 Close된 채널에 값을 전달하는 상황인데요. 이런 경우 Application은 panic으로 종료하게 됩니다.
panic: send on closed channel
goroutine 1 [running]:
main.main()
/tmp/sandbox2358964969/prog.go:19 +0xfc
우선 간단한 방법으로 이를 예방할 수 있는데요. 채널에 값을 보내기 전 채널로 아래 safeCheck
함수와 같이 채널의 Close 여부를 체크하고, 결과에 따라서 값의 송신 여부를 결정하면 됩니다.
func safeCheck(ch <-chan string) bool {
select {
case <-ch:
return false
default:
}
return true
}
테스트를 해보면 이런 느낌이죠.
package main
import "fmt"
func safeCheck(ch <-chan string) bool {
select {
case <-ch:
return false
default:
}
return true
}
func main() {
c := make(chan string)
fmt.Printf("channel status: %v\n", safeCheck(c))
close(c)
fmt.Printf("channel status: %v\n", safeCheck(c))
}
/*
$ ./chantest
channel status: true
channel status: false
*/
다만 저 방식도 온전한 방식은 아닙니다. 만약 true를 리턴한 찰나에 채널이 close 되버리면 똑같이 panic이 발생할 수 있습니다. (어느정도는 보호되겠지만 간헐적으로 나올 수 있겠죠)
그래서 저렇게 체크하는 코드에 의존하는 것 보단 채널의 상태를 코드단에서 확실하게 컨트롤하는게 더 좋은 방법이란 생각이 듭니다.
package main
import (
"fmt"
"time"
)
func safeCheck(ch <-chan string) bool {
select {
case <-ch:
return false
default:
}
return true
}
func main() {
c := make(chan string)
go func(ch chan string) {
// receive
fmt.Println(<-ch)
// close(ch) 이거보단...
}(c)
go func(ch chan string) {
// send
if safeCheck(ch) {
ch <- "abcd"
close(ch) // 이게 더 좋아보여요
}
}(c)
time.Sleep(time.Second * 2)
}
만약 송/수신이 1:1 관계라면 송신쪽에서 close 여부를 결정하는게 더 좋을 것 같고, 만약 1:n, n:n 관계라면 그냥 안닫고 main 루틴에게 맡기거나 송신자들 간의 합의 이후 close를 하는게 좋다고 생각됩니다. 중복 close를 막기 위해선 mutex 등의 대안이 있을 것 같네요.
매번 고민하지만 사실 답은 잘 모르겠습니다. 다만 이 글은 비개발인 저의 의견이니 참고만 해주세요 😊