Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/go-concurrency-patterns.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -110,7 +110,7 @@ func process(ctx context.Context) error {

**Owner**: go-architecture-assistant
**Applies when**: a Go file calls `close(ch)` on a channel that was passed in as a function parameter from elsewhere — i.e. closed by a consumer/receiver rather than by the goroutine that produces values into it.
**Enforcement**: judgment (ast-grep follow-up: `close(X)` where `X` is a parameter type `chan T` or `chan<- T`; the agent rules in whether the function is the producer or consumer based on whether it sends into `X`)
**Enforcement**: `rules/go/channel-closed-by-sender-only.yml` flags every `close($X)` call. The agent decides per-finding whether the enclosing function is the producer (sends into the channel — close correct, often `defer close(ch)`) or a consumer (only receives — `close(ch)` is the bug), and clears multi-producer cases routed through a dedicated closer goroutine + sync.WaitGroup. The producer-vs-consumer judgment needs reading the function body's send/receive direction — exactly the cross-context reasoning ast-grep can't do reliably.
**Why**: Closing a channel from the receiver side is a textbook race — the sender may still be writing when the close happens, producing `send on closed channel` panic. The Go convention is: **the producer owns the channel and is the only one allowed to close it.** Receivers learn of "no more values" via `for v := range ch` or the `comma-ok` idiom (`v, ok := <-ch`), never by closing themselves. Multi-producer cases use `sync.WaitGroup` + a single dedicated closer goroutine, not concurrent closes (which also panic).

#### Bad
Expand Down
27 changes: 27 additions & 0 deletions rules/go/channel-closed-by-sender-only.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
id: go-concurrency/channel-closed-by-sender-only
language: go
severity: error
message: |
`close(ch)` call needs a producer-only owner. Closing a channel
from the receiver side races against any still-pending send and
panics `send on closed channel`. The Go convention is: producer
owns the channel and is the only one allowed to close it;
receivers use `for v := range ch` or the comma-ok idiom.
The agent decides per-finding whether the enclosing function is
the producer (sends into the channel — close is correct, often
`defer close(ch)`) or a consumer (only receives — `close(ch)` is
the bug). The agent also clears multi-producer cases that route
through a single dedicated closer goroutine + sync.WaitGroup.
See docs/go-concurrency-patterns.md
(RULE go-concurrency/channel-closed-by-sender-only).
rule:
# Match every `close($X)` call. The producer-vs-consumer
# adjudication needs reading the enclosing function body's
# send/receive direction on $X — exactly the cross-context
# reasoning ast-grep can't do reliably.
pattern: 'close($X)'
ignores:
- "**/*_test.go"
- "vendor/**"
- "**/vendor/**"
- "**/mocks/**"
2 changes: 1 addition & 1 deletion rules/index.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -228,7 +228,7 @@
"anchor": "go-concurrency/channel-closed-by-sender-only",
"applies_when": "a Go file calls `close(ch)` on a channel that was passed in as a function parameter from elsewhere — i.e. closed by a consumer/receiver rather than by the goroutine that produces values into it.",
"doc_path": "docs/go-concurrency-patterns.md",
"enforcement": "judgment (ast-grep follow-up: `close(X)` where `X` is a parameter type `chan T` or `chan<- T`; the agent rules in whether the function is the producer or consumer based on whether it sends into `X`)",
"enforcement": "`rules/go/channel-closed-by-sender-only.yml` flags every `close($X)` call. The agent decides per-finding whether the enclosing function is the producer (sends into the channel — close correct, often `defer close(ch)`) or a consumer (only receives — `close(ch)` is the bug), and clears multi-producer cases routed through a dedicated closer goroutine + sync.WaitGroup. The producer-vs-consumer judgment needs reading the function body's send/receive direction — exactly the cross-context reasoning ast-grep can't do reliably.",
"id": "go-concurrency/channel-closed-by-sender-only",
"level": "MUST",
"owner": "go-architecture-assistant"
Expand Down
Loading