Até aqui na série os dados só andaram em um sentido. Um gerador escreve, um trabalhador lê, um estágio repassa.

Mas e quando eu mando um pedido e quero a resposta de volta? Preciso de um canal de ida e outro de volta. E se o de volta fosse dentro do pedido?

O que é requisição e resposta Link para o cabeçalho

Canais são valores como qualquer outro em Go. Dá para guardar em variável, passar como parâmetro e colocar dentro de uma struct.

Então quem pede inclui na mensagem o canal pelo qual quer a resposta, e fica lendo dele. Quem atende processa e responde no canal que veio na mensagem.

Ele também aparece como canal de resposta ou RPC interno.

type requisicao struct {
	valor    int
	resposta chan<- int
}

// atende uma requisição por vez e responde no canal que veio nela
func servico(entrada <-chan requisicao) {
	for req := range entrada {
		req.resposta <- req.valor * 2
	}
}

func main() {
	entrada := make(chan requisicao)
	pronto := make(chan struct{})
	go func() {
		servico(entrada)
		close(pronto)
	}()

	for i := range 5 {
		// um canal novo por requisição
		resposta := make(chan int, 1)
		entrada <- requisicao{valor: i, resposta: resposta}
		// fica parado até o serviço responder
		fmt.Println("resposta:", <-resposta)
	}

	close(entrada)
	<-pronto
}
$ go run .
resposta: 0
resposta: 2
resposta: 4
resposta: 6
resposta: 8

Repare no chan<- int do campo resposta. O serviço só escreve nele, e o compilador impede que ele leia a própria resposta por engano.

Preste atenção também no make dentro do laço. Cada requisição leva o seu canal, e é ele que diz para o serviço onde depositar a resposta, como um envelope com o endereço do remetente. Nenhuma variável é compartilhada, pedido e resposta viajam por canais.

Mas por que a vaga no canal de resposta? Link para o cabeçalho

O make(chan int, 1) tem uma vaga de buffer. No exemplo ela nunca é usada, porque a main sempre lê a resposta. Dá vontade de tirar.

Eu tirei. E para parecer um serviço de verdade, deixei a primeira requisição demorar 200 milissegundos e quem pede desistir depois de 100, com um select e um time.After:

$ go run .
desisti de esperar a requisição 0
fatal error: all goroutines are asleep - deadlock!

Opa! Quem pediu desistiu, foi mandar a segunda requisição e o programa morreu.

O serviço terminou de processar a primeira, tentou responder, e ninguém estava lendo o canal. Como ele não tem vaga, o serviço ficou preso no envio, e a segunda requisição nunca foi atendida.

Com a vaga de volta:

$ go run . vaga
desisti de esperar a requisição 0
servico: respondeu 0
servico: respondeu 2
resposta: 2
servico: respondeu 4
resposta: 4
fim

O serviço deposita a resposta 0 na vaga, segue em frente, e as outras duas são atendidas. A resposta que ninguém leu vai embora junto com o canal, quando o coletor de lixo passar.

Logo, a vaga não é otimização. É o que impede uma desistência de quem pediu de travar o serviço para todo mundo.

Restaurando a sequência Link para o cabeçalho

Na palestra Go Concurrency Patterns o Rob Pike usa a mesma ideia para outra coisa. No fan-in os valores das entradas chegam misturados, e ele quer controlar o passo.

Cada mensagem carrega um canal wait, e quem produziu só manda a próxima depois que o leitor avisa, por esse canal, que terminou de processar a anterior. Ele chama isso de restoring sequencing, e é requisição e resposta com os papéis trocados: a resposta é só um “pode continuar”.

De onde veio o exemplo Link para o cabeçalho

O código está na pasta requisicao_resposta do concorrencia-go. O teste de lá roda a main inteira e confere as cinco respostas.

No próximo vem a gorrotina dona do estado. É este padrão com um mapa dentro do serviço, atendendo leituras e escritas por canais, e a versão com mutex do lado para comparar.

Um canal por requisição é barato, mas se quem pede pode desistir, cada um deles precisa da vaga. Sem ela, basta uma desistência para parar o serviço.

Então é isso, pessoal!

Até a próxima!

{}’s