Goのチャンネルはクローズされていることが受信時には判断できますが送信時にはできません。
そのため、送信可能か判断できないチャンネルに送信する場合は一工夫必要になります。
問題のあるコード
以下のImpatientPrinter
は入力された文字列を出力する機能を持っています。
ただし、1秒間入力がないと自動的に入力を閉じます。
package main
import (
"fmt"
"time"
)
type ImpatientPrinter struct {
write chan string
}
func NewImpatientPrinter() ImpatientPrinter {
w := make(chan string)
go func() {
for {
select {
case s := <-w:
fmt.Println(s)
case <-time.After(time.Second):
close(w)
return
}
}
}()
return ImpatientPrinter{
write: w,
}
}
func (p ImpatientPrinter) Write(s string) {
p.write <- s
}
func main() {
w := NewImpatientPrinter()
w.Write("Hello, playground")
time.Sleep(time.Second * 1)
w.Write("Hello, playground")
time.Sleep(time.Second * 1)
w.Write("Hello, playground")
}
このコードの問題は、チャンネルに送信可能かどうかが外部から判別できないということです。
チャンネルを閉じた後にWriteしようとすると当然ながらpanicが発生します。
チャンネルをクローズしない場合でも、チャンネルを受信しているgoroutineが存在しないためデッドロックしてしまいます。
修正したコード
受信専用チャンネルをもう一つ追加し、そのチャンネルを代わりにクローズするのがシンプルな解決方法です。
送信が不可能になった後は受信専用チャンネルの受信がブロックしないため、Writeがどのgoroutineから実行されていても、送信可能かどうかが確実に判断できます。
package main
import (
"errors"
"fmt"
"time"
)
type ImpatientPrinter struct {
write chan string
closed chan int
}
func NewImpatientPrinter() ImpatientPrinter {
w := make(chan string)
c := make(chan int)
go func() {
for {
select {
case s := <-w:
fmt.Println(s)
case <-time.After(time.Second):
close(c)
return
}
}
}()
return ImpatientPrinter{
write: w,
closed: c,
}
}
func (p ImpatientPrinter) Write(s string) error {
select {
case p.write <- s:
case <-p.closed:
return errors.New("closed")
}
return nil
}
func main() {
w := NewImpatientPrinter()
w.Write("Hello, playground")
time.Sleep(time.Second * 1)
w.Write("Hello, playground")
time.Sleep(time.Second * 1)
w.Write("Hello, playground")
}
おまけ
禁断のrecover
package main
import (
"errors"
"fmt"
"time"
)
type ImpatientPrinter struct {
write chan string
}
func NewImpatientPrinter() ImpatientPrinter {
w := make(chan string)
go func() {
for {
select {
case s := <-w:
fmt.Println(s)
case <-time.After(time.Second):
close(w)
return
}
}
}()
return ImpatientPrinter{
write: w,
}
}
func (p ImpatientPrinter) Write(s string) (e error) {
defer func() {
if r := recover(); r != nil {
e = errors.New("closed")
}
}()
p.write <- s
return nil
}
func main() {
w := NewImpatientPrinter()
w.Write("Hello, playground")
time.Sleep(time.Second * 1)
w.Write("Hello, playground")
time.Sleep(time.Second * 1)
w.Write("Hello, playground")
}