Goroutines

By Syncfusion®, Inc. · Last updated

Concurrency in Go: concurrency vs parallelism, starting goroutines with go, waiting with WaitGroup, anonymous goroutines, what happens when main ends, and concurrent processing.

What Is Concurrency?

Concurrency = a program working on many jobs during the same period of time, switching between them.

Goroutine = a very light worker that runs a function at the same time as the rest of the program.

Go was built for this. Starting a goroutine takes one word: go.

Simple lesson: concurrency lets one program handle many jobs at once, and goroutines are how Go does it.

A Real-World Example

Think of the school canteen at lunch break.

  • One cook, one job at a time: make tea, then wait for it, then make sandwiches, then fry samosas. The line grows long.
  • One cook who switches: while the tea boils, make sandwiches. While the samosas fry, serve tea. Much faster.
  • Three cooks: each makes one item, truly at the same moment.

Now the mapping:

  • Switching between jobs = concurrency.
  • Three cooks at the same moment = parallelism.
  • Each job a cook works on = a goroutine.

Goroutines are much lighter than operating-system threads.

  • A new goroutine starts with only a few kilobytes of memory.
  • A program can run thousands, even millions, of them.
  • Go's scheduler decides which goroutine runs on which CPU core.

What You Will Learn

In this lesson, you will learn:

  1. What concurrency is, and how it differs from parallelism.
  2. How to start goroutines with the go keyword.
  3. How to wait for goroutines to finish.
  4. How to start goroutines from anonymous functions.
  5. What happens to goroutines when main ends.
  6. A common use: processing many items at once.

Concurrency Fundamentals

Concurrency vs Parallelism

Two ideas that are often mixed up

  • Go programs are written concurrently.
  • On a computer with many cores, Go also runs them in parallel.
  • In this browser lab, goroutines take turns on one thread. They are concurrent, but not parallel.

Simple lesson: concurrency is many jobs in progress, parallelism is many jobs at the same instant.

Goroutines

The go Keyword

Put go in front of a function call, and that call runs in a new goroutine.

go sendReminder("Asha")   // runs at the same time
fmt.Println("main goes on")
  • main does not wait for sendReminder to finish.
  • main itself runs in a goroutine too: the main goroutine.

Running Concurrent Tasks

Pick a mode and press Run.

Compare the three modes:

  • No go keyword: the total time is the sum of all jobs.
  • go + WaitGroup: the total time is the slowest job.
  • go, but no Wait: the program ends before the work is done.

Creating Goroutines and Waiting for Them

sync.WaitGroup = a counter that lets main wait until a group of goroutines is done. You will learn it fully in Synchronization.

  • wg.Add(1) before starting each goroutine.
  • defer wg.Done() inside the goroutine.
  • wg.Wait() blocks until the counter is back to zero.

Three goroutines, then wait

package main

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

func markPapers(class string, minutes int, wg *sync.WaitGroup) {
	defer wg.Done()
	time.Sleep(time.Duration(minutes) * 10 * time.Millisecond)
	fmt.Println(class, "done")
}

func main() {
	var wg sync.WaitGroup

	wg.Add(3)
	go markPapers("8A", 3, &wg)
	go markPapers("8B", 1, &wg)
	go markPapers("8C", 2, &wg)

	wg.Wait()
	fmt.Println("all done")
}
8B done
8C done
8A done
all done
  • The shortest job finishes first, not the first one started.
  • One after another, the jobs would take 30 + 10 + 20 = 60 ms. Together, they take about 30 ms: the slowest one.
  • Pass the WaitGroup as a pointer: &wg. Copying it would break it.

Goroutine Execution

  • The order goroutines run in is not fixed. Never depend on it.
  • Above, the sleeps made the order clear. Without them, any order is possible.
  • If you need a fixed order, collect results and sort them, or use channels. See Channels.

Simple lesson: go f() starts f at the same time, and a WaitGroup lets main wait for it.

Anonymous Goroutines

Goroutines with Anonymous Functions

Often the goroutine's work is short, so it is written as a function literal. See Functions.

Squares worked out in goroutines

  • go func(n int) { ... }(i) starts a goroutine running a function literal.
  • Passing i as an argument gives each goroutine its own copy.
  • Since Go 1.22, each loop trip also gets its own i, so using i directly is safe too.
  • Each goroutine writes a different slot. Writing the same variable needs a lock. See Synchronization.

Results in a fixed order

package main

import (
	"fmt"
	"strings"
	"sync"
)

func main() {
	names := []string{"asha", "ravi", "meena"}
	badges := make([]string, len(names))
	var wg sync.WaitGroup
	for i, name := range names {
		wg.Add(1)
		go func() {
			defer wg.Done()
			badges[i] = strings.ToUpper(name) + " (8A)"
		}()
	}
	wg.Wait()
	fmt.Println(badges)
}
[ASHA (8A) RAVI (8A) MEENA (8A)]

Simple lesson: start short jobs with go func() { ... }(), and give each goroutine its own place to write.

Goroutine Behavior

Concurrent Execution

The order can change

package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	for _, name := range []string{"Asha", "Ravi", "Meena"} {
		wg.Add(1)
		go func() {
			defer wg.Done()
			fmt.Println("hello from", name)
		}()
	}
	wg.Wait()
}
  • On a real computer, the three lines may come out in any order.
  • In this lab, goroutines take turns on one thread, so the order often looks the same. Do not rely on it.

Program Termination and Goroutines

When main returns, the whole program ends, even if other goroutines are still working.

main does not wait by itself

package main

import (
	"fmt"
	"time"
)

func main() {
	go func() {
		time.Sleep(50 * time.Millisecond)
		fmt.Println("this line is usually never printed")
	}()
	fmt.Println("main is done")
}
  • main prints and returns at once.
  • The goroutine is stopped before its Println.
  • Always wait for goroutines you care about: WaitGroup or channels.

Never use time.Sleep in main to "wait long enough" for goroutines.

It is either too long (slow) or too short (lost work). Use sync.WaitGroup or channels.

Easy way to remember

  • go f() = start f, do not wait.
  • wg.Add, wg.Done, wg.Wait = wait for a group.
  • When main ends, every goroutine ends.

Simple lesson: goroutines run in no fixed order, and they all stop when main returns.

Common Use Cases

Concurrent Processing

Concurrent processing = splitting a big job into pieces that run at the same time.

Good fits:

  • checking many websites or files at once
  • resizing many photos
  • handling many users' requests on a server
  • sending many notifications

Check every class's attendance at once

package main

import (
	"fmt"
	"sync"
)

func countPresent(marks []bool) int {
	n := 0
	for _, present := range marks {
		if present {
			n++
		}
	}
	return n
}

func main() {
	classes := map[string][]bool{
		"8A": {true, true, false, true},
		"8B": {true, false, false},
		"8C": {true, true, true},
	}
	order := []string{"8A", "8B", "8C"}
	present := make([]int, len(order))

	var wg sync.WaitGroup
	for i, name := range order {
		wg.Add(1)
		go func() {
			defer wg.Done()
			present[i] = countPresent(classes[name])
		}()
	}
	wg.Wait()

	for i, name := range order {
		fmt.Println(name, "present:", present[i])
	}
}
8A present: 3
8B present: 1
8C present: 3
  • One goroutine per class.
  • Each writes its own slot in present, so there is no clash.
  • Printing happens after wg.Wait(), in a fixed order.

Simple lesson: split independent work into goroutines, give each its own result slot, and wait for them all.

TL;DR

  1. Concurrency = many jobs in progress. Parallelism = many jobs at the same instant.
  2. Goroutine = a light worker. Start one with go f().
  3. main does not wait. When it returns, all goroutines stop.
  4. Wait with sync.WaitGroup: Add(n), defer Done(), Wait().

    • Pass it as &wg.
  5. Anonymous goroutine = go func() { ... }().
  6. Order is not fixed. Write results to separate slots, or use channels.
  7. Use = concurrent processing of independent jobs.

Common mistakes

MistakeWrong ideaCorrect ideaWhat goes wrong
No wg.Wait()main waits for goroutinesIt does notWork is lost when main ends.
time.Sleep to waitA sleep is enoughUse a WaitGroupFlaky, slow programs.
Passing wg by valueIt is finePass &wgWait never returns, or Done panics.
wg.Add(1) inside the goroutineIt counts in timeCall Add before goWait may return too early.
Expecting output in start orderGoroutines run in orderOrder is not fixedConfusing, changing output.
Many goroutines writing one variableIt is safeIt is a data raceWrong totals. See Synchronization.

Check Yourself

Exercises

What to Remember

  • Concurrency is many jobs in progress. Parallelism is many jobs at the same instant.
  • go f() starts a goroutine. It does not wait.
  • Use sync.WaitGroup to wait: Add, Done, Wait.
  • Goroutines run in no fixed order. Give each its own result slot.
  • When main returns, every goroutine stops.

Next: Channels. You will learn how goroutines send values to each other safely.