{"record":{"id":"20a9a3dff15ad19e","repo":"fyne-io/fyne","slug":"async-misuse-of-unbounded-channel-in-was-close-20a9a3","errorCode":null,"errorMessage":"async: misuse of unbounded channel, In() was closed","messagePattern":"async: misuse of unbounded channel, In\\(\\) was closed","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/async/chan_struct.go","lineNumber":47,"sourceCode":"// to the channel.\nfunc (ch *UnboundedStructChan) In() chan<- struct{} { return ch.in }\n\n// Out returns a receive-only channel that can be used to receive\n// values from the channel.\nfunc (ch *UnboundedStructChan) Out() <-chan struct{} { return ch.out }\n\n// Close closes the channel.\nfunc (ch *UnboundedStructChan) Close() { ch.close <- struct{}{} }\n\nfunc (ch *UnboundedStructChan) processing() {\n\tfor {\n\t\tselect {\n\t\tcase _, ok := <-ch.in:\n\t\t\tif !ok {\n\t\t\t\t// We don't want the input channel be accidentally closed\n\t\t\t\t// via close() instead of Close(). If that happens, it is\n\t\t\t\t// a misuse, do a panic as warning.\n\t\t\t\tpanic(\"async: misuse of unbounded channel, In() was closed\")\n\t\t\t}\n\t\t\tch.n++\n\t\tcase <-ch.close:\n\t\t\tch.closed()\n\t\t\treturn\n\t\t}\n\t\tfor ch.n > 0 {\n\t\t\tselect {\n\t\t\tcase ch.out <- struct{}{}:\n\t\t\t\tch.n--\n\t\t\tcase _, ok := <-ch.in:\n\t\t\t\tif !ok {\n\t\t\t\t\t// We don't want the input channel be accidentally closed\n\t\t\t\t\t// via close() instead of Close(). If that happens, it is\n\t\t\t\t\t// a misuse, do a panic as warning.\n\t\t\t\t\tpanic(\"async: misuse of unbounded channel, In() was closed\")\n\t\t\t\t}\n\t\t\t\tch.n++","sourceCodeStart":29,"sourceCodeEnd":65,"githubUrl":"https://github.com/fyne-io/fyne/blob/8860ee95c356385effa5a7be51adb11c6be9d2b6/internal/async/chan_struct.go#L29-L65","documentation":"UnboundedStructChan is the struct{} signal-channel variant of the unbounded queue (event counting). It has the same contract: terminate with Close(), never close(In()). The outer select of its processing loop treats ok==false from ch.in as misuse and panics, mirroring UnboundedChan.","triggerScenarios":"Calling close() on the channel returned from a UnboundedStructChan's In() - the outer select observes the closure and panics on the processing goroutine, killing the process.","commonSituations":"Code ported from UnboundedChan patterns that closed the input; signal fan-out helpers that close their outbound channel when the source ends; tests that close producer channels in cleanup.","solutions":["Replace close(ch.In()) with ch.Close()","If producers finish at different times, count them and Close() once at the end","Search for close( on any channel from In() in the signal path"],"exampleFix":"// before\nch := async.NewUnboundedStructChan()\nfor i := 0; i < n; i++ { ch.In() <- struct{}{} }\nclose(ch.In())\n\n// after\nch := async.NewUnboundedStructChan()\nfor i := 0; i < n; i++ { ch.In() <- struct{}{} }\nch.Close()","handlingStrategy":"validation","validationCode":"// Containment: wrap the struct-signal channel behind methods with no close.\ntype signal struct{ ch *async.UnboundedStructChan }\n\nfunc (s signal) Emit()      { s.ch.In() <- struct{}{} }\nfunc (s signal) Subscribe() <-chan struct{} { return s.ch.Out() }\nfunc (s signal) Close()     { s.ch.Close() } // sole owner closes","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never close a signal channel returned from In(); emit-and-stop is the correct pattern","Keep exactly one owner responsible for Close() (usually the driver/event-loop owner)","Review test cleanup code that closes producer channels by habit"],"tags":["go","concurrency","channels","api-misuse","async","internal"],"backgroundTag":null,"analyzedSha":"8860ee95c356385effa5a7be51adb11c6be9d2b6","analyzedAt":"2026-08-15T22:01:51.624Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}