Одной из самых крутых особенностей Go являются горутины, которые позволяют вам очень легко делать что-то одновременно. Однако одна вещь, которая чрезвычайно важна для горутин, которую можно упустить из виду, - это мьютексы. Например, когда более одной горутины пытаются увеличить глобальную переменную, мы можем столкнуться со сценарием, в котором счетчик не точен. Давайте посмотрим на пример:

var count int
func updateCount(done chan<- bool) {
  count++
  done <- true
}
func main() {
  done := make(chan bool)
  for i := 0; i < 1000; i++ {
    go updateCount(done)
  }
  for i := 0; i < 1000; i++ {
    <-done
  }
  fmt.Println(count)
}

Теперь, несмотря на то, что все горутины завершили инкрементирование, когда мы печатаем count, мы можем увидеть что-то вроде 990, 986, 987 и т. д. и т. д., даже если мы увеличили переменную в 1000 раз, начиная с 0. Так как же нам это исправить? тогда? Мы должны использовать мьютексы. Мьютексы позволяют нашим параллельным программам получать доступ к переменной, но не одновременно. Им приходится ждать своей очереди, теперь вы можете подумать, что это глупо, в конце концов, почему бы просто не использовать обычные функции вместо горутин? Ну, в других, более реалистичных примерах, мы по-прежнему будем делать другие вещи одновременно, например, у нас может быть куча горутин, делающих http-запросы и добавляющих ответы в массив. Вот обновленная версия нашей программы с использованием мьютексов:

var count int
var mu sync.Mutex
func updateCount(done chan<- bool) {
  mu.Lock()
  count++
  mu.Unlock()
  done <- true
}
func main() {
  done := make(chan bool)
  for i := 0; i < 1000; i++ {
    go updateCount(done)
  }
  for i := 0; i < 1000; i++ {
    <-done
  }
  fmt.Println(count)
}

В этом примере счетчик всегда будет 1000 при печати, потому что мы использовали мьютексы, чтобы убедиться, что он обновляется правильно. Давайте возьмем предыдущий пример http-запросов и используем мьютексы для сбора множества ответов с разных сайтов:

var responses []*http.Response
var mu sync.Mutex
func fetch(url string, done chan<- bool) {
  response, err := http.Get(url)
  if err != nil {
    fmt.Printf("error: making request to site '%s': %v\n", url, err)
  } else {
    mu.Lock()
    responses = append(responses, response)
    mu.Unlock()
  }
  done <- true
}
func main() {
  sites := []string{ /* sites */ }
  done := make(chan bool)
  for _, v := range sites {
    go fetch(v, done)
  }
  for i := 0; i < len(sites); i++ {
    <-done
  }
  var getTitle func(n *html.Node)
  getTitle = func(n *html.Node) {
    if n.Type == html.ElementNode && n.Data == "title" {
      fmt.Println(n.FirstChild.Data)
    }
    for c := n.FirstChild; c != nil; c = c.NextSibling {
      getTitle(c)
    }
  }
  for _, v := range responses {
    doc, err := html.Parse(v.Body)
    if err != nil {
      fmt.Printf("error: parsing website body: %v\n", err)
      continue
    }
    fmt.Printf("%s - ", v.Request.URL)
    getTitle(doc)
  }
}

Самая важная часть в приведенном выше коде — это функция выборки. Как видите, он блокирует мьютекс «mu» перед добавлением тела ответа в глобальную переменную ответов. В противном случае, если два запроса завершатся почти одновременно, они могут быть неправильно добавлены в наш массив ответов.

Подводя итог, мьютексы важны в параллельных программах, чтобы гарантировать, что переменная может быть доступна только по одной, а не одновременно, чтобы предотвратить состояние гонки при обновлении этой переменной, где она не обновляется правильно.