Go

Pacote io em Go: Readers, Writers e a Filosofia de Streams na Prática

12 min de leitura

Pacote io em Go: Readers, Writers e a Filosofia de Streams na Prática

A Filosofia de Streams em Go 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 streams. Um stream é uma sequência de dados que você processa continuamente, sem necessidade de carregar tudo na memória de uma só vez. 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 é a base dessa filosofia em Go, fornecendo interfaces simples que permitem criar componentes que trabalham harmoniosamente juntos, independentemente da fonte ou destino dos dados. As Interfaces Fundamentais: Reader e Writer Interface Reader A interface é o coração da leitura de dados em Go. Ela define apenas um método: Quando você implementa , está promessendo fazer uma coisa simples:

<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 &quot;tenho um arquivo de 1GB, como carrego tudo?&quot;, você pensa &quot;vou processar este dado em pequenos pedaços, conforme ele chega&quot;. 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 (

&quot;fmt&quot;

&quot;io&quot;

)

type TripleReader struct {

data []byte

position int

cycles int

}

func NewTripleReader(data []byte) *TripleReader {

return &amp;TripleReader{data: data}

}

func (tr *TripleReader) Read(p []byte) (int, error) {

if tr.cycles &gt;= 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 &amp;&amp; tr.position &gt; 0 {

tr.cycles++

}

return n, nil

}

func main() {

reader := NewTripleReader([]byte(&quot;Hello &quot;))

buffer := make([]byte, 12)

n, _ := reader.Read(buffer)

fmt.Printf(&quot;Lido: %s (total: %d bytes)\n&quot;, 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 (

&quot;bytes&quot;

&quot;fmt&quot;

)

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 == &#039;\n&#039; {

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 := &amp;LineCountWriter{}

fmt.Fprint(writer, &quot;Primeira linha\nSegunda linha\nTerceira linha\n&quot;)

fmt.Printf(&quot;Total de linhas: %d\n&quot;, writer.GetLineCount())

fmt.Printf(&quot;Conteúdo:\n%s&quot;, 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 (

&quot;compress/gzip&quot;

&quot;fmt&quot;

&quot;io&quot;

&quot;os&quot;

&quot;strings&quot;

)

func main() {

// Fonte: string em memória

source := strings.NewReader(&quot;Este é um texto que será comprimido.\n&quot; +

&quot;Você pode repetir isso várias vezes para aumentar o tamanho.\n&quot;)

// Destino: arquivo

file, err := os.Create(&quot;output.txt.gz&quot;)

if err != nil {

fmt.Println(&quot;Erro ao criar arquivo:&quot;, err)

return

}

defer file.Close()

// Cria um writer que comprime

gzipWriter := gzip.NewWriter(file)

defer gzipWriter.Close()

// Cria um pipeline: source -&gt; gzipWriter -&gt; file

// Tudo feito com io.Copy, sem carregar tudo na memória

bytes, err := io.Copy(gzipWriter, source)

if err != nil {

fmt.Println(&quot;Erro durante cópia:&quot;, err)

return

}

fmt.Printf(&quot;Foram copiados %d bytes comprimidos\n&quot;, 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 (

&quot;fmt&quot;

&quot;io&quot;

&quot;strings&quot;

)

func main() {

reader1 := strings.NewReader(&quot;Primeira parte. &quot;)

reader2 := strings.NewReader(&quot;Segunda parte. &quot;)

reader3 := strings.NewReader(&quot;Terceira parte.&quot;)

// 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(&quot;Resultado:\n%s\n&quot;, 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 (

&quot;bufio&quot;

&quot;fmt&quot;

&quot;os&quot;

&quot;strings&quot;

)

func main() {

// Cria um arquivo de exemplo

file, err := os.Create(&quot;dados.txt&quot;)

if err != nil {

fmt.Println(&quot;Erro:&quot;, err)

return

}

// Escreve 1000 linhas

for i := 1; i &lt;= 1000; i++ {

fmt.Fprintf(file, &quot;Linha %d: dados importantes\n&quot;, i)

}

file.Close()

// Agora lê o arquivo processando linha por linha

file, err = os.Open(&quot;dados.txt&quot;)

if err != nil {

fmt.Println(&quot;Erro:&quot;, 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 &quot;dados&quot;

if strings.Contains(line, &quot;dados&quot;) {

dataCount++

}

// Apenas imprime a cada 100 linhas como exemplo

if lineCount%100 == 0 {

fmt.Printf(&quot;Processadas %d linhas...\n&quot;, lineCount)

}

}

fmt.Printf(&quot;\nTotal de linhas: %d\n&quot;, lineCount)

fmt.Printf(&quot;Linhas contendo &#039;dados&#039;: %d\n&quot;, dataCount)

os.Remove(&quot;dados.txt&quot;) // 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 (

&quot;fmt&quot;

&quot;net/http&quot;

&quot;time&quot;

)

func streamHandler(w http.ResponseWriter, r *http.Request) {

// Configura headers para indicar que é um stream

w.Header().Set(&quot;Content-Type&quot;, &quot;text/plain&quot;)

// Envia dados progressivamente

for i := 1; i &lt;= 5; i++ {

fmt.Fprintf(w, &quot;Mensagem %d enviada em %v\n&quot;, i, time.Now().Format(&quot;15:04:05&quot;))

// Força o envio (flush)

if flusher, ok := w.(http.Flusher); ok {

flusher.Flush()

}

time.Sleep(1 * time.Second)

}

fmt.Fprint(w, &quot;Stream completo!\n&quot;)

}

func main() {

http.HandleFunc(&quot;/stream&quot;, streamHandler)

fmt.Println(&quot;Servidor iniciado em http://localhost:8080&quot;)

fmt.Println(&quot;Acesse: http://localhost:8080/stream&quot;)

http.ListenAndServe(&quot;:8080&quot;, 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 (

&quot;fmt&quot;

&quot;io&quot;

&quot;strings&quot;

&quot;unicode/utf8&quot;

)

// 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 &lt; n; i++ {

if p[i] &gt;= &#039;a&#039; &amp;&amp; p[i] &lt;= &#039;z&#039; {

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 -&gt; upper -&gt; counter -&gt; stdout

source := strings.NewReader(&quot;olá, mundo! isto é um teste de transformação.&quot;)

upper := &amp;UpperReader{r: source}

counter := &amp;CountingWriter{w: os.Stdout}

// Copia através de todo o pipeline

io.Copy(counter, upper)

fmt.Printf(&quot;\n\nTotal de bytes processados: %d\n&quot;, counter.count)

}</code></pre>

<p>Para fazer esse último exemplo funcionar, você precisa importar <code>os</code>:</p>

<pre><code class="language-go">import (

&quot;fmt&quot;

&quot;io&quot;

&quot;os&quot;

&quot;strings&quot;

)</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 &amp; Kernighan - The Go Programming Language</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Go

Guia Completo de sync.Mutex e sync.RWMutex em Go: Exclusão Mútua Explícita
Guia Completo de sync.Mutex e sync.RWMutex em Go: Exclusão Mútua Explícita

Entendendo Concorrência e a Necessidade de Sincronização A programação concor...

O que Todo Dev Deve Saber sobre SQLC em Go: Gerando Código Tipado a partir de Queries SQL
O que Todo Dev Deve Saber sobre SQLC em Go: Gerando Código Tipado a partir de Queries SQL

O que é SQLC e Por que Você Deveria Usar SQLC é uma ferramenta que gera códig...

Dominando Generics em Go: Type Parameters, Constraints e Casos de Uso Reais em Projetos Reais
Dominando Generics em Go: Type Parameters, Constraints e Casos de Uso Reais em Projetos Reais

Introdução aos Generics em Go Generics é um recurso que chegou ao Go na versã...