Deadlocks in Go: the dark side of concurrency (2021)
craig-wood.com
craig-wood.com
Go just shows the case as for rust: the louder you shout safety, the less safety you get.
However, that's not what we should have in mind when designing software. The Unix IPC model of file descriptors combined with select syscall (and its successors) was a powerful abstraction by itself, and Go just brought it to the single-program level - instead of pipes, you have channels, and instead of select syscall you have select{} keyword.
The problem is that we're still stuck, mentally, with the C model of threads and shared memory. You can use Go channels as locks and still write C code dressed up as Go. But in my experience, the most powerful design in Go is separating work into goroutines, each of which does one thing (and does it well :), which communicate via channels to achieve a common goal. Preferably, exposing the interface in a simple way, not requiring the user to be aware of them at all. Sort of like Actor model, but with channels instead of explicit addresses.
As long as I've followed this way of thinking, I've rarely had an issue with deadlocks at all.
There are definitely coding patterns that can help with deadlocks, like unbounded queues and avoiding cases where you are waiting on events from multiple sources in one task. But I haven't heard of a comprehensive way to solve them completely.