Let's break down each concept (interfaces, goroutines, channels, and concurrency patterns) with detailed examples.
Interfaces are types that specify method signatures. Any type that implements those methods is said to satisfy the interface. This allows you to define flexible and modular code.
Consider an interface Speaker with a single method Speak:
package main
import "fmt"
// Speaker interface
type Speaker interface {
Speak() string
}
// Structs implementing Speaker
type Person struct {
Name string
}
func (p Person) Speak() string {
return "Hello, I'm " + p.Name
}
type Dog struct {
Breed string
}
func (d Dog) Speak() string {
return "Woof! I'm a " + d.Breed
}
// Function using the interface
func sayHello(s Speaker) {
fmt.Println(s.Speak())
}
func main() {
alice := Person{Name: "Alice"}
buddy := Dog{Breed: "Golden Retriever"}
sayHello(alice) // Uses Person's Speak method
sayHello(buddy) // Uses Dog's Speak method
}In this example:
- Both
PersonandDogimplement theSpeakerinterface by having aSpeakmethod. - The
sayHellofunction accepts any type that implementsSpeaker.
An empty interface, interface{}, can hold values of any type:
func printAnything(v interface{}) {
fmt.Println(v)
}
func main() {
printAnything(42)
printAnything("Hello, Go!")
printAnything(true)
}Type assertion allows you to retrieve the actual type from an interface:
func describe(i interface{}) {
switch v := i.(type) {
case string:
fmt.Println("It's a string:", v)
case int:
fmt.Println("It's an integer:", v)
default:
fmt.Println("Unknown type")
}
}
func main() {
describe(42)
describe("Golang")
}Goroutines are functions or methods that run concurrently with other functions. They are the basic unit of concurrency in Go and are much lighter than OS threads.
package main
import (
"fmt"
"time"
)
func say(message string) {
for i := 0; i < 3; i++ {
fmt.Println(message)
time.Sleep(1 * time.Second)
}
}
func main() {
go say("Hello") // Run say() as a goroutine
say("World") // Run in the main goroutine
// Prevent the program from exiting immediately
time.Sleep(4 * time.Second)
}Here:
go say("Hello")runssayconcurrently with themainfunction.- The
time.Sleepat the end prevents the program from exiting before "Hello" has time to complete.
Channels are Go’s way to allow goroutines to communicate with each other safely. They enable the sending and receiving of data between goroutines.
package main
import "fmt"
func producer(ch chan int) {
for i := 1; i <= 3; i++ {
fmt.Println("Sending:", i)
ch <- i // Send value to channel
}
close(ch) // Close channel when done
}
func main() {
ch := make(chan int)
go producer(ch) // Run producer in a goroutine
for value := range ch { // Receive values until channel is closed
fmt.Println("Received:", value)
}
}Here:
- The
producerfunction sends integers to the channel. - The
mainfunction receives values until the channel is closed.
Buffered channels allow you to send multiple values without blocking, up to a defined capacity.
package main
import "fmt"
func main() {
ch := make(chan string, 2) // Buffered channel with capacity 2
ch <- "Hello"
ch <- "World"
fmt.Println(<-ch) // "Hello"
fmt.Println(<-ch) // "World"
}Concurrency patterns help manage complex concurrent workflows. Here are a few common patterns.
The select statement allows a goroutine to wait on multiple channel operations.
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "Channel 1"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "Channel 2"
}()
select {
case msg := <-ch1:
fmt.Println("Received:", msg)
case msg := <-ch2:
fmt.Println("Received:", msg)
case <-time.After(1 * time.Second):
fmt.Println("Timeout!")
}
}Here:
selectlistens for messages from bothch1andch2.- If neither message arrives within 1 second, it prints "Timeout!"
This pattern allows multiple goroutines to read from one channel (fan-out) and write to another channel (fan-in).
package main
import "fmt"
func producer(jobs chan<- int) {
for i := 1; i <= 5; i++ {
jobs <- i
}
close(jobs)
}
func worker(id int, jobs <-chan int, results chan<- int) {
for job := range jobs {
fmt.Printf("Worker %d processing job %d\n", id, job)
results <- job * 2
}
}
func main() {
jobs := make(chan int, 5)
results := make(chan int, 5)
go producer(jobs)
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
for i := 1; i <= 5; i++ {
fmt.Println("Result:", <-results)
}
}Here:
producersends jobs to thejobschannel.- Three worker goroutines process jobs concurrently and send results to the
resultschannel.
A worker pool is a set of goroutines processing jobs from a shared job queue.
package main
import "fmt"
func worker(id int, jobs <-chan int, results chan<- int) {
for job := range jobs {
fmt.Printf("Worker %d started job %d\n", id, job)
results <- job * job // Simulate work
}
}
func main() {
jobs := make(chan int, 5)
results := make(chan int, 5)
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
for a := 1; a <= 5; a++ {
fmt.Println("Result:", <-results)
}
}In this example:
- Three worker goroutines each pull from the
jobschannel and push to theresultschannel.
Timeouts prevent a goroutine from blocking indefinitely by waiting for too long.
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
go func() {
time.Sleep(3 * time.Second)
ch <- 42
}()
select {
case res := <-ch:
fmt.Println("Received:", res)
case <-time.After(2 * time.Second):
fmt.Println("Timeout occurred")
}
}Here:
- The
selectwaits for either the result fromchor a 2-second timeout. - Since the timeout is shorter than the sleep in the goroutine, "Timeout occurred" is printed.
These examples should give you a practical understanding of Go's concurrency features and patterns. Each pattern can be adapted to build efficient, concurrent applications. Let me know if you have any questions about these concepts!