Go Admin

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

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:

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 io é 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 Reader é o coração da leitura de dados em Go. Ela define apenas um método:

type Reader interface {
    Read(p []byte) (n int, err error)
}

Quando você implementa Read, está promessendo fazer uma coisa simples: ler até len(p) bytes de dados para dentro do slice p e retornar quantos bytes foram realmente lidos. Se não houver mais dados, você retorna io.EOF. 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.

Veja um exemplo prático. Vamos criar um Reader customizado que retorna os mesmos dados três vezes:

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)
}

Interface Writer

Simetricamente, Writer define como você escreve dados:

type Writer interface {
    Write(p []byte) (n int, err error)
}

Implementar Write 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.

Vamos criar um Writer que conta quantas linhas foram escritas:

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())
}

Composição: Transformando Streams

A Força da Composição

A verdadeira magia do pacote io está em compor essas interfaces. Um programa que espera um Reader não precisa saber se está recebendo um arquivo, uma conexão TCP ou um Reader customizado. Isso permite criar pipelines de transformação onde cada componente faz uma coisa bem.

Considere io.Copy. Essa função simples tem uma assinatura que parece trivial:

func Copy(dst Writer, src Reader) (written int64, err error)

Mas ela é extraordinariamente poderosa. Você pode copiar de qualquer Reader para qualquer Writer, e Copy gerencia o buffering e a leitura progressiva. O arquivo de 10GB? Copy não carrega tudo na memória; processa em pedaços.

Vamos ver isso na prática:

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)
}

Wrappers: Reader e Writer com Comportamento Adicional

Go fornece wrappers úteis no pacote io que adicionam funcionalidade a Readers e Writers existentes. Um exemplo é io.MultiReader, que concatena múltiplos readers:

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])
}

Padrões Práticos e Casos de Uso Reais

Processamento de Arquivos Grandes

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:

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
}

Serviços Web: Streaming de Respostas

Quando você cria um servidor HTTP em Go, as respostas também usam Writer. Isso permite enviar dados progressivamente sem precisar construir o corpo inteiro em memória:

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)
}

Transformação em Cadeia com Pipes

Às vezes você quer criar uma sequência de transformações. Go torna isso natural:

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)
}

Para fazer esse último exemplo funcionar, você precisa importar os:

import (
    "fmt"
    "io"
    "os"
    "strings"
)

Conclusão

Dominar o pacote io em Go significa compreender três conceitos fundamentais. Primeiro, Readers e Writers são interfaces poderosas que permitem desacoplar a origem/destino dos dados da lógica de processamento. Segundo, composição sobre herança é o padrão: você não estende classes, você empilha comportamentos através de interfaces. Terceiro, thinking in streams muda como você projeta soluções, tornando seu código mais eficiente em memória e mais elegante na estrutura.

Referências


Artigos relacionados