Os pacotes io e bufio
Esta aula aborda os pacotes io e bufio da biblioteca padrão de Go, que fornecem interfaces e funcionalidades para operações de entrada e saída. Você aprenderá sobre os fundamentos Reader e Writer, a função io.Copy, o buffering com bufio e a composição de interfaces para criar fluxos de dados flexíveis.
Os pacotes io e bufio são essenciais para manipulação de entrada e saída em Go. Eles fornecem abstrações que permitem ler e escrever dados de forma consistente, seja em arquivos, conexões de rede, buffers de memória ou streams. Dominar esses pacotes é fundamental para construir programas eficientes e modulares.
Nesta aula, exploraremos as interfaces io.Reader e io.Writer, a função io.Copy para transferência de dados, o uso de buffers com bufio para operações mais eficientes e como compor essas abstrações para criar pipelines de processamento.
Reader e Writer
As interfaces io.Reader e io.Writer são a base de toda E/S em Go. A interface Reader possui o método Read(p []byte) (n int, err error), que lê até len(p) bytes para o slice p e retorna o número de bytes lidos e um eventual erro. Quando não há mais dados, retorna io.EOF. Já a interface Writer possui o método Write(p []byte) (n int, err error), que escreve len(p) bytes a partir de p e retorna o número de bytes escritos e um eventual erro.
Muitos tipos implementam essas interfaces: os.File, strings.Reader, bytes.Buffer, conexões de rede, entre outros. Isso permite que funções que aceitam io.Reader ou io.Writer funcionem com qualquer fonte ou destino de dados. Exemplo:
package main
import (
"fmt"
"io"
"strings"
)
func main() {
reader := strings.NewReader("Hello, Go!")
buf := make([]byte, 4)
for {
n, err := reader.Read(buf)
if err == io.EOF {
break
}
if err != nil {
fmt.Println("Erro:", err)
return
}
fmt.Printf("Lidos %d bytes: %s\n", n, buf[:n])
}
}io.Copy
A função io.Copy(dst Writer, src Reader) (written int64, err error) copia dados de um Reader para um Writer até que EOF seja alcançado ou ocorra um erro. Ela utiliza um buffer interno de 32 KB para realizar a cópia de forma eficiente. É uma maneira concisa de transferir dados entre streams sem precisar gerenciar buffers manualmente.
Exemplo: copiar o conteúdo de um arquivo para outro, ou de um strings.Reader para os.Stdout.
package main
import (
"io"
"os"
"strings"
)
func main() {
src := strings.NewReader("Dados a serem copiados")
dst := os.Stdout
n, err := io.Copy(dst, src)
if err != nil {
panic(err)
}
println("\nBytes copiados:", n)
}Buffering
O pacote bufio implementa leitura e escrita com buffer, reduzindo o número de chamadas de sistema e melhorando a performance. Ele oferece bufio.Reader e bufio.Writer que envolvem um io.Reader ou io.Writer subjacente. O bufio.Reader fornece métodos como ReadLine, ReadString, ReadBytes e Peek, além de permitir a leitura de dados em blocos maiores. O bufio.Writer acumula dados em um buffer interno e os escreve de uma só vez quando o buffer está cheio ou quando Flush() é chamado.
Exemplo de uso de bufio.Scanner para ler linhas de um arquivo:
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
file, err := os.Open("exemplo.txt")
if err != nil {
panic(err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
if err := scanner.Err(); err != nil {
panic(err)
}
}Composição
Em Go, a composição de interfaces é uma prática poderosa. Podemos combinar io.Reader e io.Writer para criar um io.ReadWriter, ou ainda adicionar io.Closer para formar io.ReadCloser. O pacote io define várias interfaces compostas como io.ReadWriter, io.ReadCloser, io.WriteCloser e io.ReadWriteCloser. Além disso, podemos usar funções como io.MultiReader e io.MultiWriter para combinar múltiplos readers ou writers em um único fluxo.
Exemplo: criar um io.TeeReader que lê de um reader e escreve em um writer simultaneamente, útil para logging ou monitoramento.
package main
import (
"bytes"
"fmt"
"io"
"strings"
)
func main() {
var buf bytes.Buffer
reader := strings.NewReader("Dados para log")
tee := io.TeeReader(reader, &buf)
// Lê todos os dados do TeeReader, que também escreve no buf
data, _ := io.ReadAll(tee)
fmt.Println("Lido:", string(data))
fmt.Println("Log:", buf.String())
}Boas Práticas
Ao trabalhar com E/S, sempre verifique erros e feche recursos com defer. Utilize bufio para leitura/escrita de texto linha por linha ou quando precisar de eficiência. Prefira usar as interfaces io.Reader e io.Writer em vez de tipos concretos para tornar seu código mais flexível e testável. A composição de readers e writers permite criar pipelines de processamento de dados de forma elegante.
Referências
- Documentação oficial do pacote io
- Documentação oficial do pacote bufio
- Blog: Erros são valores
- Effective Go
- Tutorial sobre interfaces em Go (DigitalOcean)
- Advanced Go Concurrency Patterns (exemplo de composição)
Exercícios
Escreva uma função que copie o conteúdo de um
io.Readerpara umio.Writerusando um buffer de 512 bytes. Teste comstrings.NewReaderebytes.Buffer.✓ Resposta:func copyWithBuffer(dst io.Writer, src io.Reader) (int64, error) { buf := make([]byte, 512) var total int64 for { n, err := src.Read(buf) if n > 0 { nn, werr := dst.Write(buf[:n]) total += int64(nn) if werr != nil { return total, werr } } if err == io.EOF { return total, nil } if err != nil { return total, err } } }Use
io.Copypara copiar o conteúdo de um arquivo para outro. Crie dois arquivos temporários, escreva dados no primeiro e copie para o segundo. Verifique se o conteúdo é igual.✓ Resposta:package main import ( "io" "os" ) func main() { // Cria arquivo fonte srcFile, _ := os.CreateTemp("", "src*.txt") defer os.Remove(srcFile.Name()) srcFile.WriteString("Conteúdo original") srcFile.Close() // Cria arquivo destino dstFile, _ := os.CreateTemp("", "dst*.txt") defer os.Remove(dstFile.Name()) // Abre fonte para leitura src, _ := os.Open(srcFile.Name()) defer src.Close() // Copia io.Copy(dstFile, src) dstFile.Close() // Verifica data1, _ := os.ReadFile(srcFile.Name()) data2, _ := os.ReadFile(dstFile.Name()) // data1 == data2 }Crie um programa que leia o arquivo
/etc/hostslinha por linha usandobufio.Scannere imprima cada linha com o número da linha.✓ Resposta:package main import ( "bufio" "fmt" "os" ) func main() { file, err := os.Open("/etc/hosts") if err != nil { panic(err) } defer file.Close() scanner := bufio.NewScanner(file) lineNum := 1 for scanner.Scan() { fmt.Printf("%d: %s\n", lineNum, scanner.Text()) lineNum++ } if err := scanner.Err(); err != nil { panic(err) } }Implemente um
io.Writerque conte quantos bytes foram escritos (um contador). Use-o para envolveros.Stdoute escreva uma string, exibindo o total de bytes escritos.✓ Resposta:package main import ( "fmt" "io" "os" ) type CounterWriter struct { Writer io.Writer Count int64 } func (cw *CounterWriter) Write(p []byte) (int, error) { n, err := cw.Writer.Write(p) cw.Count += int64(n) return n, err } func main() { cw := &CounterWriter{Writer: os.Stdout} fmt.Fprintf(cw, "Hello, world!") fmt.Printf("\nTotal bytes escritos: %d\n", cw.Count) }Use
io.MultiReaderpara concatenar doisstrings.Readere ler todo o conteúdo combinado de uma vez.✓ Resposta:package main import ( "fmt" "io" "strings" ) func main() { r1 := strings.NewReader("Primeira parte ") r2 := strings.NewReader("Segunda parte") mr := io.MultiReader(r1, r2) data, _ := io.ReadAll(mr) fmt.Println(string(data)) // "Primeira parte Segunda parte" }