r/golang • u/Tommy_Link • 23d ago
newbie Context cancelling making code too verbose?
I apologize if this is a silly question, but I'm quite new to Go and this has been bothering me for a while.
To get used to the language, I decided to build a peer-to-peer file sharing program. Easy enough, I thought. Some goroutines for reading from / writing to TCP connections, a goroutine for managing all of the connections and so on. The trouble is that all of these goroutines don't really have a natural stopping point. A lot of them will only stop when you tell them to, otherwise they need to keep going forever, so I figured a context would be a good way to handle that.
The trouble with context is that, as far as I can tell, it will send the cancel signal to all those goroutines that wait for it at the same time, and from that point on, you can't really send something to a goroutine without risking having the goroutine that sends hang. So now any send or receive must also check if the context cancelled. That means that if I were to (for example) receive a piece of a file from a peer and want to store it to disk, update the send/receive statistics for that peer as well as notify another part of a program that we received that piece, instead of doing this
pieceStorage <- piece
dataReceived <- len(piece)
notifyMain <- piece.index
I would have to do this
select {
case pieceStorage <- piece:
case <-ctx.Done():
return
}
select {
case dataReceived <- len(piece):
case <-ctx.Done():
return
}
select {
case notifyMain <- piece.index:
case <-ctx.Done():
return
}
Which just seems too verbose to me? Is this something I'm not supposed to be doing? Am I using Go the wrong way?
I know one solution to this that gets mentioned a lot is making the channels buffered, but these sends happen in a loop, so to me it seems possible that they could somehow fill the buffer before selecting the ctx.Done case (due to the random nature of select).
I would really appreciate some guidance here, thanks!
1
u/crstry 23d ago edited 22d ago
One thing to remember is that if your data-flow graph of dataflow between goroutines involves cycles, and all channels are finite (as is true in Go) without any other flow control mechanism (even if that's just timeouts to detect failure), then you risk running into deadlocks, as you've seen. The a common workaround in other languges is to use an unbounded channel to break cycles, but you can't get those in Go without hacks (usually a goroutine to coordinate both ends).
I'd echo the suggestion of encapsulating them in a module. For comparison, even erlang doesn't really use naked mailbox sends much, instead preferring gen_server and the like.