Channels exhibit the following properties:

Send to a nil channel blocks forever

package main

func main() {
        var ch chan bool
        ch <- true // deadlocks because ch is nil
}

Uninitialized value of a channel is nil so the above program blocks forever.

There's no valid use case for that so it's always a bug. Don't do it.

Receive from a nil channel blocks forever

package main

import "fmt"

func main() {
        var ch chan bool
        fmt.Printf("Value received from ch is: %v\\n", <-ch) // deadlock because c is nil
}

Similarly, receive from nil channel blocks forever and it's always a bug. Don't do it.

Send to a closed channel panics

package main

import (
	"fmt"
	"time"
)

func main() {
	var ch = make(chan int, 100)
	go func() {
		ch <- 1
		time.Sleep(time.Second)
		close(ch)
		ch <- 1
	}()
	for i := range ch {
		fmt.Printf("i: %d\\n", i)
	}
}

Output:

i: 1
panic: send on closed channel

goroutine 5 [running]:
main.main.func1(0x452000, 0xc99)
	/tmp/sandbox307976305/main.go:14 +0xa0
created by main.main
	/tmp/sandbox307976305/main.go:10 +0x60

You should architecture your programs so that one sender controls the lifetime of a channel.

This rule emphasizes that: if there's only one channel sender then there's no problem making sure that you never write to a closed channel.

If you have multiple senders then it becomes hard: if one sender closes a channel, how other senders are supposed to not crash?

Instead of trying to find solution to the above problem, re-architect your code so that there's only one sender that controls the lifetime of a channel.