I dug into your example and I think I see two problems. The first problem is that `S` gets allocated in the tiny allocator, and so its lifetime may get batched together with something long-lived. This creates some inconsistent behaviors in the non-Goexit case. (I have an experimental debug mode in the works that will help flag such issues in the future.) Making `S` bigger or contain a pointer fixes this.
The second problem is that the variable `s` stays live until after the condition, since it's used by Fprint. The compiler doesn't understand that `Goexit` exits the function, because it's just a normal function implemented in the runtime. Therefore, when the GC is executed from Goexit, `s` is actually live on the stack still, so the runtime.GC calls inside the defer fail to reclaim it. To prove this point: put a `return` after `runtime.Goexit`. For me, this makes the program consistently call both AddCleanup callbacks. On Wednesday, April 9, 2025 at 8:55:41 AM UTC-4 Yaroslav Brustinov wrote: > Thanks for the explanation! > I ended up ignoring cleanup checks if test failed (=> runtime.Goexit() is > called) > IMHO it's somewhat counter-intuitive that after calling runtime.Goexit() > something is still held... > > On Friday, April 4, 2025 at 7:18:32 AM UTC+3 Robert Engels wrote: > >> And as Sean pointed out, the reason it is never run is that the object >> being cleaned up is still referenced (I didn’t verify this) >> >> On Apr 3, 2025, at 5:28 PM, Yaroslav Brustinov <y.bru...@gmail.com> >> wrote: >> >> >> >> I was sure the doc means the case when a program exits *too quickly,* >> before cleanup has a chance to run, or am I wrong? >> Otherwise, why need to specify "not guaranteed to run before program >> exit."? It obviously can't run after program exits :) >> >> In the code example above, I can wait indefinitely for cleanup to run, it >> will not trigger: >> >> for range 20 { // <-- can increase this arbitrary >> runtime.GC() >> if releaseFlag.Load() { >> fmt.Println("released, cond:", goexit) >> return >> } >> time.Sleep(10 * time.Millisecond) >> } >> >> >> On Thursday, April 3, 2025 at 8:50:24 PM UTC+3 robert engels wrote: >> >>> It states this in the API docs: >>> >>> "The cleanup(arg) call is not always guaranteed to run; in particular >>> it is not guaranteed to run before program exit." >>> >>> On Apr 3, 2025, at 10:23 AM, Yaroslav Brustinov <y.bru...@gmail.com> >>> wrote: >>> >>> Hello, experts. >>> >>> Given following code as example: >>> >>> package main >>> >>> import ( >>> "fmt" >>> "io" >>> "runtime" >>> "sync/atomic" >>> "time" >>> ) >>> >>> type S struct { >>> foo int >>> } >>> >>> var released1 atomic.Bool >>> var released2 atomic.Bool >>> >>> func releaseCb(releaseFlag *atomic.Bool) { >>> fmt.Println("release CB") >>> releaseFlag.Store(true) >>> } >>> >>> func deferredCheckRelease(goexit bool, releaseFlag *atomic.Bool) { >>> for range 20 { >>> runtime.GC() >>> if releaseFlag.Load() { >>> fmt.Println("released, cond:", goexit) >>> return >>> } >>> time.Sleep(10 * time.Millisecond) >>> } >>> fmt.Println("not released, cond:", goexit) >>> } >>> >>> func f(goexit bool, releaseFlag *atomic.Bool) { >>> defer deferredCheckRelease(goexit, releaseFlag) >>> s := &S{1} >>> runtime.AddCleanup(s, releaseCb, releaseFlag) >>> if goexit { >>> // releaseFlag will not be set >>> runtime.Goexit() >>> } >>> // releaseFlag will be set >>> fmt.Fprint(io.Discard, s) >>> } >>> >>> func main() { >>> go f(true, &released1) >>> go f(false, &released2) >>> time.Sleep(time.Second) >>> } >>> >>> As comment inside mentions, release flag is not set if runtime.Goexit() >>> executed. >>> >>> Maybe it's because runtime.AddCleanup is not "guaranteed" to run? >>> In such case would be great to clarify (in docs?) in which cases it >>> might not run. >>> People might rely on the callback... >>> >>> >>> >>> >>> -- >>> You received this message because you are subscribed to the Google >>> Groups "golang-nuts" group. >>> To unsubscribe from this group and stop receiving emails from it, send >>> an email to golang-nuts...@googlegroups.com. >>> To view this discussion visit >>> https://groups.google.com/d/msgid/golang-nuts/a0df1e83-2e25-4de2-89b5-25de4e892670n%40googlegroups.com >>> >>> <https://groups.google.com/d/msgid/golang-nuts/a0df1e83-2e25-4de2-89b5-25de4e892670n%40googlegroups.com?utm_medium=email&utm_source=footer> >>> . >>> >>> >>> -- >> You received this message because you are subscribed to the Google Groups >> "golang-nuts" group. >> To unsubscribe from this group and stop receiving emails from it, send an >> email to golang-nuts...@googlegroups.com. >> >> To view this discussion visit >> https://groups.google.com/d/msgid/golang-nuts/d4f12d3c-bf22-4710-bafd-2df9429cb727n%40googlegroups.com >> >> <https://groups.google.com/d/msgid/golang-nuts/d4f12d3c-bf22-4710-bafd-2df9429cb727n%40googlegroups.com?utm_medium=email&utm_source=footer> >> . >> >> -- You received this message because you are subscribed to the Google Groups "golang-nuts" group. To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts+unsubscr...@googlegroups.com. To view this discussion visit https://groups.google.com/d/msgid/golang-nuts/1ce705b2-3417-44d2-a56e-5f3a6d8f6ab4n%40googlegroups.com.