Channels com buffer
1. Introdução aos Channels com Buffer
Channels com buffer em Golang são canais que possuem uma capacidade interna de armazenamento, permitindo que dados sejam enviados sem que haja um receptor imediato. Diferentemente dos channels sem buffer, onde o envio bloqueia até que outra goroutine receba o dado, os channels com buffer oferecem um espaço temporário para armazenar mensagens.
A sintaxe de criação é simples:
ch := make(chan int, 3) // canal com buffer de capacidade 3
O funcionamento básico segue estas regras:
- O envio bloqueia apenas quando o buffer está completamente cheio
- O recebimento bloqueia apenas quando o buffer está vazio
- Enquanto houver espaço no buffer, o produtor pode continuar enviando sem esperar o consumidor
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan string, 2)
// Envio não bloqueia pois buffer tem espaço
ch <- "mensagem 1"
ch <- "mensagem 2"
fmt.Println("Ambas mensagens enviadas sem bloqueio")
// Recebimento não bloqueia pois buffer tem dados
msg1 := <-ch
msg2 := <-ch
fmt.Println(msg1, msg2)
}
2. Comportamento de Bloqueio e Não-Bloqueio
O comportamento de bloqueio em channels com buffer é mais flexível que em channels sem buffer. Vamos explorar os cenários possíveis:
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int, 2)
// Buffer parcialmente preenchido - operações não-bloqueantes
ch <- 1 // não bloqueia
ch <- 2 // não bloqueia
// Buffer cheio - próximo envio bloqueia
go func() {
ch <- 3 // bloqueia até que alguém receba
fmt.Println("Terceiro valor enviado")
}()
time.Sleep(100 * time.Millisecond)
fmt.Println("Consumindo um valor...")
<-ch // libera espaço para o terceiro envio
time.Sleep(100 * time.Millisecond)
}
Quando o buffer está vazio, o recebimento bloqueia até que um novo dado seja enviado:
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int, 3)
go func() {
time.Sleep(1 * time.Second)
ch <- 42
}()
fmt.Println("Aguardando recebimento...")
valor := <-ch // bloqueia por 1 segundo
fmt.Println("Recebido:", valor)
}
3. Capacidade e Desempenho
Escolher o tamanho adequado do buffer é crucial para o desempenho da aplicação. Não existe uma fórmula mágica, mas algumas diretrizes ajudam:
package main
import (
"fmt"
"time"
)
func processarComBuffer(tamanhoBuffer int) time.Duration {
inicio := time.Now()
ch := make(chan int, tamanhoBuffer)
go func() {
for i := 0; i < 100000; i++ {
ch <- i
}
close(ch)
}()
for range ch {
// processa
}
return time.Since(inicio)
}
func main() {
fmt.Println("Buffer 1:", processarComBuffer(1))
fmt.Println("Buffer 10:", processarComBuffer(10))
fmt.Println("Buffer 100:", processarComBuffer(100))
fmt.Println("Buffer 1000:", processarComBuffer(1000))
}
Trade-offs importantes:
- Buffer pequeno: menor latência individual, mas mais bloqueios e trocas de contexto
- Buffer grande: maior throughput em picos, mas maior uso de memória e menor sincronismo
- Buffer muito grande: pode mascarar problemas de desempenho e aumentar o consumo de memória
4. Padrões de Uso com Buffer
Produtor-Consumidor com Buffer
O padrão mais comum, suavizando picos de produção:
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
func produtor(ch chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for i := 0; i < 10; i++ {
valor := rand.Intn(100)
ch <- valor
fmt.Printf("Produziu: %d\n", valor)
time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)
}
}
func consumidor(ch <-chan int, wg *sync.WaitGroup) {
defer wg.Done()
for valor := range ch {
fmt.Printf("Consumiu: %d\n", valor)
time.Sleep(200 * time.Millisecond) // consumidor mais lento
}
}
func main() {
ch := make(chan int, 5) // buffer suaviza produção mais rápida
var wg sync.WaitGroup
wg.Add(2)
go produtor(ch, &wg)
go consumidor(ch, &wg)
wg.Wait()
close(ch)
}
Uso como Semáforo para Limitar Concorrência
package main
import (
"fmt"
"sync"
"time"
)
func main() {
semaforo := make(chan struct{}, 3) // máximo 3 goroutines simultâneas
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
semaforo <- struct{}{} // adquire permissão
defer func() { <-semaforo }() // libera permissão
fmt.Printf("Goroutine %d executando\n", id)
time.Sleep(1 * time.Second)
}(i)
}
wg.Wait()
}
5. Fechamento e Range em Channels com Buffer
O fechamento de channels com buffer segue as mesmas regras dos channels sem buffer, mas com uma diferença importante: dados ainda presentes no buffer podem ser lidos após o fechamento.
package main
import (
"fmt"
)
func main() {
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
close(ch) // fecha o canal, mas dados ainda estão no buffer
// Iteração segura com for range
for valor := range ch {
fmt.Println("Valor do buffer:", valor)
}
// Verificação de estado com segundo valor booleano
ch2 := make(chan string, 2)
ch2 <- "dado"
close(ch2)
valor, ok := <-ch2
fmt.Printf("Valor: %s, Channel aberto: %v\n", valor, ok)
valor, ok = <-ch2
fmt.Printf("Valor: %s, Channel aberto: %v\n", valor, ok)
}
6. Armadilhas e Boas Práticas
Deadlock com Buffer
package main
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
ch <- 3 // deadlock! buffer cheio e ninguém recebe
}
Vazamento de Goroutines
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int, 2)
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
// Se não fechar o canal, consumidor pode ficar esperando
close(ch)
}()
for valor := range ch {
fmt.Println(valor)
if valor == 2 {
break // consumidor saiu, mas produtor continua?
}
}
time.Sleep(100 * time.Millisecond) // produtor pode estar bloqueado
}
Uso Excessivo de Buffer
// Evite: buffer muito grande que esconde problemas de coordenação
ch := make(chan int, 1000000)
// Prefira: buffer moderado com monitoramento
ch := make(chan int, 100)
7. Comparação com Channels Sem Buffer
Quando usar buffer (comunicação assíncrona):
// Produtor mais rápido que consumidor
ch := make(chan Task, 100) // buffer suaviza picos
Quando evitar buffer (sincronização estrita):
// Sincronização precisa entre goroutines
ch := make(chan struct{}) // sem buffer, garantia de sincronismo
Exemplo prático de migração:
Antes (sem buffer):
func processar(dados []int) []int {
ch := make(chan int)
resultado := make([]int, 0, len(dados))
go func() {
for _, d := range dados {
ch <- d * 2
}
close(ch)
}()
for v := range ch {
resultado = append(resultado, v)
}
return resultado
}
Depois (com buffer):
func processarComBuffer(dados []int) []int {
ch := make(chan int, len(dados))
resultado := make([]int, 0, len(dados))
go func() {
for _, d := range dados {
ch <- d * 2 // não bloqueia se buffer tiver espaço
}
close(ch)
}()
for v := range ch {
resultado = append(resultado, v)
}
return resultado
}
Channels com buffer são ferramentas poderosas quando usadas corretamente. Eles oferecem flexibilidade na comunicação entre goroutines, mas exigem cuidado para evitar deadlocks e vazamentos. A escolha do tamanho do buffer deve ser baseada em medições reais de desempenho e nos requisitos específicos da aplicação.
Referências
- Documentação Oficial do Go: Channels — Tour interativo do Go explicando channels com buffer
- Effective Go: Channels — Guia oficial do Go sobre boas práticas com channels
- Go by Example: Channel Buffering — Exemplos práticos e concisos de channels com buffer
- Dave Cheney: Channel Buffering Patterns — Artigo técnico sobre axiomas e padrões de channels em Go
- The Go Blog: Share Memory By Communicating — Postagem oficial do blog do Go sobre comunicação entre goroutines
- Concurrency in Go: Tools and Techniques — Livro referência sobre concorrência em Go, incluindo channels com buffer