Skip to content

Commit

Permalink
Revisão do capítulo
Browse files Browse the repository at this point in the history
  • Loading branch information
larien committed Dec 25, 2019
1 parent 670ddab commit 7d754a4
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 113 deletions.
183 changes: 71 additions & 112 deletions primeiros-passos-com-go/sync.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,27 @@

[**Você pode encontrar todo o código para esse capítulo aqui**](https://github.com/larien/learn-go-with-tests/tree/master/sync)

Queremos fazer um contador que é seguro para se usar concorrentemente.
Queremos fazer um contador que é seguro para ser usado concorrentemente.

Vamos começar com um contador inseguro e verificar se seu comportamento funciona em um ambiente com apenas uma thread.
Vamos começar com um contador não seguro e verificar se seu comportamento funciona em um ambiente com apenas uma _thread_.

Depois vamos testar sua falta de segurança com múltiplas *goroutines* tentando
usar o contador através de teste e consertar essa falha.
Em seguida, vamos testar sua falta de segurança com várias *goroutines* tentando usar o contador dentro dos testes e consertar essa falha.

## Escreva o teste primeiro

Queremos que nossa API nos dê um método para incrementar o contador e depois recupere esse valor.

```go
func TestContador(t *testing.T) {
t.Run("incrementar o contador 3 vezes o deixa com valor 3", func(t *testing.T) {
t.Run("incrementar o contador 3 vezes resulta no valor 3", func(t *testing.T) {
contador := Contador{}
contador.Incrementa()
contador.Incrementa()
contador.Incrementa()

if contador.Valor() != 3 {
t.Errorf("recebido %d, desejado %d", contador.Valor(), 3)
}
t.Errorf("resultado %d, esperado %d", contador.Valor(), 3)
}
})
}
```
Expand All @@ -34,7 +33,7 @@ func TestContador(t *testing.T) {
./sync_test.go:9:14: undefined: Contador
```

## Escreva a quantidade mínima de código para o teste rodar e verifique a saída do teste que falhou
## Escreva o mínimo de código possível para fazer o teste rodar e verifique a saída do teste que tiver falhado

Vamos definir `Contador`.

Expand All @@ -44,7 +43,7 @@ type Contador struct {
}
```

Tente de novo e ele falhará com o seguinte erro:
Tente rodar o teste de novo e ele falhará com o seguinte erro:

```text
./sync_test.go:14:10: contador.Incrementa undefined (type Contador has no field or method Incrementa)
Expand All @@ -67,17 +66,15 @@ Agora tudo deve rodar e falhar:

```text
=== RUN TestContador
=== RUN TestContador/incrementar_o_contador_3_vezes_o_deixa_com_valor_3
=== RUN TestContador/incrementar_o_contador_3_vezes_resulta_no_valor_3
--- FAIL: TestContador (0.00s)
--- FAIL: TestContador/incrementar_o_contador_3_vezes_o_deixa_com_valor_3 (0.00s)
sync_test.go:27: recebido 0, desejado 3
--- FAIL: TestContador/incrementar_o_contador_3_vezes_resulta_no_valor_3 (0.00s)
sync_test.go:27: resultado 0, esperado 3
```

## Escreva código o suficiente para fazer o teste passar

Isso deve ser simples para _experts_ em Go como nós. Precisamos manter algum
estado do contador no nosso datatype e daí incrementá-lo em cada chamada do
`Incrementa`.
Isso deve ser simples para _experts_ em Go como nós. Precisamos criar uma instância do tipo Contador e incrementá-lo com cada chamada de `Incrementa`.


```go
Expand All @@ -96,89 +93,76 @@ func (c *Contador) Valor() int {

## Refatoração

Não há muito o que refatorar, mas, dado que iremos escrever mais testes em
torno do `Contador`, vamos escrever uma pequena função de asserção `verificaContador`
para que o teste fique um pouco mais legível.
Não há muito o que refatorar, mas já que iremos escrever mais testes em torno do `Contador`, vamos escrever uma pequena função de asserção `verificaContador` para que o teste fique um pouco mais legível.


```go
t.Run("incrementar o contador 3 vezes o deixa com valor 3", func(t *testing.T) {
contador := Contador{}
contador.Incrementa()
contador.Incrementa()
contador.Incrementa()
t.Run("incrementar o contador 3 vezes resulta no valor 3", func(t *testing.T) {
contador := Contador{}
contador.Incrementa()
contador.Incrementa()
contador.Incrementa()

verificaContador(t, contador, 3)
verificaContador(t, contador, 3)
})

func verificaContador(t *testing.T, recebido Contador, desejado int) {
t.Helper()
if recebido.Valor() != desejado {
t.Errorf("recebido %d, quero receber %d", recebido.Valor(), desejado)
}
func verificaContador(t *testing.T, resultado Contador, esperado int) {
t.Helper()
if resultado.Valor() != esperado {
t.Errorf("resultado %d, esperado %d", resultado.Valor(), esperado)
}
}
```

## Próximos passos

Isso foi muito fácil, mas agora nós temos um requerimento que é: ele precisa
ser seguro o suficiente para ser usado em um ambiente com acesso concorrente.
Vamos precisar criar um teste para exercitar isso.
Isso foi muito fácil, mas agora temos um requerimento que é: o programa precisa ser seguro o suficiente para ser usado em um ambiente com acesso concorrente. Vamos precisar criar um teste para exercitar isso.

## Escreva o teste primeiro

```go
t.Run("roda concorrentemente com segurança", func(t *testing.T) {
contadorDesejado := 1000
contador := Contador{}

var wg sync.WaitGroup
wg.Add(contadorDesejado)

for i:=0; i<contadorDesejado; i++ {
go func(w *sync.WaitGroup) {
contador.Incrementa()
w.Done()
}(&wg)
}
wg.Wait()

verificaContador(t, contador, contadorDesejado)
t.Run("roda concorrentemente em segurança", func(t *testing.T) {
contagemEsperada := 1000
contador := NovoContador()

var wg sync.WaitGroup
wg.Add(contagemEsperada)

for i := 0; i < contagemEsperada; i++ {
go func(w *sync.WaitGroup) {
contador.Incrementa()
w.Done()
}(&wg)
}
wg.Wait()

verificaContador(t, contador, contagemEsperada)
})
```

Isso vai iterar pelo nosso `contadorDesejado` e disparar uma *goroutine* para chamar `contador.Incrementa()`.
Isso vai iterar até a nossa `contagemEsperada` e disparar uma *goroutine* para chamar `contador.Incrementa()` a cada iteração.

Nós estamos usando [`sync.WaitGroup`](https://golang.org/pkg/sync/#WaitGroup)
que é uma maneira conveniente de sincronizar processos concorrentes.
Estamos usando [`sync.WaitGroup`](https://golang.org/pkg/sync/#WaitGroup), que é uma maneira simples de sincronizar processos concorrentes.

> Um WaitGroup aguarda por uma coleção de *goroutines* terminar seu processamento.
A *goroutine* principal faz a chamada para o Add definir o número de *goroutines*
que serão esperadas. Então, cada uma das *goroutines* roda novamente e chama
Done quando terminar sua execução. Ao mesmo tempo, Wait pode ser usada para
bloquear a execução até que todas as *goroutines* tenham terminado.
> Um WaitGroup aguarda por uma coleção de *goroutines* terminar seu processamento. A *goroutine* principal faz a chamada para o `Add` definir o número de *goroutines* que serão esperadas. Então, cada uma das *goroutines* é executada e chama `Done` quando termina sua execução. Ao mesmo tempo, `Wait` pode ser usado para bloquear a execução até que todas as *goroutines* tenham terminado.
Ao esperar por `wg.Wait()` terminar sua execução antes de fazer nossas asserções, nós
podemos ter certeza que todas as nossas *goroutines* tentaram `Incrementa` o `Contador`.
Ao esperar por `wg.Wait()` terminar sua execução antes de fazer nossas asserções, podemos ter certeza que todas as nossas *goroutines* tentaram chamar o `Incrementa` no `Contador`.

## Tente rodar o teste

```text
=== RUN TestContador/roda_concorrentemente_em_seguranca
--- FAIL: TestContador (0.00s)
--- FAIL: TestContador/roda_concorrentemente_em_seguranca (0.00s)
sync_test.go:26: recebido 939, desejado 1000
sync_test.go:26: resultado 939, esperado 1000
FAIL
```

O teste _provavelmente_ vai falhar com um número diferente, mas de toda forma
ele demonstra que não roda corretamente quando várias *goroutines* tentam
mudar o valor do contador ao mesmo tempo.
O teste _provavelmente_ vai falhar com um número diferente, mas de qualquer forma demonstra que não roda corretamente quando várias *goroutines* tentam mudar o valor do contador ao mesmo tempo.

## Escreva código o suficiente para fazer o teste passar

Uma solução simples é adicionar uma trava ao nosso `Contador`, um
[`Mutex`](https://golang.org/pkg/sync/#Mutex).
Uma solução simples é adicionar uma trava ao nosso `Contador`, um [`Mutex`](https://golang.org/pkg/sync/#Mutex).

> Um Mutex é uma trava de exclusão mútua. O valor zero de um Mutex é um Mutex destravado.
Expand All @@ -195,14 +179,11 @@ func (c *Contador) Incrementa() {
}
```

Isso significa que qualquer *goroutine* chamando `Incrementa` vai receber a trava em `Contador`
se for a primeira. Todas as outras *goroutines* vão ter que esperar por ele até
que ele esteja `Unlock`, ou destravado, antes de ganhar o acesso.
Isso significa que qualquer *goroutine* chamando `Incrementa` vai receber a trava em `Contador` se for a primeira chamando essa função. Todas as outras *goroutines* vão ter que esperar por essa primeira execução até que ele esteja `Unlock`, ou destravado, antes de ganhar o acesso à instância de `Contador` alterada pela primeira chamada de função.

Agora se você rodar o teste novamente, ele deve funcionar porque cada uma das *goroutines*
têm que esperar até que seja sua vez antes de fazer alguma mudança.
Agora, se você rodar o teste novamente, ele deve funcionar porque cada uma das *goroutines* tem que esperar até que seja sua vez antes de fazer alguma mudança.

## Eu vi outros exemplos nos quais `sync.Mutex` está embutido dentro da struct.
## vi outros exemplos em que o `sync.Mutex` está embutido dentro da struct.

Você pode ver exemplos como esse:

Expand All @@ -213,7 +194,7 @@ type Contador struct {
}
```

Há quem diga que isso pode tornar o código um pouco mais elegante.
Há quem diga que isso torna o código um pouco mais elegante.

```go
func (c *Contador) Incrementa() {
Expand All @@ -223,22 +204,15 @@ func (c *Contador) Incrementa() {
}
```

Isso _parece_ legal, mas, apesar de programação ser uma área altamente
subjetiva, isso é **feio e errado**.
Isso _parece_ legal, mas, apesar de programação ser uma área altamente subjetiva, isso é **feio e errado**.

Às vezes as pessoas esquecem que tipos embutidos significam que os métodos
daquele tipo se tornam _parte da interface pública_; e você geralmente não
quer isso. Não se esqueçam que devemos ter muito cuidado com as nossas APIs
públicas. O momento que tornamos algo público é o momento que outros códigos
podem acoplar-se a ele e nós queremos evitar acoplamentos desnecessários.
Às vezes as pessoas esquecem que tipos embutidos significam que os métodos daquele tipo se tornam _parte da interface pública_; e você geralmente não quer isso. Não se esqueçam que devemos ter muito cuidado com as nossas APIs públicas. O momento que tornamos algo público é o momento que outros códigos podem acoplar-se a ele e queremos evitar acoplamentos desnecessários.

Expôr `Lock` e `Unlock` é, no seu melhor caso, muito confuso e, no seu pior
caso, potencialmente perigoso para o seu software se quem chamar o seu tipo
começar a chamar esses métodos diretamente.
Expôr `Lock` e `Unlock` é, no seu melhor caso, muito confuso e, no seu pior caso, potencialmente perigoso para o seu software se quem chamar o seu tipo começar a chamar esses métodos diretamente.

![Demonstração de como um usuário dessa API pode chamar erroneamente o estado da trava](https://i.imgur.com/SWYNpwm.png)

_Isso parece uma péssima ideia_
_Isso parece uma péssima ideia._

## Copiando mutexes

Expand All @@ -251,25 +225,19 @@ sync/v2/sync_test.go:16: call of verificaContador copies lock valor: v1.Contador
sync/v2/sync_test.go:39: verificaContador passes lock by valor: v1.Contador contains sync.Mutex
```

Uma rápida olhada na documentação do [`sync.Mutex`](https://golang.org/pkg/sync/#Mutex)
nos diz o porquê:
Uma rápida olhada na documentação do [`sync.Mutex`](https://golang.org/pkg/sync/#Mutex) nos diz o porquê:

> Um Mutex não deve ser copiado depois do primeiro uso.
Quando passamos nosso `Contador` \(por valor\) para `verificaContador`,
ele vai tentar criar uma cópia do mutex.
Quando passamos nosso `Contador` \(por valor\) para `verificaContador`, ele vai tentar criar uma cópia do mutex.

Para resolver isso, devemos passar um ponteiro para o nosso `Contador`.
Vamos, então, mudar a assinatura de `verificaContador`.
Para resolver isso, devemos passar um ponteiro para o nosso `Contador`. Vamos, então, mudar a assinatura de `verificaContador`.

```go
func verificaContador(t *testing.T, recebido *Contador, desejado int)
func verificaContador(t *testing.T, resultado *Contador, esperado int)
```

Nossos testes não vão mais compilar porque estamos tentando passar um `Contador`
em vez de um `*Contador`. Para resolver isso, é preferível criar um construtor
que mostra aos usuários da nossa API que seria melhor não inicializar o tipo
ele mesmo.
Nossos testes não vão mais compilar porque estamos tentando passar um `Contador` ao invés de um `*Contador`. Para resolver isso, é melhor criar um construtor que mostra aos usuários da nossa API que seria melhor ele mesmo não inicializar seu tipo.


```go
Expand All @@ -280,24 +248,20 @@ func NovoContador() *Contador {

Use essa função em seus testes quando for inicializar o `Contador`.

## Resumindo
## Resumo

Cobrimos algumas coisas do [pacote sync](https://golang.org/pkg/sync/):
Falamos sobre algumas coisas do [pacote sync](https://golang.org/pkg/sync/):

* `Mutex` que nos permite adicionar travas aos nossos dados
* `Waitgroup` que é uma maneira de esperar as *goroutines* terminarem suas tarefas
* `Mutex` nos permite adicionar travas aos nossos dados
* `WaitGroup` é uma maneira de esperar as *goroutines* terminarem suas tarefas

### Quando usar travas em vez de *channels* e *goroutines*?

[Anteriormente cobrimos *goroutines* no primeiro capítulo de concorrência](concorrencia.md)
[Anteriormente falamos sobre *goroutines* no primeiro capítulo sobre concorrência](concorrencia.md)
que nos permite escrever código concorrente e seguro, então por que usar travas?
[A wiki do go tem uma página dedicada para esse tópico: Mutex ou Channel?](https://github.com/golang/go/wiki/MutexOrChannel)
[A wiki do Go tem uma página dedicada para esse tópico: Mutex ou Channel?](https://github.com/golang/go/wiki/MutexOrChannel)

> Um erro comum de um iniciante em Go é usar demais os *channels* e *goroutines* apenas
porque é possível e/ou porque é divertido. Não tenha medo de usar um `sync.Mutex` se
ele se encaixar melhor no seu problema. Go é pragmático em deixar você escolher as
ferramentas que melhor resolvem o seu problema e não te força em um único estilo
de código.
> Um erro comum de um iniciante em Go é usar demais os *channels* e *goroutines* apenas porque é possível e/ou porque é divertido. Não tenha medo de usar um `sync.Mutex` se for uma solução melhor para o seu problema. Go é pragmático em deixar você escolher as ferramentas que melhor resolvem o seu problema e não te força em um único estilo de código.
Resumindo:

Expand All @@ -306,16 +270,11 @@ Resumindo:

### go vet

Não se esqueça de usar `go vet` nos seus scripts de build porque ele pode te alertar a respeito de bugs mais
sutis no seu código antes que eles atinjam seus pobres usuários.
Não se esqueça de usar `go vet` nos seus scripts de _build_ porque ele pode te alertar a respeito de bugs mais sutis no seu código antes que eles atinjam seus pobres usuários.

### Não use códigos embutidos apenas porque é conveniente

* Pense a respeito do efeito que embutir códigos tem na sua API pública.
* Você _realmente_ quer expôr esses métodos e ter pessoas acoplando o código
próprio delas a ele?
* Em relação a mutexes, pode ser potencialmente um desastre de maneiras
muito imprevisíveis e estranhas. Imagine algum código obscuro destravando um
mutex quando não deveria; isso causaria erros muito estranhos e que seriam bastante
difíceis de encontrar.
* Você _realmente_ quer expôr esses métodos e ter pessoas acoplando o código próprio delas a ele?
* Mutexes podem se tornar um desastre de maneiras muito imprevisíveis e estranhas. Imagine um código inesperado destravando um mutex quando não deveria? Isso causaria erros muito estranhos que seriam muito difíceis de encontrar.

2 changes: 1 addition & 1 deletion sync/v1/sync_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
)

func TestContador(t *testing.T) {
t.Run("incrementar o contador 3 vezes o deixa com valor 3", func(t *testing.T) {
t.Run("incrementar o contador 3 vezes resulta no valor 3", func(t *testing.T) {
contador := Contador{}
contador.Incrementa()
contador.Incrementa()
Expand Down

0 comments on commit 7d754a4

Please sign in to comment.