You can check the Go source and easily find out: it happens in this function, which is called in various places where the program might enter a deadlock state.
The relevant part is that the runtime gets the number of open OS threads, and checks how many of them are actually running code. There are a few more checks, but that's basically it. Whenever you run a blocking operation - such as as locking a mutex when it's already been locked elsewhere, or receiving from an empty channel - the scheduler will try to make the thread do the work of another goroutine. If none can be found, it enters an idle state.
Basically, the scheduler always tries to find code that is waiting to be ran. If none can be found, then it's a deadlock situation.
This of course excludes cases of, i.e., goroutines which are running time.Sleep
, which although are "idling", there's a thread actively checking when they are ready to be run. In other words, they are not dependent on other parts of the program to start being "runnable" again (such as is the case for mutexes).