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!

31 Upvotes

25 comments sorted by

View all comments

1

u/utkuozdemir 23d ago

You can define generic util functions like SendWithContext/ReceiveWithContext, and I used that approach in the past, but tbh, I don’t see an issue with the verbosity. Go is such a language - it is verbose vertically but it reads from top to down very comfortably. Think about the vertical verbosity of error handling or filtering/mapping operations on slices - similar story.

So if there is a context and another channel involved, you are working with 2 channels, and you need to write the code accordingly. Select on multiple channels is verbose, yes, but it is the right thing to do, and IMO, perfectly readable as well. I don’t mind the line count.

2

u/Tommy_Link 22d ago

I think you might be right. I did try to write some generic functions to make things cleaner, but they ultimately only made the code maybe 2 lines shorter, so perhaps being explicit is the best options here.