The architecture of Go forces you to have 1 goroutine servicing every socket, so the number of runnable goroutines will then be at the mercy of your packet inter-arrival process.
Go provides go routines as a building block, it's up to you to use that or use something else ( reactor pattern, epoll ect ... )
https://www.usenix.org/system/files/login/articles/chen12-06...
I don't know which category epoll fits into, but it seems like it should be treated optimistically, since epoll is used for non-blocking I/O.
https://utcc.utoronto.ca/~cks/space/blog/programming/GoSched...
In epoll case, using RawConn.Read would look like this:
err := rawConn.Read(func(fd uintptr) bool {
nevents, err = syscall.EpollWait(int(fd), events[:], 0)
if nevents == 0 {
return false // try again
}
return true
})
Note that using RawConn.Read here is only necessary because epoll needs epoll_wait(2) instead of typical read(2). For ordinary file descriptors, like pipes, etc., setting them to non-blocking mode, wrapping them with os.NewFile, and using its ordinary Read/Write methods is sufficient.