---
id: 01933e13f963942e4c9bc8c3c89fbc2b
author: snowmerak
title: Kit Inicial de Concorrência em Go
description: Apresentamos diversas ferramentas e técnicas para o gerenciamento de concorrência em Go, incluindo goroutines, canais e mutexes.
language: pt
date: 2024-10-18T05:51:30.010680581Z
path: /blog/posts/go-concurrency-starter-pack-z221a3399
category: concurrency
---

## Visão geral

### Breve introdução

A linguagem Go possui muitas ferramentas para o gerenciamento de concorrência. Neste artigo, apresentaremos algumas delas e alguns truques.

### Goroutines?

A goroutine é um novo modelo de concorrência suportado pela linguagem Go. Geralmente, para executar várias tarefas simultaneamente, um programa recebe threads do SO do sistema operacional e executa trabalhos em paralelo, na quantidade de núcleos disponíveis. Para realizar concorrência em uma escala menor, são criadas threads verdes (green threads) no espaço do usuário (userland), de modo que várias threads verdes se revezem na execução dentro de uma única thread do SO. No entanto, as goroutines tornaram essa forma de thread verde ainda menor e mais eficiente. Essas goroutines utilizam menos memória do que as threads e podem ser criadas e alternadas mais rapidamente do que elas.

Para usar goroutines, basta utilizar a palavra-chave `go`. Isso permite que, no processo de escrita de um programa, código síncrono seja executado de forma intuitiva como código assíncrono.

```go
package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan struct{})
    go func() {
        defer close(ch)
        time.Sleep(1 * time.Second)
        fmt.Println("Hello, World!")
    }()

    fmt.Println("Waiting for goroutine...")
    for range ch {}
}
```

Este código transforma um código síncrono, que espera 1 segundo e então imprime `Hello, World!`, em um fluxo assíncrono. Embora o exemplo atual seja simples, ao converter código síncrono mais complexo em código assíncrono, a legibilidade, a visibilidade e a compreensão do código tornam-se melhores do que com métodos existentes como async/await ou promise.

No entanto, em muitos casos, sem compreender o fluxo de simplesmente chamar código síncrono de forma assíncrona e fluxos como `fork & join` (semelhantes a dividir para conquistar), podem ser criados códigos de goroutine inadequados. Para lidar com essas situações, apresentaremos neste artigo alguns métodos e técnicas.

## Gerenciamento de concorrência

### context

Pode ser surpreendente que o `context` seja a primeira técnica de gerenciamento apresentada. Porém, na linguagem Go, o `context` vai além de uma simples função de cancelamento e desempenha um papel excelente no gerenciamento de toda a árvore de tarefas. Para quem não conhece, explicaremos brevemente esse pacote.

```go
package main

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    go func() {
        <-ctx.Done()
        fmt.Println("Context is done!")
    }()

    time.Sleep(1 * time.Second)

    cancel()

    time.Sleep(1 * time.Second)
}
```

O código acima utiliza `context` para imprimir `Context is done!` após 1 segundo. O `context` permite verificar se houve cancelamento por meio do método `Done()` e oferece diversos métodos de cancelamento, como `WithCancel`, `WithTimeout`, `WithDeadline` e `WithValue`.

Vamos criar um exemplo simples. Suponha que você escreva um código que utilize o padrão `aggregator` para buscar `user`, `post` e `comment`, e que todas as requisições devam ser concluídas em 2 segundos. Nesse caso, pode-se escrever da seguinte forma.

```go
package main

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 2 * time.Second)
    defer cancel()

    ch := make(chan struct{})
    go func() {
        defer close(ch)
        user := getUser(ctx)
        post := getPost(ctx)
        comment := getComment(ctx)

        fmt.Println(user, post, comment)
    }()

    select {
    case <-ctx.Done():
        fmt.Println("Timeout!")
    case <-ch:
        fmt.Println("All data is fetched!")
    }
}
```

O código acima imprime `Timeout!` caso não consiga buscar todos os dados em 2 segundos, e imprime `All data is fetched!` caso consiga buscar todos os dados. Dessa forma, ao utilizar `context`, é possível gerenciar facilmente cancelamentos e timeouts mesmo em códigos com várias goroutines em execução.

Diversas funções e métodos relacionados ao context podem ser consultados em [godoc context](https://pkg.go.dev/context). Espero que, após estudar o básico, você consiga utilizá-los com facilidade.

### channel

#### unbuffered channel

`channel` é uma ferramenta para comunicação entre goroutines. Um `channel` pode ser criado com `make(chan T)`. Aqui, `T` é o tipo de dado que o `channel` transmitirá. Dados são enviados e recebidos com `<-`, e o `channel` pode ser fechado com `close`.

```go
package main

func main() {
    ch := make(chan int)
    go func() {
        ch <- 1
        ch <- 2
        close(ch)
    }()

    for i := range ch {
        fmt.Println(i)
    }
}
```

O código acima utiliza `channel` para imprimir 1 e 2. Este exemplo mostra apenas o envio e o recebimento simples de valores pelo channel, mas o `channel` oferece mais funcionalidades do que isso. Primeiro, vamos conhecer o `buffered channel` e o `unbuffered channel`. Antes de começar, o exemplo acima é um `unbuffered channel`, em que a ação de enviar dados ao canal e a ação de receber dados devem ocorrer simultaneamente. Se essas ações não ocorrerem ao mesmo tempo, pode ocorrer um deadlock.

#### buffered channel

E se, em vez de apenas imprimir, o código acima executasse dois processos pesados? Se o segundo processo, ao ler e processar dados, demorar muito tempo, o primeiro processo também ficará parado durante esse período. Para evitar essa situação, podemos usar um `buffered channel`.

```go
package main

func main() {
    ch := make(chan int, 2)
    go func() {
        ch <- 1
        ch <- 2
        close(ch)
    }()

    for i := range ch {
        fmt.Println(i)
    }
}
```

O código acima utiliza `buffered channel` para imprimir 1 e 2. Neste código, por meio do `buffered channel`, não é necessário que a ação de enviar dados ao canal e a ação de receber dados ocorram simultaneamente. Ao colocar um buffer no canal, há folga equivalente ao seu tamanho, o que permite evitar atrasos de trabalho causados pela influência de tarefas posteriores.

#### select

Ao lidar com vários canais, é possível implementar facilmente a estrutura `fan-in` com a sintaxe `select`.

```go
package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan int, 10)
    ch2 := make(chan int, 10)
    ch3 := make(chan int, 10)

    go func() {
        for {
            ch1 <- 1
            time.Sleep(1 * time.Second)
        }
    }()
    go func() {
        for {
            ch2 <- 2
            time.Sleep(2 * time.Second)
        }
    }()
    go func() {
        for {
            ch3 <- 3
            time.Sleep(3 * time.Second)
        }
    }()

    for i := 0; i < 3; i++ {
        select {
        case v := <-ch1:
            fmt.Println(v)
        case v := <-ch2:
            fmt.Println(v)
        case v := <-ch3:
            fmt.Println(v)
        }
    }
}
```

O código acima cria três canais que periodicamente enviam 1, 2 e 3, e utiliza `select` para receber valores desses canais e imprimi-los. Dessa forma, ao usar `select`, é possível receber dados de vários canais simultaneamente e processar os valores na ordem em que chegam.

#### for range

É possível receber dados de um `channel` de forma simples com `for range`. Quando `for range` é usado com um canal, ele é executado sempre que um dado é adicionado ao canal e encerra o loop quando o canal é fechado.

```go
package main

func main() {
    ch := make(chan int)
    go func() {
        ch <- 1
        ch <- 2
        close(ch)
    }()

    for i := range ch {
        fmt.Println(i)
    }
}
```

O código acima utiliza `channel` para imprimir 1 e 2. Neste código, `for range` é usado para receber e imprimir dados sempre que um dado é adicionado ao canal. Quando o canal é fechado, o loop é encerrado.

Como foi escrito algumas vezes acima, essa sintaxe também pode ser usada como um meio simples de sincronização.

```go
package main

func main() {
    ch := make(chan struct{})
    go func() {
        defer close(ch)
        time.Sleep(1 * time.Second)
        fmt.Println("Hello, World!")
    }()

    fmt.Println("Waiting for goroutine...")
    for range ch {}
}
```

O código acima espera 1 segundo e então imprime `Hello, World!`. Neste código, o `channel` é utilizado para transformar código síncrono em código assíncrono. Dessa forma, ao usar `channel`, é possível converter facilmente código síncrono em assíncrono e definir pontos de `join`.

#### etc

1. Ao enviar ou receber dados em um nil channel, pode ocorrer um loop infinito e um deadlock.
2. Ao enviar dados depois de fechar um canal, ocorre um panic.
3. Mesmo sem fechar explicitamente um canal, ele será fechado pelo GC quando for coletado.

### mutex

#### spinlock

`spinlock` é um método de sincronização que tenta continuamente adquirir o lock em um loop. Na linguagem Go, é possível implementar um spinlock de forma simples utilizando ponteiros.

```go
package spinlock

import (
    "runtime"
    "sync/atomic"
)

type SpinLock struct {
    lock uintptr
}

func (s *SpinLock) Lock() {
    for !atomic.CompareAndSwapUintptr(&s.lock, 0, 1) {
        runtime.Gosched()
    }
}

func (s *SpinLock) Unlock() {
    atomic.StoreUintptr(&s.lock, 0)
}

func NewSpinLock() *SpinLock {
    return &SpinLock{}
}
```

O código acima é a implementação do pacote `spinlock`. Nele, o `SpinLock` foi implementado com o pacote `sync/atomic`. O método `Lock` tenta adquirir o lock usando `atomic.CompareAndSwapUintptr`, e o método `Unlock` libera o lock usando `atomic.StoreUintptr`. Como esse método tenta adquirir o lock sem parar, ele consome CPU continuamente até que o lock seja obtido, podendo entrar em um loop infinito. Portanto, o `spinlock` é recomendado apenas para sincronizações simples ou para uso durante períodos curtos.

#### sync.Mutex

`mutex` é uma ferramenta para sincronização entre goroutines. O `mutex` fornecido pelo pacote `sync` oferece os métodos `Lock`, `Unlock`, `RLock`, `RUnlock` e outros. Um `mutex` pode ser criado com `sync.Mutex`, e também é possível utilizar um lock de leitura/escrita com `sync.RWMutex`.

```go
package main

import (
    "sync"
)

func main() {
    var mu sync.Mutex
    var count int

    go func() {
        mu.Lock()
        count++
        mu.Unlock()
    }()

    mu.Lock()
    count++
    mu.Unlock()

    println(count)
}
```

No código acima, duas goroutines acessam quase simultaneamente a mesma variável `count`. Nesse caso, ao transformar o código que acessa a variável `count` em uma região crítica com `mutex`, é possível impedir o acesso concorrente à variável `count`. Assim, este código sempre imprimirá `2`, independentemente de quantas vezes for executado.

#### sync.RWMutex

`sync.RWMutex` é um `mutex` que permite distinguir e utilizar lock de leitura e lock de escrita. É possível adquirir e liberar o lock de leitura com os métodos `RLock` e `RUnlock`.

```go
package cmap

import (
    "sync"
)

type ConcurrentMap[K comparable, V any] struct {
    sync.RWMutex
    data map[K]V
}

func (m *ConcurrentMap[K, V]) Get(key K) (V, bool) {
    m.RLock()
    defer m.RUnlock()

    value, ok := m.data[key]
    return value, ok
}

func (m *ConcurrentMap[K, V]) Set(key K, value V) {
    m.Lock()
    defer m.Unlock()

    m.data[key] = value
}
```

O código acima é a implementação de `ConcurrentMap` utilizando `sync.RWMutex`. Nele, o método `Get` adquire o lock de leitura e o método `Set` adquire o lock de escrita, permitindo acessar e modificar o mapa `data` de forma segura. O motivo de usar lock de leitura é que, em casos com muitas operações de leitura simples, não se bloqueia com lock de escrita; apenas o lock de leitura é adquirido, permitindo que várias goroutines executem operações de leitura simultaneamente. Assim, nos casos em que não há alteração de estado e, portanto, não é necessário o lock de escrita, pode-se melhorar o desempenho utilizando apenas o lock de leitura.

#### fakelock

`fakelock` é um truque simples que implementa `sync.Locker`. Esta estrutura fornece os mesmos métodos que `sync.Mutex`, mas não realiza nenhuma operação real.

```go
package fakelock

type FakeLock struct{}

func (f *FakeLock) Lock() {}

func (f *FakeLock) Unlock() {}
```

O código acima é a implementação do pacote `fakelock`. Esse pacote implementa `sync.Locker` e fornece os métodos `Lock` e `Unlock`, mas, na prática, não executa nenhuma operação. Explicarei por que esse código é necessário em outra oportunidade.

### waitgroup

#### sync.WaitGroup

`sync.WaitGroup` é uma ferramenta que aguarda até que todas as tarefas das goroutines sejam concluídas. Ela fornece os métodos `Add`, `Done` e `Wait`: o método `Add` adiciona a quantidade de goroutines, o método `Done` sinaliza que a tarefa de uma goroutine foi concluída, e o método `Wait` aguarda até que as tarefas de todas as goroutines sejam concluídas.

```go
package main

import (
    "sync"
    "sync/atomic"
)

func main() {
    wg := sync.WaitGroup{}
    c := atomic.Int64{}

    for i := 0; i < 100 ; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            c.Add(1)
        }()
    }

    wg.Wait()
    println(c.Load())
}
```

O código acima utiliza `sync.WaitGroup` para que 100 goroutines somem valores simultaneamente à variável `c`. Neste código, `sync.WaitGroup` é utilizado para aguardar até que todas as goroutines terminem e, em seguida, imprimir o valor acumulado em `c`. Em casos simples de `fork & join` com poucas tarefas, basta usar apenas canais, mas para `fork & join` com grande quantidade de tarefas, usar `sync.WaitGroup` também é uma boa escolha.

#### with slice

Quando usado junto com slices, o `waitgroup` pode se tornar uma ferramenta excelente para gerenciar execuções concorrentes sem locks.

```go
package main

import (
	"fmt"
	"sync"
    "rand"
)

func main() {
	var wg sync.WaitGroup
	arr := [10]int{}

	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()

			arr[id] = rand.Intn(100)
		}(i)
	}

	wg.Wait()
	fmt.Println("Done")

    for i, v := range arr {
        fmt.Printf("arr[%d] = %d\n", i, v)
    }
}
```

O código acima utiliza apenas `waitgroup` para que cada goroutine gere simultaneamente um inteiro aleatório e o armazene no índice que lhe foi atribuído. Neste código, `waitgroup` é utilizado para aguardar até que todas as goroutines terminem e, em seguida, imprimir `Done`. Dessa forma, ao usar `waitgroup`, várias goroutines podem executar tarefas simultaneamente, armazenar dados sem locks até que todas terminem e, após a conclusão, realizar o pós-processamento de forma conjunta.

#### golang.org/x/sync/errgroup.ErrGroup

`errgroup` é um pacote que estende o `sync.WaitGroup`. Diferentemente do `sync.WaitGroup`, o `errgroup` cancela todas as goroutines e retorna o erro caso ocorra um erro em qualquer uma das tarefas.

```go
package main

import (
    "context"
    "fmt"
    "golang.org/x/sync/errgroup"
)

func main() {
    g, ctx := errgroup.WithContext(context.Background())
    _ = ctx

    for i := 0; i < 10; i++ {
        i := i
        g.Go(func() error {
            if i == 5 {
                return fmt.Errorf("error")
            }
            return nil
        })
    }

    if err := g.Wait(); err != nil {
        fmt.Println(err)
    }
}
```

O código acima utiliza `errgroup` para criar 10 goroutines, gerando um erro na quinta goroutine. Propositalmente, um erro foi gerado na quinta goroutine para demonstrar o caso em que ocorre um erro. Na prática, ao usar `errgroup`, basta criar as goroutines e aplicar diversos tipos de pós-processamento para os casos em que ocorram erros em cada uma delas.

### once

É uma ferramenta para executar código que deve ser executado apenas uma vez. O código relacionado pode ser executado pelos construtores abaixo.

```go
func OnceFunc(f func()) func()
func OnceValue[T any](f func() T) func() T
func OnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2)
```

#### OnceFunc

`OnceFunc` simplesmente garante que a função correspondente seja executada apenas uma vez em todo o programa.

```go
package main

import "sync"

func main() {
    once := sync.OnceFunc(func() {
        println("Hello, World!")
    })

    once()
    once()
    once()
    once()
    once()
}
```

O código acima utiliza `sync.OnceFunc` para imprimir `Hello, World!`. Neste código, `sync.OnceFunc` é usado para criar a função `once`, e mesmo que ela seja chamada várias vezes, `Hello, World!` é impresso apenas uma vez.

#### OnceValue

`OnceValue` não apenas garante que a função seja executada uma única vez em todo o programa, mas também armazena o valor de retorno da função e, nas chamadas seguintes, retorna o valor armazenado.

```go
package main

import "sync"

func main() {
    c := 0
    once := sync.OnceValue(func() int {
        c += 1
        return c
    })

    println(once())
    println(once())
    println(once())
    println(once())
    println(once())
}
```

O código acima utiliza `sync.OnceValue` para incrementar a variável `c` em 1. Neste código, `sync.OnceValue` é usado para criar a função `once`, e mesmo que ela seja chamada várias vezes, a variável `c` é incrementada apenas uma vez e retorna 1.

#### OnceValues

`OnceValues` funciona da mesma forma que `OnceValue`, mas pode retornar múltiplos valores.

```go
package main

import "sync"

func main() {
    c := 0
    once := sync.OnceValues(func() (int, int) {
        c += 1
        return c, c
    })

    a, b := once()
    println(a, b)
    a, b = once()
    println(a, b)
    a, b = once()
    println(a, b)
    a, b = once()
    println(a, b)
    a, b = once()
    println(a, b)
}
```

O código acima utiliza `sync.OnceValues` para incrementar a variável `c` em 1. Neste código, `sync.OnceValues` é usado para criar a função `once`, e mesmo que ela seja chamada várias vezes, a variável `c` é incrementada apenas uma vez e retorna 1.

### atomic

O pacote `atomic` fornece operações atômicas. Ele oferece métodos como `Add`, `CompareAndSwap`, `Load`, `Store` e `Swap`, mas recentemente é recomendado o uso de tipos como `Int64`, `Uint64` e `Pointer`.

```go
package main

import (
    "sync"
    "sync/atomic"
)

func main() {
    wg := sync.WaitGroup{}
    c := atomic.Int64{}

    for i := 0; i < 100 ; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            c.Add(1)
        }()
    }

    wg.Wait()
    println(c.Load())
}
```

Este é o exemplo utilizado anteriormente. É o código que incrementa atomicamente a variável `c` usando o tipo `atomic.Int64`. Com os métodos `Add` e `Load`, é possível incrementar a variável atomicamente e ler seu valor. Além disso, o método `Store` armazena um valor, o método `Swap` substitui um valor, e o método `CompareAndSwap` compara um valor e, se for adequado, realiza a substituição.

### cond

#### sync.Cond

O pacote `cond` fornece variáveis de condição. Ele pode ser criado com `sync.Cond` e oferece os métodos `Wait`, `Signal` e `Broadcast`.

```go
package main

import (
    "sync"
)

func main() {
    c := sync.NewCond(&sync.Mutex{})
    ready := false

    go func() {
        c.L.Lock()
        ready = true
        c.Signal()
        c.L.Unlock()
    }()

    c.L.Lock()
    for !ready {
        c.Wait()
    }
    c.L.Unlock()

    println("Ready!")
}
```

O código acima utiliza `sync.Cond` para aguardar até que a variável `ready` se torne `true`. Neste código, `sync.Cond` é utilizado para aguardar até que `ready` seja `true` e, em seguida, imprimir `Ready!`. Dessa forma, ao usar `sync.Cond`, é possível fazer com que várias goroutines aguardem até que determinada condição seja satisfeita.

Com isso, é possível implementar uma `queue` simples.

```go
package queue

import (
    "sync"
    "sync/atomic"
)

type Node[T any] struct {
    Value T
    Next  *Node[T]
}

type Queue[T any] struct {
    sync.Mutex
    Cond *sync.Cond
    Head *Node[T]
    Tail *Node[T]
    Len  int
}

func New[T any]() *Queue[T] {
    q := &Queue[T]{}
    q.Cond = sync.NewCond(&q.Mutex)
    return q
}

func (q *Queue[T]) Push(value T) {
    q.Lock()
    defer q.Unlock()

    node := &Node[T]{Value: value}
    if q.Len == 0 {
        q.Head = node
        q.Tail = node
    } else {
        q.Tail.Next = node
        q.Tail = node
    }
    q.Len++
    q.Cond.Signal()
}

func (q *Queue[T]) Pop() T {
    q.Lock()
    defer q.Unlock()

    for q.Len == 0 {
        q.Cond.Wait()
    }

    node := q.Head
    q.Head = q.Head.Next
    q.Len--
    return node.Value
}
```

Ao utilizar `sync.Cond` dessa forma, em vez de consumir muita CPU com `spin-lock`, é possível aguardar de maneira eficiente e retomar a execução quando a condição for satisfeita.

### semaphore

#### golang.org/x/sync/semaphore.Semaphore

O pacote `semaphore` fornece semáforos. Ele pode ser criado com `golang.org/x/sync/semaphore.Semaphore` e oferece os métodos `Acquire`, `Release` e `TryAcquire`.

```go
package main

import (
    "context"
    "fmt"
    "golang.org/x/sync/semaphore"
)

func main() {
    s := semaphore.NewWeighted(1)

    if s.TryAcquire(1) {
        fmt.Println("Acquired!")
    } else {
        fmt.Println("Not Acquired!")
    }

    s.Release(1)
}
```

O código acima cria um semáforo com `semaphore` e demonstra como adquirir o semáforo com o método `Acquire` e liberá-lo com o método `Release`. Neste código, foi mostrado como adquirir e liberar um semáforo usando `semaphore`.

## Considerações finais

Parece que o conteúdo básico é suficiente até aqui. Espero que, com base neste artigo, você compreenda como gerenciar a concorrência usando goroutines e consiga aplicá-la na prática. Espero que este artigo tenha sido útil para você. Obrigado.
