Time.After adjustment / GC

61 views
Skip to first unread message

Slawomir Pryczek

unread,
Feb 26, 2026, 10:07:37 AM (15 hours ago) Feb 26
to golang-nuts
Hi Everyone. In my code i'm making eg. 8 http requests, in fact i need half of it, but it's good to have these missing ones if i'm able to get it in short timespan. For example having 4 requests in 10 ms is good, 8 requests in 9ms is great while waiting for full timeout (eg. 50ms) to just get these missing 4 is counter productive. So it's like "get half of the requests ASAP+5ms or get all in 50ms max"

So i'm implementing a timeout using select / time.After. When i get 4 requests i'm just creating another time.After and replace variable with it, attaching code

----------------------------------------
total_requests := len(j.id)
out := make([]httpjob_response, 0, total_requests)
timeout := time.After(time.Duration(maxTimeMS) * time.Millisecond)
....
should_run := true
var error_timeout Status = ERR_TIMEOUT
for i := 0; should_run && i < total_requests; i++ {

  // here we have special support for case where we already got minResult results
  // so we can wait a little more (eg. 5 ms) to try to get the last result, or just finish
  if i == minResults {
    timeout = time.After(time.Duration(5) * time.Millisecond)
    error_timeout = ERR_SKIPPED // if we can't get this request in time, treat it as skipped as we don't need it
  }

  select {
  case v := <-j.ch:
    out = append(out, v)
    case <-timeout:
      append_missing(error_timeout)
      should_run = false
    }
  }
  append_missing(ERR_SKIPPED)
----------------------------------------

Now claude ai is insisting that this time.After (which creates a channel) is getting properly garbage collected. Not sure how, when (as per spec) the channel with pending write should linger indefinitely as long as it isn't read from. Moreover "only the sender should close a channel, and only when it's done sending", during that potential GC this channel will get closed.

If the channel is closed / GC'd with blocking send, how that works. If not, what's some better way of doing that?

"As of Go 1.23, the channel is synchronous (unbuffered, capacity 0)" so if the write occurs before GC, and there is no one to read from the channel, is it still properly collected?



Brian Candler

unread,
Feb 26, 2026, 11:47:04 AM (13 hours ago) Feb 26
to golang-nuts
https://go.dev/wiki/Go123Timer

"Unstopped timers and tickers that are no longer referenced can be garbage collected. Before Go 1.23, unstopped timers could not be garbage collected until the timer went off"

Slawomir Pryczek

unread,
Feb 26, 2026, 3:03:04 PM (10 hours ago) Feb 26
to golang-nuts
Yes thanks for the answer, also found this one however my concern is about GC'ing timers (stopped or unstopped) after they fired and there is nothing on the other side to "receive" the message (there is only writer [with or without a pending write] AND [no reader] )


Would be great if i could just do time.After and never use the result, this case i want to confirm

Jason E. Aten

unread,
12:06 AM (1 hour ago) 12:06 AM
to golang-nuts
Slawomir Pryczek wrote:
Yes thanks for the answer, also found this one however my concern is about GC'ing timers (stopped or unstopped) after they fired and there is nothing on the other side to "receive" the message (there is only writer [with or without a pending write] AND [no reader] )

Would be great if i could just do time.After and never use the result, this case i want to confirm

short answer: Using Go 1.23 or greater, yes, you will be fine.  

longer answer: This is just garbage collection. If you overwrite "timeout", the old timer channel that was referenced now has no more references, and will be garbage collected, full stop. It does not matter what its fired or non-fired state is. 

more than you want to know probably:  Delivery on the one-way <-chan time.Time channel of a timer is done here in the time.sendTime() function: https://github.com/golang/go/blob/master/src/time/sleep.go#L188

Notice the default: clause in that select: the runtime will never wait on you; 

func sendTime(c any, seq uintptr, delta int64) {
// delta is how long ago the channel send was supposed to happen.
// The current time can be arbitrarily far into the future, because the runtime
// can delay a sendTime call until a goroutine tries to receive from
// the channel. Subtract delta to go back to the old time that we
// used to send.
select {
case c.(chan Time) <- Now().Add(Duration(-delta)):
default:
}
}

and >= Go 1.23, the timer channel is unbuffered. 
I'm told the runtime does some "magic" to only even try to delivery timers 
when they are ready to fire too... but you'd have to confirm that as I 
don't remember the source of that impression.
Reply all
Reply to author
Forward
0 new messages