Os pacotes io e bufio são fundamentais para manipulação de entrada e saída em Go. Eles fornecem interfaces abstratas e implementações concretas que permitem ler e escrever dados de forma flexível, seja em arquivos, conexões de rede, buffers em memória ou outras fontes. Nesta aula, vamos explorar os conceitos centrais de Reader e Writer, a utilidade de io.Copy, a importância do buffering e como compor diferentes fluxos para construir pipelines de dados.

Entender esses pacotes é essencial para escrever código eficiente e reutilizável, pois eles seguem o princípio de interfaces pequenas e composição, que são marcas registradas da linguagem Go.

Reader e Writer

As interfaces io.Reader e io.Writer são a base de toda a E/S em Go. Elas definem contratos simples: um Reader tem o método Read(p []byte) (n int, err error) que preenche o slice com dados e retorna o número de bytes lidos; um Writer tem o método Write(p []byte) (n int, err error) que escreve os bytes do slice e retorna quantos foram escritos. Qualquer tipo que implemente esses métodos pode ser usado como fonte ou destino de dados.

Por exemplo, um arquivo (os.File), um buffer (bytes.Buffer) ou uma conexão de rede (net.Conn) implementam essas interfaces. Isso permite escrever funções genéricas que operam sobre qualquer Reader ou Writer, independentemente da fonte real. Veja um exemplo simples:

package main

import (
	"fmt"
	"strings"
)

func main() {
	var r io.Reader = strings.NewReader("Olá, mundo!")
	buf := make([]byte, 4)
	for {
		n, err := r.Read(buf)
		if err != nil {
			break
		}
		fmt.Printf("Lidos %d bytes: %s\n", n, buf[:n])
	}
}

Neste código, criamos um Reader a partir de uma string e lemos em blocos de 4 bytes. A interface permite que o mesmo código funcione com qualquer outra fonte, como um arquivo ou uma requisição HTTP.

io.Copy

A função io.Copy simplifica a transferência de dados de um Reader para um Writer. Ela lê do Reader até EOF e escreve tudo no Writer, retornando o número total de bytes copiados e qualquer erro. Sua assinatura é func Copy(dst Writer, src Reader) (written int64, err error). É uma ferramenta poderosa para copiar streams sem precisar gerenciar buffers manualmente.

Um uso comum é copiar o conteúdo de um arquivo para outro, ou de uma resposta HTTP para um arquivo. Exemplo:

package main

import (
	"io"
	"os"
)

func main() {
	src, _ := os.Open("origem.txt")
	defer src.Close()

	dst, _ := os.Create("destino.txt")
	defer dst.Close()

	bytes, err := io.Copy(dst, src)
	if err != nil {
		panic(err)
	}
	println("Copiados", bytes, "bytes")
}

Além de io.Copy, existem variantes como io.CopyN (copia exatamente N bytes) e io.CopyBuffer (usa um buffer fornecido). Essas funções tornam a cópia de dados concisa e eficiente.

Buffering

O pacote bufio fornece leitores e escritores com buffer, que reduzem o número de chamadas de sistema ao agrupar operações de E/S. Um bufio.Reader envolve um io.Reader e mantém um buffer interno; quando você lê dele, ele primeiro preenche o buffer com uma leitura maior do que a solicitada, e depois atende as chamadas subsequentes a partir desse buffer. Isso é especialmente útil para leituras pequenas, como ler linha por linha.

Da mesma forma, bufio.Writer acumula escritas em um buffer e as envia de uma vez quando o buffer está cheio ou quando você chama Flush(). Exemplo de leitura com buffer:

package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	file, _ := os.Open("exemplo.txt")
	defer file.Close()

	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		line := scanner.Text()
		fmt.Println(line)
	}
}

Neste exemplo, bufio.NewScanner usa um bufio.Reader internamente para ler linhas de forma eficiente. O buffering reduz drasticamente o número de chamadas de sistema, melhorando a performance.

Composição

Uma das grandes vantagens das interfaces Reader e Writer é a possibilidade de compor streams. Você pode encadear vários processamentos: por exemplo, ler um arquivo compactado, descomprimir, decodificar JSON e processar os dados. Go oferece vários pacotes que implementam essas interfaces, como compress/gzip, crypto/aes, encoding/json, etc.

Para compor, basta passar um Reader como argumento para outro construtor. Exemplo: ler um arquivo gzip e copiar para a saída padrão:

package main

import (
	"compress/gzip"
	"io"
	"os"
)

func main() {
	file, _ := os.Open("dados.gz")
	defer file.Close()

	gzReader, _ := gzip.NewReader(file)
	defer gzReader.Close()

	io.Copy(os.Stdout, gzReader)
}

Aqui, gzReader implementa io.Reader e descomprime os dados lidos de file. A composição permite construir pipelines de dados de forma declarativa e modular.

Boas práticas e observações finais

Sempre verifique os erros retornados por Read, Write e outras operações de E/S. Use defer para fechar recursos como arquivos e conexões. Prefira funções como io.ReadAll e io.Copy para operações comuns, em vez de reinventar a roda. O buffering é recomendado para melhorar performance em leituras/escritas pequenas. A composição de streams é um padrão poderoso que torna o código mais limpo e reutilizável.

Referências

Exercícios

  1. Escreva um programa que copie o conteúdo de um arquivo para outro usando io.Copy.

    ✓ Resposta:
    package main
    
    import (
    	"io"
    	"os"
    )
    
    func main() {
    	src, _ := os.Open("origem.txt")
    	defer src.Close()
    
    	dst, _ := os.Create("destino.txt")
    	defer dst.Close()
    
    	io.Copy(dst, src)
    }
  2. Crie uma função que leia um arquivo linha por linha usando bufio.Scanner e imprima cada linha com seu número.

    ✓ Resposta:
    package main
    
    import (
    	"bufio"
    	"fmt"
    	"os"
    )
    
    func main() {
    	file, _ := os.Open("arquivo.txt")
    	defer file.Close()
    
    	scanner := bufio.NewScanner(file)
    	linhaNum := 1
    	for scanner.Scan() {
    		fmt.Printf("%d: %s\n", linhaNum, scanner.Text())
    		linhaNum++
    	}
    }
  3. Use strings.NewReader para criar um Reader a partir de uma string e leia todos os bytes de uma vez com io.ReadAll.

    ✓ Resposta:
    package main
    
    import (
    	"fmt"
    	"io"
    	"strings"
    )
    
    func main() {
    	r := strings.NewReader("Hello, Go!")
    	data, _ := io.ReadAll(r)
    	fmt.Println(string(data))
    }
  4. Implemente um Writer que conte quantos bytes foram escritos (use io.MultiWriter para escrever em dois lugares ao mesmo tempo).

    ✓ Resposta:
    package main
    
    import (
    	"bytes"
    	"fmt"
    	"io"
    )
    
    type CounterWriter struct {
    	Writer io.Writer
    	Count  int64
    }
    
    func (cw *CounterWriter) Write(p []byte) (n int, err error) {
    	n, err = cw.Writer.Write(p)
    	cw.Count += int64(n)
    	return
    }
    
    func main() {
    	var buf bytes.Buffer
    	cw := &CounterWriter{Writer: &buf}
    
    	io.WriteString(cw, "Olá, mundo!")
    	fmt.Println("Bytes escritos:", cw.Count)
    	fmt.Println(buf.String())
    }
  5. Crie um pipe entre um leitor e um escritor usando io.Pipe e transfira dados de um para o outro.

    ✓ Resposta:
    package main
    
    import (
    	"fmt"
    	"io"
    )
    
    func main() {
    	r, w := io.Pipe()
    
    	go func() {
    		w.Write([]byte("Dados do pipe"))
    		w.Close()
    	}()
    
    	data, _ := io.ReadAll(r)
    	fmt.Println(string(data))
    }