Os pacotes io e bufio
Esta aula explora os pacotes io e bufio da biblioteca padrão de Go, apresentando interfaces como Reader e Writer, a função io.Copy, o papel do buffering e a composição de streams. Você aprenderá a manipular fluxos de dados de forma eficiente e idiomática.
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
- Documentação do pacote io
- Documentação do pacote bufio
- Artigo do blog oficial: The Go io package
- Go by Example: Reading Files
- Go by Example: Writing Files
Exercícios
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) }Crie uma função que leia um arquivo linha por linha usando
bufio.Scannere 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++ } }Use
strings.NewReaderpara criar um Reader a partir de uma string e leia todos os bytes de uma vez comio.ReadAll.✓ Resposta:package main import ( "fmt" "io" "strings" ) func main() { r := strings.NewReader("Hello, Go!") data, _ := io.ReadAll(r) fmt.Println(string(data)) }Implemente um Writer que conte quantos bytes foram escritos (use
io.MultiWriterpara 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()) }Crie um pipe entre um leitor e um escritor usando
io.Pipee 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)) }