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 등의 대안이 있을 것 같네요.

이러고 한 반년뒤에 또 과거의 나를 탓하겠지...

매번 고민하지만 사실 답은 잘 모르겠습니다. 다만 이 글은 비개발인 저의 의견이니 참고만 해주세요 😊