Understanding When To Use Channels Or Mutexes In Go

Problem Domain Link to heading

Community - “Share Memory By Communicating”

Interpretation - “CHANNEL ALL TEH THINGZ!”

Because many developers come from backgrounds (Php, Ruby, Perl, Python) where unlike Go, concurrency is not a first class citizen, they struggle when learning about it. But they apply themselves and take the time to dig into Go’s concurrency model. And just as they finally feel they’ve come to grips, something painful happens. The developer decides to use their new found super-power (goroutines + channels) for absolutely everything and it suddenly becomes an anti-pattern.

External Confirmation Link to heading

I’m not the only one who has noticed. In the discussion below Andrew warns about the overuse of channels to keep state in goroutines.

“Hey, concurrency is great, but all this needs is a mutex” Link to heading

Also, in the following interview between Andrew and Dave. Andrew asks Dave to give an example of when he wrote some bad code and Dave mentions specifically about using too many channels.

“and I tried to use channels for everything, if you want to talk about worst code.” Link to heading

Confession Link to heading

When I created BadActor, I needed it to help me in identify malicious activities against my systems so that I could respond to them. I set out to solve my problem (and gain proficiency with Go) and I envisioned this grand design where everything was a goroutine and communication would happen only through channels and I would never need another mutex ever again. (Hah!)

It sounded great in theory. But the composition was cumbersome and holy crap did the Benchmarks suck. I didn’t think too much of this. I thought that it was just me misunderstanding on how to use channels in Go. So I hunkered down and persisted with my design for a while until I learned better. When I realized my error, I rewrote large portions of my codebase.

Coding Horror Link to heading

Below is an old commit of BadActor‘s where originally I had architected the exact model that Andrew and Dave spoke about. Many goroutines, each with their own channel(s), combined with message types to define the behavior of the communication. I leave them here so you can hopefully learn from my mistake.

Here is the Actor’s goroutine with its open channel.

func (a *Actor) Run() {
  go func() {
    for {
      select {
        case in := <-a.Incoming:
          a.switchBoard(in)
        default:
          a.overwatch()
      }
    }
  }()
}

And here is the awkward Type checking that Andrew warns about.

func (a *Actor) switchBoard(in *Incoming) {
  switch in.Type {
    case KEEP_ALIVE:
        err := a.rebaseAll()
        in.Outgoing <- &Outgoing{Error: err}
    case INFRACTION:
        err := a.infraction(in.RuleName)
        in.Outgoing <- &Outgoing{Error: err}
    case CREATE_INFRACTION:
        err := a.createInfraction(in.Infraction)
        in.Outgoing <- &Outgoing{Error: err}
    case STRIKES:
        total := a.strikes(in.RuleName)
        in.Outgoing <- &Outgoing{Message: strconv.Itoa(total)}
    case IS_JAILED:
        res := a.isJailed()
        in.Outgoing <- &Outgoing{Message: strconv.FormatBool(res), Error: nil}
    case IS_JAILED_FOR:
        res := a.isJailedFor(in.RuleName)
        in.Outgoing <- &Outgoing{Message: strconv.FormatBool(res), Error: nil}
    
  }
}

Practical Example - Mutex Link to heading

If I synchronously need to solve for (x) and I just have one input, then I protect it with a mutex. I do not implement channels and goroutines just to protect the state of some asset.

Example: A cache takes one key (k) to gather one value (x).

Below is a very naive cache and http server I created to help you understand.

We start by creating our main.go file.

$ vi main.go

Then we import the needing packages (thanks goimport).

package main
 
import (
  "fmt"
  "html"
  "log" 
  "net/http"
  "net/url"
  "sync"
)

Next we define our NaiveCache and define some getter/setter methods.

//
// NaiveCache
//
 
var c *NaiveCache
 
type NaiveCache struct {
  mu      sync.Mutex
  storage map[string]string

}
 
func (c *NaiveCache) Value(k string) (string, error) {
  var v string
  c.mu.Lock()
  defer c.mu.Unlock()
  if v, ok := c.storage[k]; ok {
    return v, nil
  
  }
  return v, fmt.Errorf("Value Not Found")

}
 
func (c *NaiveCache) SetValue(k string, v string) {
  c.mu.Lock()
  c.storage[k] = v
  c.mu.Unlock()
  return

}

Here we implement the server giving it a handler and two helper functions, get() and post().

//
// Server
//
func NaiveCacheHandler(w http.ResponseWriter, r *http.Request) {
  switch r.Method {
  case "GET":
    get(w, r)
  case "POST":
    post(w, r)
  
  }

}
 
func get(w http.ResponseWriter, r *http.Request) {
  id, err := url.QueryUnescape(r.URL.Query().Get("key"))
    if err != nil {
    w.WriteHeader(http.StatusBadRequest)
    b := []byte(fmt.Sprintf("%v StatusBadRequest", http.StatusBadRequest))
    w.Write(b)
    return
  
    }
 
  v, err := c.Value(id)
    if err != nil {
    w.WriteHeader(http.StatusNotFound)
    b := []byte(fmt.Sprintf("%v StatusNotFound", http.StatusNotFound))
    w.Write(b)
    return
  
    }
 
  w.Write([]byte(html.EscapeString(v)))
  return

}
 
func post(w http.ResponseWriter, r *http.Request) {
  k := html.EscapeString(r.FormValue("key"))
  v := html.EscapeString(r.FormValue("value"))
  if len(k) == 0 || len(v) == 0 {
    w.WriteHeader(http.StatusBadRequest)
    b := []byte(fmt.Sprintf("%v StatusBadRequest", http.StatusBadRequest))
    w.Write(b)
    return
  
  }
 
  c.SetValue(k, v)
  w.WriteHeader(http.StatusOK)
  b := []byte(fmt.Sprintf("%v StatusOK", http.StatusOK))
  w.Write(b)
  return

}

Ending with the main function which gets called upon running our compiled application.

//
// Main
//
func main() {
  // init the cache
  c = &NaiveCache{storage: make(map[string]string)}
 
  // start the server
  http.HandleFunc("/cache", NaiveCacheHandler)
  log.Fatal(http.ListenAndServe(":9090", nil))

}

Run the application.

$ go run main.go

And submit some curl requests to see it work.

$ curl -X GET http://localhost:9090/cache\?key\=somekey
404 StatusNotFound
$ curl --data "key=somekey&value=somevalue" http://localhost:9090/cache
200 StatusOK
$ curl -X GET http://localhost:9090/cache\?key\=somekey
somevalue
$ curl -X GET http://localhost:9090/cache\?key\=somekey
somevalue
$

Closing Link to heading

Don’t worry about failing, everyone learns when they fail so go ahead and dive in and start building something in Go. But remember, if you feel like you are fighting your design and you notice that you are primarily using channels to protect state, then hey, concurrency is great, but sometimes all you need is a mutex.