The preferred and sanctioned way of managing concurrency and state is exclusively through channels.
We've demonstrated a few more complex types of channels, but we haven't looked at what can become a daunting but powerful implementation: channels of channels. This might at first sound like some unmanageable wormhole, but in some situations we want a concurrent action to generate more concurrent actions; thus, our goroutines should be capable of spawning their own.
As always, the way you manage this is through design while the actual code may simply be an aesthetic byproduct here. Building an application this way should make your code more concise and clean most of the time.
Let's revisit a previous example of an RSS feed reader to demonstrate how we could manage this, as shown in the following code:
package main import ( "fmt" ) type master chan Item var feedChannel chan master var done chan bool type Item struct { Url string Data []byte } type Feed struct { Url string Name string Items []Item } var Feeds []Feed func process(feedChannel *chan master, done *chan bool) { for _, i := range Feeds { fmt.Println("feed", i) item := Item{} item.Url = i.Url itemChannel := make(chan Item) *feedChannel <- itemChannel itemChannel <- item } *done <- true } func processItem(url string) { // deal with individual feed items here fmt.Println("Got url", url) } func main() { done := make(chan bool) Feeds = []Feed{Feed{Name: "New York Times", Url: "http://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml"}, Feed{Name: "Wall Street Journal", Url: "http://feeds.wsjonline.com/wsj/xml/rss/3_7011.xml"}} feedChannel := make(chan master) go func(done chan bool, feedChannel chan master) { for { select { case fc := <-feedChannel: select { case item := <-fc: processItem(item.Url) } default: } } }(done, feedChannel) go process(&feedChannel, &done) <-done fmt.Println("Done!") }
Here, we manage feedChannel
as a custom struct that is itself a channel for our Item
type. This allows us to rely exclusively on channels for synchronization handled through a semaphore-esque construct.
If we want to look at another way of handling a lower-level synchronization, sync.atomic
provides some simple iterative patterns that allow you to manage synchronization directly in memory.
As per Go's documentation, these operations require great care and are prone to data consistency errors, but if you need to touch memory directly, this is the way to do it. When we talk about advanced concurrency features, we'll utilize this package directly.