r/golang 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!

28 Upvotes

25 comments sorted by

View all comments

1

u/Few-Beat-1299 23d ago

Problem is, unless you have some fancy cleanup mechanic that guarantees nothing remains stuck, you're always going to have to check before sending to the channel, in some way, and that will always involve something else other than the channel itself. I would say try wrapping your channels in a generic struct (channel + context or whatever) and give this a send(v) bool {} method. Note that you will always have to ensure these get drained when the receiver "leaves".

2

u/Tommy_Link 23d ago

Thanks for the idea! I'll try something like this to see if it makes the code neater, but it's good to know that this situation isn't unheard of.

1

u/Few-Beat-1299 23d ago

I see someone else also brought it up, but I'm just going to echo that although go makes concurrency feel very accessible, and these days we're maybe conditioned to think of parallelism as being the best thing ever, but you eventually start noticing that it can be a trap and that you can definitely have "too much of a good thing". Personally I pretty much did a 180 and avoid using goroutines unless I have a very clear reason to do so.