{"record":{"id":"cccf5790f34ae792","repo":"ginuerzh/gost","slug":"listener-has-been-closed","errorCode":null,"errorMessage":"listener has been closed","messagePattern":"listener has been closed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"info","filePath":"tuntap.go","lineNumber":115,"sourceCode":"\nfunc (l *tunListener) Accept() (net.Conn, error) {\n\tselect {\n\tcase conn := <-l.conns:\n\t\treturn conn, nil\n\tcase <-l.closed:\n\t}\n\n\treturn nil, errors.New(\"accept on closed listener\")\n}\n\nfunc (l *tunListener) Addr() net.Addr {\n\treturn l.addr\n}\n\nfunc (l *tunListener) Close() error {\n\tselect {\n\tcase <-l.closed:\n\t\treturn errors.New(\"listener has been closed\")\n\tdefault:\n\t\tclose(l.closed)\n\t}\n\treturn nil\n}\n\ntype tunHandler struct {\n\toptions *HandlerOptions\n\troutes  sync.Map\n\tchExit  chan struct{}\n}\n\n// TunHandler creates a handler for tun tunnel.\nfunc TunHandler(opts ...HandlerOption) Handler {\n\th := &tunHandler{\n\t\toptions: &HandlerOptions{},\n\t\tchExit:  make(chan struct{}, 1),\n\t}","sourceCodeStart":97,"sourceCodeEnd":133,"githubUrl":"https://github.com/ginuerzh/gost/blob/a33fdbf4c98034f4bfeeaea9868909822b9c526d/tuntap.go#L97-L133","documentation":"tunListener.Close uses a `closed` channel as a once-guard: if the channel is already closed, it returns \"listener has been closed\" instead of nil. This deviates from the usual net.Listener idiom where Close is idempotent, so a double Close surfaces as an error.","triggerScenarios":"Calling Close() twice on a tunListener, e.g. both an explicit shutdown path and a defer ln.Close(); Close after an external component already closed the listener.","commonSituations":"defer + explicit close patterns; restart/reload logic that closes listeners in more than one place; tests calling t.Cleanup close plus manual close.","solutions":["Call Close only once per listener lifecycle; rely on the closed channel to signal other goroutines","Ignore this specific error in cleanup paths (treat as success, like net.ErrClosed handling)","Wrap Close in a sync.Once in your own code to guarantee idempotency"],"exampleFix":"// before\ndefer ln.Close()\n...\nif err := ln.Close(); err != nil { log.Fatal(err) }\n// after\nvar closeOnce sync.Once\ncloseFn := func() { closeOnce.Do(func() { ln.Close() }) }\ndefer closeFn()\n...","handlingStrategy":"validation","validationCode":"var closed atomic.Bool\n// before calling Close:\nif closed.CompareAndSwap(false, true) {\n    if err := ln.Close(); err != nil { log.Printf(\"close: %v\", err) }\n}","typeGuard":null,"tryCatchPattern":"if err := ln.Close(); err != nil {\n    if strings.Contains(err.Error(), \"has been closed\") {\n        return nil // idempotent close: treat as success\n    }\n    return err\n}","preventionTips":["Wrap Close in sync.Once to make it idempotent at the call site","Avoid defer ln.Close() plus an explicit Close on the same listener","Ignore \"has been closed\" errors in cleanup/teardown paths"],"tags":["tun","listener","close","idempotency"],"backgroundTag":"listener-already-closed","analyzedSha":"a33fdbf4c98034f4bfeeaea9868909822b9c526d","analyzedAt":"2026-09-02T22:15:54.506Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-10T02:17:09.455Z"}