Search and navigate

Pages

Start HereRoadmapTutorialsCheatsheetInterview QuestionsResources

Published tutorials

Building a REST API with Go: The Complete TutorialGeneric Functions in Go: Writing Reusable CodeGo Channels: Communication Between GoroutinesGo Constraints: comparable, Ordered and Custom SetsGo Context: Cancellation, Deadlines, and Request ValuesGo HTTP Client: Timeouts, JSON, and Reliable RequestsGo HTTP Middleware: Build a Production ChainGo Interview Questions: 25 Answers That Show UnderstandingGo Mutex: Protecting Shared State from Race ConditionsGo net/http: Build an HTTP Server from ScratchGo select Statement: Multiplexing Channels ExplainedGo Tools You Need for a Reliable Development LoopGo Tutorial: From First Program to Backend FoundationsGo Type Parameters: Syntax and Constraints ExplainedGo Worker Pool Pattern: Bounded Concurrency at ScaleGo's Type System Deep Dive: Static Typing Done RightGolang Compilation and Execution ExplainedGolang File Structure for Modules and ApplicationsGolang Hello World and Your First Go ProgramGolang IDEs: Choose an Editor for Your WorkflowGolang Interfaces: What They Are and How to Use ThemGoroutines in Go: Concurrency Without LeaksHow to Download and Install Golang SafelyHow to Learn Golang with a Focused Study PlanJSON in Go: Encoding, Decoding, and ValidationLearn Golang with a Practical Beginner Coursesync.WaitGroup in Go: Coordinating Multiple GoroutinesTesting in Go: Table Tests, HTTP, Fakes, and CoverageWhat Is Go and How Does a Go Program Work?Why Learn Golang and What Is Go Used For?
Open full search and filters →

Goroutines in Go: Concurrency Without Leaks

Learn how goroutines run, how to wait for them, propagate errors, cancel work, and prevent leaks.

Standard-library lessonGo requirement: Go 1.21+How tutorials are checked
Concurrency · Lesson 6Saved in this browser. No account required.

A goroutine is a function executing concurrently with other goroutines in the same process. Starting one is easy; owning its lifetime is the important part.

Reviewer’s note: This tutorial treats goroutines as owned work: every goroutine gets a completion path, a cancellation path, and a clear owner for errors and shutdown. The examples focus on preventing leaks rather than merely starting concurrent functions.

A Go goroutine lifecycle from start through work, waiting, cancellation, and clean exit.

Start a goroutine

go func() {
	fmt.Println("working")
}()

The program does not wait automatically. If main returns, all goroutines stop. Use synchronization rather than sleeping and hoping work finishes.

Wait for known work

var wg sync.WaitGroup
for _, item := range items {
	item := item
	wg.Add(1)
	go func() {
		defer wg.Done()
		process(item)
	}()
}
wg.Wait()

See the complete WaitGroup guide for ordering mistakes and safe result collection.

Bound concurrency

Launching one goroutine for every unbounded input can exhaust memory, connections, or downstream capacity. Use a semaphore or worker pool to set an intentional limit.

A bounded Go worker group receives jobs, merges results, and stops through a cancellation signal.

Propagate cancellation

Long-lived goroutines should have a defined stop condition:

func watch(ctx context.Context, updates <-chan Update) {
	for {
		select {
		case update, ok := <-updates:
			if !ok { return }
			apply(update)
		case <-ctx.Done():
			return
		}
	}
}

Capture errors

Errors returned inside a goroutine do not travel anywhere automatically. Send them on a channel, collect them in protected state, or use a structured concurrency helper.

Test for races and leaks

Run go test -race ./.... The race detector finds unsynchronized memory access, not every deadlock or leak. Design tests that cancel operations and assert they return promptly.

Every goroutine should have an owner, a stop condition, and a strategy for reporting failure. If you cannot explain those three things, keep the operation synchronous until you can.