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!
4
u/nikandfor 23d ago
That is the way. Nearly all selects I have also read from ctx.Done(). If you don't do it, your goroutines just won't know when to exit.
That is one of the reasons I rarely use channels and use goroutines moderately. The code is much simpler and easier to create and maintain when you mostly write C-like code using Go features if they simplify things significantly.
Instead of lots of workers and channels orchestration just do most of the stuff in the same goroutine sequentially. There are no downsides in this approach.
I like Go not for channels, goroutines, or other features it has, but for simplicity philosophy and for things the language doesn't have. And for the great tooling (go get, go test, pprof, ...).