Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Go  Module 13 – Goroutines, channels and sync

sync: WaitGroup.Go, Mutex, RWMutex and Once

Wait for goroutines with WaitGroup.Go, guard shared data with Mutex and RWMutex, and run setup exactly once with sync.Once and OnceValues.

  • Intermediate
  • 15 minutes
  • Examples run with Go 1.27.1 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 6 of 15 minutes free
Free: A WaitGroup counts goroutines, and wg.Go starts and counts them · A Mutex lets one goroutine at a time into a critical section · An RWMutex lets many readers in at once, or one writer · sync.Once and OnceValues run initialisation exactly once. With a pass: 2 in-depth parts and 3 interview answers, about 9 minutes.

What you will learn

  • Use sync.WaitGroup to wait for goroutines, with WaitGroup.Go and the older Add and Done
  • Protect shared state with a Mutex, and explain why an unguarded counter loses updates
  • Choose an RWMutex for read-heavy state, and predict when readers hold it together
  • Implement lazy initialisation with sync.Once, OnceFunc, OnceValue or OnceValues
  • Find WaitGroup and lock mistakes with go vet and the race detector, and fix them

Before you start

What you’ll learn · Before you start

What you will learn

  • Use sync.WaitGroup to wait for goroutines, with WaitGroup.Go and the older Add and Done
  • Protect shared state with a Mutex, and explain why an unguarded counter loses updates
  • Choose an RWMutex for read-heavy state, and predict when readers hold it together
  • Implement lazy initialisation with sync.Once, OnceFunc, OnceValue or OnceValues
  • Find WaitGroup and lock mistakes with go vet and the race detector, and fix them

Before you start

On this page

Sixteen goroutines each added 100,000 to one shared counter, and without a lock the total came out short of 1,600,000, a different number on every run. With a sync.Mutex it was exactly 1,600,000 every time. The sync package holds the tools that make shared data safe: a WaitGroup to wait for goroutines, a Mutex and an RWMutex to guard data, and Once to run setup exactly once.

You will use each of them on a program that shows what it prevents, and predict which goroutines wait and for how long. Each output on this page is a run recorded on Go 1.27.1.

A WaitGroup counts goroutines, and wg.Go starts and counts them

A WaitGroup keeps a counter of tasks. wg.Go(f), new in Go 1.25, adds one, runs f in a new goroutine and takes the one away when f returns; wg.Wait() blocks until the counter is zero. The older form does the same by hand: wg.Add(1) before each go statement and defer wg.Done() inside the goroutine:

wait_sum.go: 100 goroutines, two ways Go · wait_sum.go
// wait_sum.go: a WaitGroup waits for 100 goroutines, started with wg.Go (Go 1.25) and with Add and Done.
package main

import (
	"fmt"
	"sync"
)

func sum(xs []int) int {
	total := 0
	for _, x := range xs {
		total += x
	}
	return total
}

func main() {
	parts := make([]int, 101) // each goroutine writes only its own element

	var wg sync.WaitGroup
	for i := 1; i <= 100; i++ {
		wg.Go(func() { // counts the task, starts it, and marks it done when it returns
			parts[i] = i
		})
	}
	wg.Wait() // returns once all 100 have returned
	fmt.Println("wg.Go, sum of 1..100:", sum(parts))

	clear(parts)
	var old sync.WaitGroup
	for i := 1; i <= 100; i++ {
		old.Add(1) // before the go statement, never inside the goroutine
		go func() {
			defer old.Done()
			parts[i] = i
		}()
	}
	old.Wait()
	fmt.Println("Add and Done, the same sum:", sum(parts))
}

Output

wg.Go, sum of 1..100: 5050
Add and Done, the same sum: 5050

Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run wait_sum.go

Both forms summed 1 to 100 to 5050, each goroutine writing only its own element. Step through the counter:

Interactive · message by message A WaitGroup counts its goroutines down to zero Step through three calls of wg.Go and one wg.Wait, and watch the WaitGroup's counter.
A WaitGroup counts its goroutines down to zero, step 5 of 5. Messages so far: main to the WaitGroup: wg.Go(f) × 3; three goroutines to the WaitGroup: one returns; three goroutines to the WaitGroup: one returns; the WaitGroup to main: Wait returns.maingoes onthe WaitGroupcounter: 0three goroutinesall returnedwg.Go(f) × 3one returnsone returnsWait returns

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. wg.Go, three times. Each call of wg.Go adds 1 to the counter and starts its function in a new goroutine. After three calls the counter is 3 and three goroutines run.
  2. main waits. wg.Wait blocks while the counter is above zero. main is now asleep inside Wait.
  3. The first one returns. When a function started by wg.Go returns, its task is done and the counter falls by one, to 2. Wait keeps waiting.
  4. The second one returns. Another goroutine returns, and the counter falls to 1.
  5. Zero releases Wait. The last goroutine returns and the counter reaches 0. Wait returns, and main goes on knowing that all three are done and their writes are visible to it.

When Wait returns, every counted goroutine has returned, and what each wrote before it finished is visible to the goroutine that waited. wg.Go keeps the counting and the starting together, so the two can no longer drift apart.

Add inside the goroutine

go func() { wg.Add(1); … }() adds too late: Wait may run before any goroutine has started, see a counter of 0 and return at once. Call Add before the go statement, or use wg.Go, which cannot get it wrong.

go vet reports this mistake without running the code, and the race detector, turned on with -race, reports data races while a program runs.

Checkpoint

Check: Waiting with a WaitGroup

  1. Question 1 of 2 Three goroutines were started with wg.Go, and two of them have returned. What does wg.Wait() do now?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It blocks until the third goroutine returns

    Each wg.Go added one to the counter and each return took one away, so it stands at 1. Wait blocks until the third goroutine returns and the counter is 0.

  2. Question 2 of 2 What can this print?

    Read the code, then choose one answer.

    var wg sync.WaitGroup
    results := make([]int, 3)
    for i := range 3 {
    	go func() {
    		wg.Add(1)
    		defer wg.Done()
    		results[i] = i + 1
    	}()
    }
    wg.Wait()
    fmt.Println(results)
    Show the answer: Question 2 of 2

    Answer: [0 0 0], or any mix of zeros and values, because Wait may return before the goroutines add

    Add must happen before the go statement, so that Wait sees the count. Inside the goroutine it may come too late. wg.Go counts the task before the goroutine starts, so it makes the mistake impossible.

A Mutex lets one goroutine at a time into a critical section

counter++ looks like one step but is three: read the value, add one, write it back. Two goroutines can both read 41 and both write 42, and an increment is lost. That is a data race: A write to a memory location at the same time as another read or write of it in another goroutine, with nothing ordering the two. Its result cannot be predicted, a lost update for example, and the Go memory model treats it as a bug., and here is what it does to 1,600,000 increments:

counter.go: one counter, 16 goroutines Go · counter.go
// counter.go: 16 goroutines add 100,000 each to one counter, first with no lock, then with a Mutex.
package main

import (
	"fmt"
	"sync"
)

func main() {
	const workers, adds = 16, 100_000

	unguarded := 0
	var wg sync.WaitGroup
	for range workers {
		wg.Go(func() {
			for range adds {
				unguarded++ // read, add, write: another goroutine can come between the steps
			}
		})
	}
	wg.Wait()
	fmt.Printf("without a lock: %d of %d\n", unguarded, workers*adds)

	var mu sync.Mutex
	guarded := 0
	for range workers {
		wg.Go(func() {
			for range adds {
				mu.Lock()
				guarded++ // one goroutine at a time runs this line
				mu.Unlock()
			}
		})
	}
	wg.Wait()
	fmt.Printf("with a Mutex:   %d of %d\n", guarded, workers*adds)
}

Output

without a lock: 1008101 of 1600000
with a Mutex:   1600000 of 1600000

This output changes from run to run: the total without a lock depends on how the goroutines' steps interleave, so it changes from run to run

Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run counter.go

Without a lock the total fell short and changes with every run; with a mutex: A mutual exclusion lock, sync.Mutex. Lock waits until no other goroutine holds it, and releasing it lets the next one in, so the code in between runs in one goroutine at a time. Its zero value is a lock nobody holds. it was exactly 1,600,000. Step through one lost update:

Interactive · message by message Two goroutines, one counter, one update lost Step through two increments that overlap, and see why counter.go ended short of 1,600,000 without a lock.
Two goroutines, one counter, one update lost, step 6 of 6. Messages so far: unguarded to goroutine 1: read 41; unguarded to goroutine 2: read 41; goroutine 1 to unguarded: write 42; goroutine 2 to unguarded: write 42.goroutine 1has 41, will write 42unguardedwith the lock: 43goroutine 2has 41, will write 42read 41read 41write 42write 42

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Three steps, not one. unguarded++ is a read, an add and a write. Nothing stops another goroutine from running its own steps in between.
  2. Goroutine 1 reads. Goroutine 1 reads the counter and gets 41.
  3. Goroutine 2 reads. Before goroutine 1 writes, goroutine 2 reads the same counter. It still holds 41.
  4. Goroutine 1 writes. Goroutine 1 adds 1 to the 41 it read and writes 42.
  5. Goroutine 2 writes. Goroutine 2 also adds 1 to the 41 it read, and writes 42 over goroutine 1's 42. Two increments ran, and the counter rose by one.
  6. With a Mutex. With mu.Lock() before and mu.Unlock() after, the second goroutine cannot read until the first has written, so it reads 42 and writes 43. No update is lost.

mu.Lock() waits until no other goroutine holds the lock, and mu.Unlock() releases it, so the code between them, the critical section: Code that reads or writes shared data and must not run in two goroutines at once, such as the code a goroutine runs while it holds a Mutex., runs in one goroutine at a time. A Mutex’s zero value is a lock nobody holds, ready to use. Write defer mu.Unlock() right after mu.Lock() in a function that can return early, so every path releases it. A Mutex must not be copied after first use: share the struct that holds it by pointer.

Key idea

Every access to shared data that some goroutine writes must happen while holding the same lock, reads included.

Checkpoint

Check: Mutexes and critical sections

  1. Question 1 of 2 A counter holds 41. Two goroutines each run counter++ once, with no lock. Which final values are possible?

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • 42
    • 43

    counter++ is a read, an add and a write, and without a lock the steps of two goroutines can interleave. That is why counter.go's unguarded total fell short of 1,600,000.

  2. Question 2 of 2 What does var mu sync.Mutex give you?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A lock that nobody holds, ready to use

    Zero values that work are a Go habit, and Mutex follows it: declare one, then call Lock and Unlock. Share the struct that holds it by pointer, because a Mutex must not be copied after first use.

An RWMutex lets many readers in at once, or one writer

Data that is read far more often than it is written can use an RWMutex: A reader/writer lock, sync.RWMutex: any number of goroutines can hold its read lock together, taken with RLock, or a single goroutine its write lock, taken with Lock.. RLock takes a read lock, which any number of goroutines can hold together; Lock takes the write lock, which excludes everyone. Eight readers that each hold their lock for 100 ms show the difference:

readers.go: eight readers, two kinds of lock Go · readers.go
// readers.go: eight readers hold a lock for 100 ms each. How many are inside at once with an RWMutex, and with a Mutex?
package main

import (
	"fmt"
	"sync"
	"time"
)

// gauge counts the goroutines inside, and the most it has seen at once.
type gauge struct {
	mu        sync.Mutex
	now, most int
}

func (g *gauge) enter() {
	g.mu.Lock()
	g.now++
	g.most = max(g.most, g.now)
	g.mu.Unlock()
}

func (g *gauge) leave() {
	g.mu.Lock()
	g.now--
	g.mu.Unlock()
}

func main() {
	var rw sync.RWMutex
	var shared gauge
	var wg sync.WaitGroup
	start := time.Now()
	for range 8 {
		wg.Go(func() {
			rw.RLock() // many readers may hold the read lock together
			shared.enter()
			time.Sleep(100 * time.Millisecond) // reading a large table
			shared.leave()
			rw.RUnlock()
		})
	}
	wg.Wait()
	fmt.Println("RWMutex: at most", shared.most, "readers at once; under 400 ms in all:", time.Since(start) < 400*time.Millisecond)

	var mu sync.Mutex
	var alone gauge
	start = time.Now()
	for range 8 {
		wg.Go(func() {
			mu.Lock() // one goroutine at a time, readers included
			alone.enter()
			time.Sleep(100 * time.Millisecond)
			alone.leave()
			mu.Unlock()
		})
	}
	wg.Wait()
	fmt.Println("Mutex: at most", alone.most, "reader at once; at least 800 ms in all:", time.Since(start) >= 800*time.Millisecond)
}

Output

RWMutex: at most 8 readers at once; under 400 ms in all: true
Mutex: at most 1 reader at once; at least 800 ms in all: true

Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run readers.go

With the RWMutex all 8 readers were inside at once and the whole run took under 400 ms; with a Mutex they went in one at a time and needed at least 800 ms. A writer waits until every reader has released the read lock, and while it waits, new readers wait too, so a steady stream of readers cannot keep a writer out for ever.

The sync package also has two specialised types: sync.Map, a map for keys written once and read often, and sync.Pool, a store of spare objects to reuse.

Checkpoint

Check: Readers and writers

  1. One question Ten readers start together, and each holds a plain Mutex for 50 ms. At least how many milliseconds pass before all ten have finished?

    Type a number.

    A hint

    With a Mutex, how many readers can be inside at once?

    Show the answer: One question

    Answer: 500 ms

    A Mutex lets one goroutine in at a time, readers included, so ten 50 ms sections take at least 10 × 50 = 500 ms. With an RWMutex's read lock all ten can be inside together, as the 8 readers of readers.go were.

sync.Once and OnceValues run initialisation exactly once

once.Do(f) calls f the first time it is called, for that Once, and never again; callers that arrive while f runs wait for it to finish. Go 1.21 added three functions built on the same idea: sync.OnceFunc(f) returns a function that calls f once, and sync.OnceValue and sync.OnceValues return a function that calls f once and hands every caller its result, or its two results, usually a value and an error:

once.go: four callers, one load Go · once.go
// once.go: four goroutines need the same configuration; sync.OnceValues loads it once and shares the result.
package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

type config struct{ timezone, locale string }

var loads atomic.Int32

// load stands in for reading a file or calling a service: slow, and it can fail.
func load() (config, error) {
	loads.Add(1)
	return config{timezone: "Asia/Kolkata", locale: "en-IN"}, nil
}

var getConfig = sync.OnceValues(load) // a function that calls load on its first call only

func main() {
	results := make([]string, 4)
	var wg sync.WaitGroup
	for i := range 4 {
		wg.Go(func() {
			c, err := getConfig() // every call gets the one result, error included
			results[i] = fmt.Sprintf("%s %s %v", c.timezone, c.locale, err)
		})
	}
	wg.Wait()
	fmt.Println(results)
	fmt.Println("load ran", loads.Load(), "time for 4 calls")

	var once sync.Once
	for range 3 {
		once.Do(func() { fmt.Println("Once.Do: this line prints once") })
	}
}

Output

[Asia/Kolkata en-IN <nil> Asia/Kolkata en-IN <nil> Asia/Kolkata en-IN <nil> Asia/Kolkata en-IN <nil>]
load ran 1 time for 4 calls
Once.Do: this line prints once

Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run once.go

Four goroutines called getConfig, made by sync.OnceValues(load), at the same time, and load ran once: every caller got the same values and the same nil error. The Once printed its line once in three calls. This is lazy initialisation: Creating a value on first use instead of when the program starts. In Go, sync.Once, OnceFunc, OnceValue and OnceValues do it safely from any number of goroutines, running the work exactly once.: the work happens on first use, not when the program starts, and it is safe from any number of goroutines.

Checkpoint

Check: Once and lazy initialisation

  1. Question 1 of 2 once.Do(f) is called 5 times on the same Once. How many times does f run?

    Type a number.

    Show the answer: Question 1 of 2

    Answer: 1 times

    Do calls f only when it is called for the first time on that Once. once.go printed its line once in three calls.

  2. Question 2 of 2 getConfig := sync.OnceValues(load). load returns an error on its first call. What do later calls of getConfig return?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The same zero config and the same error, without calling load again

    OnceValues returns a function that calls load on its first call and returns the same values on every call. An error from that call is part of the result, so every caller sees it and can decide what to do.

WaitGroup and lock mistakes, reproduced with go vet, go fix and the runtime Pro: opens with a Pro, Premium or Ultimate pass

The sync types fail in two ways: quietly, with a wrong result or a race, and loudly, with a crash. This part runs the tools that find the quiet failures before they ship, rewrites old WaitGroup code automatically, and reproduces the crashes that recover cannot catch.

In this part:

  • What go vet and the race detector catch, reproduced
  • Crashes that recover cannot catch, and one that it can

6 runnable examples (Go, Python, lock_order.go, rlock_twice.go, unlock_twice.go) · about 6 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Once when setup fails, a worked leaderboard, and choosing the right tool Pro: opens with a Pro, Premium or Ultimate pass

Setup can fail, shared state comes in several shapes, and the right tool depends on both. This part shows what each Once does when its function panics, builds a leaderboard that 64 goroutines share safely, and puts the choices side by side.

In this part:

  • Once, OnceFunc and OnceValues when the loader panics
  • A leaderboard for 64 goroutines: an RWMutex, OnceValues and no copied lock
  • sync.Map and sync.Pool, the two specialised types
  • Choosing the tool for the shared state

3 runnable examples (Go) · about 9 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Practise

Work through questions on WaitGroups, Mutexes, RWMutexes and Once, one at a time, beginning with the concept your answers show you know least:

Practice that adapts to you

Practise this lesson

12 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 12 What does wg.Go(f) do?

    Choose one answer.

    Show the answer: Question 1 of 12

    Answer: It adds one task to the WaitGroup and runs f in a new goroutine, marking the task done when f returns

    wg.Go(f) counts the task and starts f in a goroutine; when f returns, the task is done. wait_sum.go used it to sum 1 to 100 to 5050.

  2. Question 2 of 12 In the older form, why is it defer wg.Done() at the top of the goroutine, rather than wg.Done() on its last line?

    Choose one answer.

    Show the answer: Question 2 of 12

    Answer: So that Done runs on every path out of the function, early returns included

    A deferred Done is called whichever way the function returns, so the counter always comes down. wg.Go does the same for you, which is one reason to prefer it.

  3. Question 3 of 12 Goroutines started with wg.Go each write results[i], and main reads results after wg.Wait(). Is main's read a data race?

    Choose one answer.

    Show the answer: Question 3 of 12

    Answer: No; everything a goroutine wrote before it returned is visible after Wait returns

    Wait is a synchronisation point. When it returns, every counted goroutine has returned, and their writes are visible to the goroutine that waited, so main can read the results without a lock.

  4. Question 4 of 12 A writer holds mu while it updates a price map. Readers read the map without taking mu, since they never write. Is that safe?

    Choose one answer.

    Show the answer: Question 4 of 12

    Answer: No; a read that runs while the writer writes is a data race, so readers must take the lock too

    Every access to data that some goroutine writes must take the same lock. Reads may share it, which is what an RWMutex is for, but they cannot skip it.

  5. Question 5 of 12 A function calls mu.Lock() and, three lines later, returns early on an error, before its mu.Unlock(). What happens next?

    Choose one answer.

    Show the answer: Question 5 of 12

    Answer: The lock stays held, and the next mu.Lock() anywhere blocks for ever

    A held Mutex stays held until someone calls Unlock. defer mu.Unlock() right after the Lock releases it on every return path, early errors included.

  6. Question 6 of 12 Why should a struct that holds a sync.Mutex be passed around by pointer?

    Choose one answer.

    Show the answer: Question 6 of 12

    Answer: Because a Mutex must not be copied after first use, and a copy would lock a different Mutex

    The sync documentation says a Mutex must not be copied after first use. Share the struct by pointer, and use pointer receivers on its methods, so every caller locks the same Mutex.

  7. Question 7 of 12 Who can hold an RWMutex at the same time?

    Choose one answer.

    Show the answer: Question 7 of 12

    Answer: Any number of readers, or one writer

    An RWMutex has two modes: shared, for readers with RLock, and exclusive, for a writer with Lock. readers.go had 8 readers inside at once.

  8. Question 8 of 12 Three readers hold the read lock. A writer calls Lock and waits. Then a fourth reader calls RLock. What happens to it?

    Choose one answer.

    Show the answer: Question 8 of 12

    Answer: It waits until the writer has had the lock

    The RWMutex documentation says that once a writer is waiting, new RLock calls wait until the writer has acquired and released the lock. Readers already inside finish first.

  9. Question 9 of 12 For which shared data does an RWMutex give the most over a plain Mutex?

    Choose every answer that is right.

    Show the answer: Question 9 of 12

    Answer:

    • A large lookup table that requests read for a long time each, and that rarely changes
    • A price table read thousands of times a second and updated once a minute

    An RWMutex pays off when reads dominate and take long enough to queue. When most accesses write, it behaves like a Mutex with more work.

  10. Question 10 of 12 What does sync.OnceFunc(f) return?

    Choose one answer.

    Show the answer: Question 10 of 12

    Answer: A function that calls f only the first time it is called

    OnceFunc, OnceValue and OnceValues, added in Go 1.21, package the common use of Once: a function you call freely that does its work on the first call only.

  11. Question 11 of 12 Four goroutines call getConfig, made with sync.OnceValues(load), at the same moment, and load takes 2 seconds. What do three of them do?

    Choose one answer.

    Show the answer: Question 11 of 12

    Answer: They wait until load finishes, then get the same result

    The first caller runs load; the others block until it returns, then all four receive the same configuration and error.

  12. Question 12 of 12 A program needs a connection pool only on some requests. Why make it with sync.OnceValues rather than in an init function?

    Choose one answer.

    Show the answer: Question 12 of 12

    Answer: The pool is created on first use, only if it is needed, and its error goes to the caller that needs it

    An init function runs at start-up whether or not the work is needed, and has no caller to return an error to. OnceValues runs the work on first use and hands every caller the value and the error.

Interview questions

Warm-up (fresher): What is a critical section, and how does a Mutex protect one?

A critical section is code that reads or writes shared data and must not run in two goroutines at once. A Mutex protects it: each goroutine calls Lock before the section and Unlock after it, so while one goroutine is inside, every other one waits in Lock.

Three longer questions of the module bank build on this lesson:

  1. Fresher to mid

    A batch job starts one goroutine per file, and each goroutine calls wg.Add(1) as its first line, then defer wg.Done(). Tests pass on a laptop, but in production the report sometimes misses files. Explain why, fix it, and say which tools would have caught it.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A cache's Get method takes the read lock of an RWMutex and calls stats(), which takes the read lock again. Refresh, which runs every minute, takes the write lock. The service passes every test, then freezes under production load. Explain the freeze and fix it.

    Model answer, Pro. First step and follow-ups

  3. Mid to senior

    A payments client is created lazily with sync.Once. One morning the network was down when the first request came in, the creation failed, and every request since has used a nil client and crashed, although the network is back. Explain why, and redesign the initialisation.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A WaitGroup counts tasks: wg.Go starts and counts a goroutine, Wait returns when the count is zero, and the older Add belongs before the go statement.
  • An unguarded counter++ from many goroutines loses updates; a Mutex makes the critical section run in one goroutine at a time.
  • An RWMutex lets readers hold the lock together and gives a writer the lock alone.
  • sync.Once, OnceFunc, OnceValue and OnceValues run setup once, however many goroutines ask at the same time.

Terms in this lesson

critical section
Code that reads or writes shared data and must not run in two goroutines at once, such as the code a goroutine runs while it holds a Mutex.
data race
A write to a memory location at the same time as another read or write of it in another goroutine, with nothing ordering the two. Its result cannot be predicted, a lost update for example, and the Go memory model treats it as a bug.
lazy initialisation
Creating a value on first use instead of when the program starts. In Go, sync.Once, OnceFunc, OnceValue and OnceValues do it safely from any number of goroutines, running the work exactly once.
mutex
A mutual exclusion lock, sync.Mutex. Lock waits until no other goroutine holds it, and releasing it lets the next one in, so the code in between runs in one goroutine at a time. Its zero value is a lock nobody holds.
RWMutex
A reader/writer lock, sync.RWMutex: any number of goroutines can hold its read lock together, taken with RLock, or a single goroutine its write lock, taken with Lock.
WaitGroup
A sync.WaitGroup counts tasks, usually goroutines: wg.Go or Add raises the count, a finished task lowers it, and Wait blocks until the count is zero.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.