GO

Go Fundamentals

18 lessons

Progress0%
1. Introduction to Go
1What is Go?
2. Variables and Data Types
1Data Types in Go
3. Control Flow
If, For, and SwitchDefer, Panic, Recover
4. Functions
Function BasicsError Handling
5. Structs and Methods
StructsMethods and Interfaces
6. Concurrency
Goroutines and ChannelsSelect and Sync
7. Maps & Slices Advanced
Slices Deep DiveMaps Operations & Patterns
8. Interfaces Deep Dive
Interface Composition & anyCommon Interfaces & Patterns
9. Packages & Modules
Package SystemGo Modules & Workspace
10. Testing & Standard Library
Testing in GoStandard Library Essentials
All Tutorials
GoConcurrency
Lesson 9 of 18 min
Chapter 6 · Lesson 1

Goroutines and Channels

Goroutines and Channels in Go

Go's concurrency model is built on goroutines (lightweight threads) and channels (typed communication pipes).

Goroutines Launch with the go keyword:

go
go someFunction()
go func() { … }()

Goroutines are much cheaper than OS threads — you can spawn thousands.

Channels Channels pass data between goroutines safely:

go
ch := make(chan int)        // unbuffered
ch := make(chan int, 10)    // buffered (capacity 10)
ch <- value                // send
value := <-ch              // receive

Unbuffered vs buffered

  • Unbuffered: send blocks until a receiver is ready (synchronizes goroutines).
  • Buffered: send blocks only when the buffer is full.

range over channel Receive values until the channel is closed:

go
for v := range ch { … }

close Signals no more values will be sent. Receivers see remaining values then the zero value with ok == false.

Key points:

  • Never close a channel from the receiver side.
  • Sending on a closed channel panics.
  • Goroutines are not threads — the Go scheduler multiplexes them onto OS threads.

Code Examples

Goroutines with a channelgo
package main

import (
	"fmt"
)

func squares(n int, ch chan<- int) {
	for i := 1; i <= n; i++ {
		ch <- i * i
	}
	close(ch)
}

func main() {
	ch := make(chan int)
	go squares(5, ch)

	for v := range ch {
		fmt.Print(v, " ")
	}
	fmt.Println()
}

squares runs in a goroutine, sending values to ch. The main goroutine reads with range until ch is closed.

Buffered channel as a semaphorego
package main

import (
	"fmt"
	"sync"
)

func worker(id int, wg *sync.WaitGroup, sem chan struct{}) {
	defer wg.Done()
	sem <- struct{}{} // acquire slot
	fmt.Printf("Worker %d running\n", id)
	<-sem // release slot
}

func main() {
	const maxConcurrent = 2
	sem := make(chan struct{}, maxConcurrent)
	var wg sync.WaitGroup

	for i := 1; i <= 4; i++ {
		wg.Add(1)
		go worker(i, &wg, sem)
	}
	wg.Wait()
	fmt.Println("All done")
}

A buffered channel with capacity N acts as a semaphore, limiting concurrent access. WaitGroup waits for all goroutines to finish.

Quick Quiz

1. What happens when you send on an unbuffered channel with no receiver ready?

2. What does `close(ch)` signal to receivers?

Was this lesson helpful?

PreviousNext