I enjoy writing Go. Here's a fun micro-project that I did to explore what happens when you try to close a closed channel... what if I didn't want a panic to get thrown? Is it possible? Why yes it is!
safe_close.go
package main
func safeClose[T any](ch chan T) (closed bool) {
// allow panic
defer func() {
if r := recover(); r != nil {
closed = true
return
}
}()
_, chOpen := <-ch
if chOpen {
closed = true
close(ch)
} else {
closed = false
}
return
}
safe_close_test.go
package main
import (
"github.com/stretchr/testify/assert"
"testing"
)
func Test_safeClose(t *testing.T) {
type args[T any] struct {
ch chan T
}
type testCase[T any] struct {
name string
args args[T]
want bool
}
// open channel with bool in buffer
ch1 := make(chan bool, 1)
ch1 <- true
// closed channel with bool in buffer
ch2 := make(chan bool, 1)
ch2 <- false
close(ch2)
// closed channel with empty buffer
ch3 := make(chan bool, 1)
close(ch3)
tests := []testCase[bool]{
{
name: "open channel with bool in buffer",
args: args[bool]{ch1},
want: true,
},
{
name: "closed channel with bool in buffer",
args: args[bool]{ch2},
want: true,
},
{
name: "closed channel with empty buffer",
args: args[bool]{ch3},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, safeClose(tt.args.ch))
})
}
}
Then go run it....
go test .
Which returns:
=== RUN Test_safeClose
--- PASS: Test_safeClose (0.00s)
=== RUN Test_safeClose/open_channel_with_bool_in_buffer
--- PASS: Test_safeClose/open_channel_with_bool_in_buffer (0.00s)
=== RUN Test_safeClose/closed_channel_with_bool_in_buffer
--- PASS: Test_safeClose/closed_channel_with_bool_in_buffer (0.00s)
=== RUN Test_safeClose/closed_channel_with_empty_buffer
--- PASS: Test_safeClose/closed_channel_with_empty_buffer (0.00s)
PASS
Process finished with the exit code 0
Why do I need this functionality? How does this for select
statement look to you?
// wait for results
for {
select {
case <-ctx.Done(): // parent context canceled
if ctx.Err() != nil {
log.Println(ctx.Err()) // with error
}
closeEmAll(false)
return ImportedAssetDownloadOutput{} // exit out of the func
case d, chOk := <-documentResults: // documents finished!
if chOk { // open channel
dmu.Lock()
documents = d // set documents
documentsReceived.Store(true) // mark flag
dmu.Unlock()
closeAllDocuments(false)
}
case p, chOk := <-pageResults: // pages finished!
if chOk { // open channel
pmu.Lock()
pages = p // set pages
pagesReceived.Store(true) // mark flag
pmu.Unlock()
closeAllPages(false)
}
default: // when both aren't completed yet
if documentsReceived.Load() && pagesReceived.Load() { // both received
output := ImportedAssetDownloadOutput{ // structure the output
ImportedDocumentsDownloadOutput: documents,
ImportedPagesDownloadOutput: pages,
}
log.Printf("total documents = %d\ntotal pages = %d\n", len(output.Documents), len(output.Pages))
closeEmAll(true)
return output
}
}
}