chat 프로그램에서 go channel 을 안정적으로 사용하기
전쳬 에제 소스
https://github.com/seungkyua/hello-go
Go 프로그램에서 channel 을 사용할 때 가장 중요한 부분은 channel 을 한 번만 닫아야 한다는 것이다. 그렇지 않고 닫힌 channel 을 중복해서 닫으려고 하면 panic 이 발생한다. 그래서 Discovery Go 책에서는 channel 에 값을 보내는 쪽에서 닫는 패턴을 추천한다.
프로젝트로 완성하는 Go 프로그래밍(Go Programming Blueprints Second Edition) 의 chat 예제를 보면 client 가 종료되어도 client 가 가지고 있는 채널을 닫지 않는 에러가 있어 이를 간단히 수정했다.
client.go 의 client struct 에 필요없는 room 변수는 삭제했다. (나중에 필요하면 추가할지도)
대신 read 메소드에 root 의 forward channel 을 인자로 받는다.
type client struct {
socket *websocket.Conn
send chan []byte
}
read 메소드에는 socket close 하는 부분이 있는데 socket 에러가 나면 read 가 에러가 날 것이고 이 때 defer 로 channel 닫는 부분을 추가한다. write 메소드에는 중복을 피하기 위해서 channel 닫는 부분을 넣지 않았다.
func (c *client) read(forward chan<- []byte) {
defer func() {
c.socket.Close()
log.Println("Client socker read closed.")
close(c.send)
log.Println("Client send channel closed.")
}()
for {
_, msg, err := c.socket.ReadMessage()
if err != nil {
log.Println("Client Socket ReadMaessage error")
return
}
forward <- msg
}
}
root.go 에서는 쓰이고 있지는 않지만 client 에 leave 할 경우 기존 client channel 닫는 소스 대신 client socket 을 닫는 부분으로 변경했다. range 로 모든 client channel 에 값을 보내고 있기 때문에 여기는 보내는 channel 이지만 channel 을 닫기가 어렵다. 그래서 client 에서 channel 을 닫는 로직을 추가했다.
root struct 에서 생성한 forward, join, leave channel 은 처리하지 않았다. 나중에 room 에 client 가 아무도 없을 때의 로직에 추가되면 그 때 닫는 부분을 추가해야 한다.
func (r *room) run() {
go func() {
for {
select {
case client := <-r.join:
r.clients[client] = true
case client := <-r.leave:
delete(r.clients, client)
client.socket.Close()
case msg := <-r.forward:
for client := range r.clients {
client.send <- msg
}
}
}
}()
}