Introduction
In multithreaded applications, it’s common for one thread to let another know when specific conditions are met, like when data is ready or a time-consuming task is finished. In Go, you can use goroutines and channels to achieve this. This example demonstrates implementing a GuardedGarage
to park ExpensiveCar
objects.
Implementation in Go
We will start by importing the necessary packages:
import (
"fmt"
"time"
)
Next we define our ExpensiveCar
struct:
type ExpensiveCar struct {
brand string
color string
}
func CreateCar(brand string, color string) *ExpensiveCar {
return &ExpensiveCar{brand: brand, color: color}
}
This code is quite self-explanatory.
Let’s have a look at some more interesting code, the GuardedGarage
:
type GuardedGarage struct {
ch chan *ExpensiveCar
}
func NewGuardedGarage() *GuardedGarage {
g := &GuardedGarage{}
g.ch = make(chan *ExpensiveCar)
return g
}
func (g *GuardedGarage) Put(c *ExpensiveCar) {
g.ch <- c
fmt.Println("Putting", c)
}
func (g *GuardedGarage) Get() *ExpensiveCar {
return <-g.ch
}
A few notes here:
- In the
GuardedGarage
we keep- A channel on which we can send our parked cars.
- There is a simple constructor-like method
- In the
Put()
method, we lock our code, put the car to the channel and return - The
Get()
method just returns what is on the channel. There is no need to lock the code here, it will even give an error if you do.
Why we don’t a lock in the Get() method?
The Get()
method doesn’t need to be locked as it is using a channel to synchronize operation. When we put a car in the garage we send it to the channel. When we dequeued it, the channel receives it. Channel operations are synchronized which means the Put()
method will not continue unless the Get()
method has received a car. This way channels are thread-safe and there is no need for an explicit lock.
Testing time
Now we can test our setup:
func main() {
garage := NewGuardedGarage()
go func() {
garage.Put(CreateCar("BMW", "red"))
garage.Put(CreateCar("Audi", "blue"))
garage.Put(CreateCar("Mercedes", "green"))
close(garage.ch)
}()
fmt.Println(garage.Get())
fmt.Println(garage.Get())
fmt.Println(garage.Get())
}
We create a garage, start a go routine which fills the garage, and the try and get some cars from it. In the output you will probably see something like this:
Putting &{BMW red}
&{BMW red}
&{Audi blue}
Putting &{Audi blue}
Putting &{Mercedes green}
&{Mercedes green}
The puts and gets seem to be out of order, but that has to do with the multi-threaded nature of this pattern.
Conclusion
It took a while for me to understand this pattern, but it turned to be quite easy using channels, which are by their very nature thread-safe. One possible enhancement could be to have multiple consumers for our GuardedGarage
instead of just one. Also, make this pattern more generic could be another enhancements.