🪟 Windows TippsThe Gemini desktop app is now available for Windows(11.09.2026 um 17:06 Uhr)
🪟 Windows TippsHeader and Footer not showing in Excel(14.09.2026 um 22:43 Uhr)
🕵️ SicherheitslückenBurn Out, Or Fade Away(14.09.2026 um 14:25 Uhr)
🪟 Windows TippsKB5129194 Windows 11 26H1 Out of Band Update - Deskmodder.de(14.09.2026 um 19:25 Uhr)
🪟 Windows TippsThe Gemini desktop app is now available for Windows(11.09.2026 um 17:06 Uhr)
🪟 Windows TippsHeader and Footer not showing in Excel(14.09.2026 um 22:43 Uhr)
🕵️ SicherheitslückenBurn Out, Or Fade Away(14.09.2026 um 14:25 Uhr)
🪟 Windows TippsKB5129194 Windows 11 26H1 Out of Band Update - Deskmodder.de(14.09.2026 um 19:25 Uhr)

🔧 Programmierung 🕛 vor 2 Monaten 7 Min Lesezeit
0

Share Memory by Communicating: When a Channel Beats a Mutex in Go

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht


  • Book: — the companion book in the Thinking in Go series


  • My project: — an IDE for developers who ship with Claude Code and other AI coding tools


  • Me:






You've read the proverb. It's in the Go documentation, on

stickers, in half the conference talks: "Do not communicate by

sharing memory; instead, share memory by communicating."



Then you open a real Go codebase and every concurrent type in it

is a struct with a sync.Mutex on top. The proverb says channels.

The code says mutex. Somebody is wrong, and it's tempting to

assume it's the code.



It isn't. The proverb is a design hint, not a lint rule. Both

tools are correct Go. The skill is knowing which problem you have

in front of you, because they are two different problems wearing

similar clothes. One is about guarding a piece of state that

several goroutines touch. The other is about handing a piece of

state from one goroutine to the next so only one owns it at a

time.





The two problems, stated plainly



A mutex guards state. The data stays in one place. Many

goroutines reach into that place, one at a time, do their thing,

and leave. Nobody owns the data; they take turns.



A channel transfers ownership. The data moves. A goroutine

builds a value, sends it, and then must never touch it again. The

receiver now owns it, exclusively, until it sends the value

somewhere else. There is no shared access because at any moment

exactly one goroutine holds the value.



That second sentence is the whole proverb. "Share memory by

communicating" means: instead of parking data in a shared box and

locking the box, pass the data down a channel so ownership moves

with it. The synchronization is the send and the receive. You

never lock because you never share.





The mutex version



Here is a counter. Many goroutines increment it. This is the

guard-state problem, and a mutex is the right answer.




CODE
type Counter struct {
mu sync.Mutex
n int
}

func (c *Counter) Inc() {
c.mu.Lock()
c.n++
c.mu.Unlock()
}

func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.n
}






Nothing moves. c.n lives in the Counter for the whole

program. Every goroutine that calls Inc reaches into the same

integer and takes its turn under the lock. Pointer receiver, so

every caller shares the one mutex. This is small, obvious, and

fast.



Trying to force a channel here makes the code worse. You'd stand

up a goroutine that owns the counter, plus a request channel, plus

a reply channel for Value. More moving parts to protect a single

int++. The proverb doesn't ask you to do that.





The channel version



Now change the problem. A worker pool processes jobs. Each job is

owned by exactly one worker while it runs, then the result moves

on. Nothing is shared. This is the transfer-ownership problem, and

a channel fits it exactly.




CODE
type Job struct {
ID int
Data []byte
}

type Result struct {
ID int
Sum int
}

func worker(jobs <-chan Job, out chan<- Result) {
for j := range jobs {
sum := 0
for _, b := range j.Data {
sum += int(b)
}
out <- Result{ID: j.ID, Sum: sum}
}
}






When worker receives a Job off jobs, it owns that job. No

other goroutine has a reference to it. It computes, builds a

Result, and sends it away. After the send, the worker forgets

the result and loops for the next job. No lock appears anywhere,

because at no point do two goroutines hold the same value.



The wiring shows the ownership handoff:




CODE
func run(datas [][]byte) []Result {
jobs := make(chan Job)
out := make(chan Result)

var wg sync.WaitGroup
for i := 0; i < 4; i++ {
wg.Add(1)
go func() {
defer wg.Done()
worker(jobs, out)
}()
}

go func() {
for id, d := range datas {
jobs <- Job{ID: id, Data: d}
}
close(jobs)
}()

go func() {
wg.Wait()
close(out)
}()

var results []Result
for r := range out {
results = append(results, r)
}
return results
}






Notice there is still a sync.WaitGroup in there. That's the

point of the proverb being a hint and not a law: you use a

channel for the data flow, where ownership actually moves, and a

WaitGroup for the "are all workers done" signal, where nothing

moves and a counter is the honest tool. Mixing them is ordinary Go.





The ownership rule that keeps you honest



The line that separates a correct channel design from a subtle

data race is this: after you send a value on a channel, treat it

as gone.




CODE
job := Job{ID: 1, Data: buf}
jobs <- job
// buf is now owned by whoever received job.
// Writing to buf here is a data race.
buf[0] = 0x00 // BUG






Job.Data is a slice, which is a pointer to a backing array. The

send copied the slice header, not the bytes. Sender and receiver

now point at the same array. The channel gave you the

synchronization to hand it off, and then you reached back in and

mutated shared memory anyway. The race detector will find this;

go run -race on the real thing prints a DATA RACE report.



The mental discipline is simple. A send is a goodbye. If you need

the value after sending it, copy it before the send, or don't send

the original. This is exactly the guarantee a mutex does not give

you and does not need to: under a mutex, everyone expects to share,

so everyone locks.





A quick way to pick



Ask one question about the data: does it stay put, or does it move?





  • Stays put, many readers/writers take turns — mutex. Counters,
    caches, a config struct reloaded in place, connection pools. The
    data has a home and goroutines visit it.


  • Moves from producer to consumer, one owner at a time
    channel. Pipelines, worker pools, event fan-out, request/reply
    between goroutines. The data has a journey and ownership travels
    with it.



Two more practical tie-breakers when it's genuinely ambiguous:




  • If the shared thing is a single field you read and write in
    nanoseconds, a mutex is almost always simpler and faster. Don't
    build a goroutine and two channels to protect an int.

  • If you find yourself locking a mutex, kicking off work, and
    unlocking in a different function than you locked in, you're
    probably fighting the guard-state model. That shape usually wants
    ownership transfer instead.





Where people misread the proverb



The proverb is not "channels good, mutex bad." The Go standard

library is full of mutexes, and sync.Mutex exists precisely

because guarding state is a real, common, correct pattern. The

proverb is aimed at a specific mistake: reaching for a shared

variable plus a lock when what you actually have is a pipeline,

and then spending your afternoons debugging lock ordering and

missed unlocks that a channel would have made structurally

impossible.



There's also sync/atomic for the narrow case of a single word

of state (a flag, a counter) where even a mutex is more machinery

than you need:




CODE
var closed atomic.Bool

func markClosed() { closed.Store(true) }
func isClosed() bool { return closed.Load() }






Same guard-state family as the mutex, one instruction cheaper, and

correct only because a single boolean is exactly what atomic

covers. Reach past it the moment you need to guard more than one

field together.






The takeaway



Go gives you both primitives because both problems exist. The bugs

come from solving one with the tool meant for the other.






Concurrency is where Go's small surface area hides a lot of depth:

the memory model, what a send actually guarantees, how the

scheduler parks and wakes goroutines. The Complete Guide to Go

Programming
works through those runtime details, so the choice

between a channel and a mutex stops being a coin flip. Hexagonal

Architecture in Go
is about the other half — keeping this

decision at the right boundary, so your domain code isn't the

place where lock ordering and channel plumbing leak in.



Thinking in Go — the 2-book series on Go programming and hexagonal architecture

Vollständiger Original-Bericht
Ausführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
1 Quelle
The Gemini desktop app is now available for Windows
1 Quelle
Header and Footer not showing in Excel
1 Quelle
Burn Out, Or Fade Away
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Share Memory by Communicating: When a Channel Beats a Mutex in Go

Thematisch verwandte Begriffe: Share, Memory, Communicating, When · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...