<h2>A Filosofia de Streams em Go</h2>
<p>A programação tradicional frequentemente trabalha com dados armazenados inteiramente na memória: você carrega um arquivo completo, processa tudo, depois escreve o resultado. Go, porém, abraça uma filosofia diferente através do conceito de <strong>streams</strong>. Um stream é uma sequência de dados que você processa continuamente, sem necessidade de carregar tudo na memória de uma só vez.</p>
<p>Esta abordagem é particularmente poderosa para trabalhar com dados grandes ou em tempo real. Em vez de pensar "tenho um arquivo de 1GB, como carrego tudo?", você pensa "vou processar este dado em pequenos pedaços, conforme ele chega". O pacote <code>io</code> é a base dessa filosofia em Go, fornecendo interfaces simples que permitem criar componentes que trabalham harmoniosamente juntos, independentemente da fonte ou destino dos dados.</p>
<h2>As Interfaces Fundamentais: Reader e Writer</h2>
<h3>Interface Reader</h3>
<p>A interface <code>Reader</code> é o coração da leitura de dados em Go. Ela define apenas um método:</p>
<pre><code class="language-go">type Reader interface {
Read(p []byte) (n int, err error)
}</code></pre>
<p>Quando você implementa <code>Read</code>, está promessendo fazer uma coisa simples: ler até <code>len(p)</code> bytes de dados para dentro do slice <code>p</code> e retornar quantos bytes foram realmente lidos. Se não houver mais dados, você retorna <code>io.EOF</code>. Essa simplicidade é revolucionária porque qualquer coisa que saiba implementar esse método — um arquivo, uma conexão de rede, uma string em memória — pode ser utilizada no mesmo lugar.</p>
<p>Veja um exemplo prático. Vamos criar um <code>Reader</code> customizado que retorna os mesmos dados três vezes:</p>
<pre><code class="language-go">package main
import (
"fmt"
"io"
)
type TripleReader struct {
data []byte
position int
cycles int
}
func NewTripleReader(data []byte) *TripleReader {
return &TripleReader{data: data}
}
func (tr *TripleReader) Read(p []byte) (int, error) {
if tr.cycles >= 3 {
return 0, io.EOF
}
// Calcula o índice dentro do ciclo atual
idx := tr.position % len(tr.data)
// Copia até o final do slice p ou do data
n := copy(p, tr.data[idx:])
tr.position += n
// Se completamos um ciclo completo
if tr.position%len(tr.data) == 0 && tr.position > 0 {
tr.cycles++
}
return n, nil
}
func main() {
reader := NewTripleReader([]byte("Hello "))
buffer := make([]byte, 12)
n, _ := reader.Read(buffer)
fmt.Printf("Lido: %s (total: %d bytes)\n", buffer[:n], n)
}</code></pre>
<h3>Interface Writer</h3>
<p>Simetricamente, <code>Writer</code> define como você escreve dados:</p>
<pre><code class="language-go">type Writer interface {
Write(p []byte) (n int, err error)
}</code></pre>
<p>Implementar <code>Write</code> significa aceitar um slice de bytes e fazer algo com eles — escrevê-los em um arquivo, enviar pela rede, armazenar em memória. Novamente, a simplicidade permite que qualquer coisa que saiba escrever possa ser usada em conjunto com qualquer coisa que saiba ler.</p>
<p>Vamos criar um <code>Writer</code> que conta quantas linhas foram escritas:</p>
<pre><code class="language-go">package main
import (
"bytes"
"fmt"
)
type LineCountWriter struct {
lines int
buf bytes.Buffer
}
func (lcw *LineCountWriter) Write(p []byte) (int, error) {
// Conta quebras de linha
for _, b := range p {
if b == '\n' {
lcw.lines++
}
}
// Armazena também em um buffer interno
n, err := lcw.buf.Write(p)
return n, err
}
func (lcw *LineCountWriter) GetContent() string {
return lcw.buf.String()
}
func (lcw *LineCountWriter) GetLineCount() int {
return lcw.lines
}
func main() {
writer := &LineCountWriter{}
fmt.Fprint(writer, "Primeira linha\nSegunda linha\nTerceira linha\n")
fmt.Printf("Total de linhas: %d\n", writer.GetLineCount())
fmt.Printf("Conteúdo:\n%s", writer.GetContent())
}</code></pre>
<h2>Composição: Transformando Streams</h2>
<h3>A Força da Composição</h3>
<p>A verdadeira magia do pacote <code>io</code> está em compor essas interfaces. Um programa que espera um <code>Reader</code> não precisa saber se está recebendo um arquivo, uma conexão TCP ou um <code>Reader</code> customizado. Isso permite criar <strong>pipelines de transformação</strong> onde cada componente faz uma coisa bem.</p>
<p>Considere <code>io.Copy</code>. Essa função simples tem uma assinatura que parece trivial:</p>
<pre><code class="language-go">func Copy(dst Writer, src Reader) (written int64, err error)</code></pre>
<p>Mas ela é extraordinariamente poderosa. Você pode copiar de qualquer <code>Reader</code> para qualquer <code>Writer</code>, e <code>Copy</code> gerencia o buffering e a leitura progressiva. O arquivo de 10GB? <code>Copy</code> não carrega tudo na memória; processa em pedaços.</p>
<p>Vamos ver isso na prática:</p>
<pre><code class="language-go">package main
import (
"compress/gzip"
"fmt"
"io"
"os"
"strings"
)
func main() {
// Fonte: string em memória
source := strings.NewReader("Este é um texto que será comprimido.\n" +
"Você pode repetir isso várias vezes para aumentar o tamanho.\n")
// Destino: arquivo
file, err := os.Create("output.txt.gz")
if err != nil {
fmt.Println("Erro ao criar arquivo:", err)
return
}
defer file.Close()
// Cria um writer que comprime
gzipWriter := gzip.NewWriter(file)
defer gzipWriter.Close()
// Cria um pipeline: source -> gzipWriter -> file
// Tudo feito com io.Copy, sem carregar tudo na memória
bytes, err := io.Copy(gzipWriter, source)
if err != nil {
fmt.Println("Erro durante cópia:", err)
return
}
fmt.Printf("Foram copiados %d bytes comprimidos\n", bytes)
}</code></pre>
<h3>Wrappers: Reader e Writer com Comportamento Adicional</h3>
<p>Go fornece wrappers úteis no pacote <code>io</code> que adicionam funcionalidade a <code>Readers</code> e <code>Writers</code> existentes. Um exemplo é <code>io.MultiReader</code>, que concatena múltiplos readers:</p>
<pre><code class="language-go">package main
import (
"fmt"
"io"
"strings"
)
func main() {
reader1 := strings.NewReader("Primeira parte. ")
reader2 := strings.NewReader("Segunda parte. ")
reader3 := strings.NewReader("Terceira parte.")
// Cria um reader que lê de três fontes em sequência
combined := io.MultiReader(reader1, reader2, reader3)
// Lê tudo como se fosse um único reader
buffer := make([]byte, 128)
n, _ := combined.Read(buffer)
fmt.Printf("Resultado:\n%s\n", buffer[:n])
}</code></pre>
<h2>Padrões Práticos e Casos de Uso Reais</h2>
<h3>Processamento de Arquivos Grandes</h3>
<p>Um dos cenários onde streams brilham é no processamento de arquivos grandes. Em vez de carregar um arquivo inteiro em memória, você processa linha por linha ou bloco por bloco:</p>
<pre><code class="language-go">package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func main() {
// Cria um arquivo de exemplo
file, err := os.Create("dados.txt")
if err != nil {
fmt.Println("Erro:", err)
return
}
// Escreve 1000 linhas
for i := 1; i <= 1000; i++ {
fmt.Fprintf(file, "Linha %d: dados importantes\n", i)
}
file.Close()
// Agora lê o arquivo processando linha por linha
file, err = os.Open("dados.txt")
if err != nil {
fmt.Println("Erro:", err)
return
}
defer file.Close()
scanner := bufio.NewScanner(file)
lineCount := 0
dataCount := 0
// Processa cada linha sem carregar o arquivo inteiro
for scanner.Scan() {
line := scanner.Text()
lineCount++
// Conta ocorrências de "dados"
if strings.Contains(line, "dados") {
dataCount++
}
// Apenas imprime a cada 100 linhas como exemplo
if lineCount%100 == 0 {
fmt.Printf("Processadas %d linhas...\n", lineCount)
}
}
fmt.Printf("\nTotal de linhas: %d\n", lineCount)
fmt.Printf("Linhas contendo 'dados': %d\n", dataCount)
os.Remove("dados.txt") // Limpeza
}</code></pre>
<h3>Serviços Web: Streaming de Respostas</h3>
<p>Quando você cria um servidor HTTP em Go, as respostas também usam <code>Writer</code>. Isso permite enviar dados progressivamente sem precisar construir o corpo inteiro em memória:</p>
<pre><code class="language-go">package main
import (
"fmt"
"net/http"
"time"
)
func streamHandler(w http.ResponseWriter, r *http.Request) {
// Configura headers para indicar que é um stream
w.Header().Set("Content-Type", "text/plain")
// Envia dados progressivamente
for i := 1; i <= 5; i++ {
fmt.Fprintf(w, "Mensagem %d enviada em %v\n", i, time.Now().Format("15:04:05"))
// Força o envio (flush)
if flusher, ok := w.(http.Flusher); ok {
flusher.Flush()
}
time.Sleep(1 * time.Second)
}
fmt.Fprint(w, "Stream completo!\n")
}
func main() {
http.HandleFunc("/stream", streamHandler)
fmt.Println("Servidor iniciado em http://localhost:8080")
fmt.Println("Acesse: http://localhost:8080/stream")
http.ListenAndServe(":8080", nil)
}</code></pre>
<h3>Transformação em Cadeia com Pipes</h3>
<p>Às vezes você quer criar uma sequência de transformações. Go torna isso natural:</p>
<pre><code class="language-go">package main
import (
"fmt"
"io"
"strings"
"unicode/utf8"
)
// UpperReader transforma tudo em maiúsculas
type UpperReader struct {
r io.Reader
}
func (ur *UpperReader) Read(p []byte) (int, error) {
n, err := ur.r.Read(p)
// Converte bytes lidos para maiúsculas (simplista, funciona para ASCII)
for i := 0; i < n; i++ {
if p[i] >= 'a' && p[i] <= 'z' {
p[i] -= 32
}
}
return n, err
}
// CountingWriter conta bytes escritos
type CountingWriter struct {
w io.Writer
count int64
}
func (cw *CountingWriter) Write(p []byte) (int, error) {
cw.count += int64(len(p))
return cw.w.Write(p)
}
func main() {
// Cria um pipeline: string -> upper -> counter -> stdout
source := strings.NewReader("olá, mundo! isto é um teste de transformação.")
upper := &UpperReader{r: source}
counter := &CountingWriter{w: os.Stdout}
// Copia através de todo o pipeline
io.Copy(counter, upper)
fmt.Printf("\n\nTotal de bytes processados: %d\n", counter.count)
}</code></pre>
<p>Para fazer esse último exemplo funcionar, você precisa importar <code>os</code>:</p>
<pre><code class="language-go">import (
"fmt"
"io"
"os"
"strings"
)</code></pre>
<h2>Conclusão</h2>
<p>Dominar o pacote <code>io</code> em Go significa compreender três conceitos fundamentais. Primeiro, <strong>Readers e Writers são interfaces poderosas</strong> que permitem desacoplar a origem/destino dos dados da lógica de processamento. Segundo, <strong>composição sobre herança</strong> é o padrão: você não estende classes, você empilha comportamentos através de interfaces. Terceiro, <strong>thinking in streams</strong> muda como você projeta soluções, tornando seu código mais eficiente em memória e mais elegante na estrutura.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://golang.org/pkg/io/" target="_blank" rel="noopener noreferrer">Go Documentation: package io</a></li>
<li><a href="https://golang.org/doc/effective_go#io" target="_blank" rel="noopener noreferrer">Effective Go - io Package</a></li>
<li><a href="https://golang.org/ref/spec" target="_blank" rel="noopener noreferrer">The Go Programming Language Specification</a></li>
<li><a href="https://go.dev/blog/pipelines" target="_blank" rel="noopener noreferrer">Go Blog: Pipelines</a></li>
<li><a href="https://www.gopl.io/" target="_blank" rel="noopener noreferrer">Donovan & Kernighan - The Go Programming Language</a></li>
</ul>
<p><!-- FIM --></p>