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

Exercícios

  1. Escreva uma função que copie o conteúdo de um io.Reader para um io.Writer usando um buffer de 512 bytes. Teste com strings.NewReader e bytes.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
    		}
    	}
    }
  2. Use io.Copy para 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
    }
  3. Crie um programa que leia o arquivo /etc/hosts linha por linha usando bufio.Scanner e 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)
    	}
    }
  4. Implemente um io.Writer que conte quantos bytes foram escritos (um contador). Use-o para envolver os.Stdout e 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)
    }
  5. Use io.MultiReader para concatenar dois strings.Reader e 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"
    }