Golang

Leitura e escrita de arquivos com os pacotes os e bufio Já leu

10 min de leitura

Leitura e escrita de arquivos com os pacotes os e bufio
Antes de bancos de dados, filas de mensagens e APIs REST, existe o arquivo. Logs, configurações, dados exportados, relatórios, backups — praticamente todo sistema lida com arquivos em algum ponto. Go oferece uma API de I

Antes de bancos de dados, filas de mensagens e APIs REST, existe o arquivo. Logs, configurações, dados exportados, relatórios, backups — praticamente todo sistema lida com arquivos em algum ponto. Go oferece uma API de I/O coerente e bem projetada que vai desde operações simples de leitura e escrita até streaming eficiente de grandes volumes de dados.

A abstração central é a interface io.Reader e io.Writer — qualquer coisa que implemente essas interfaces pode ser lida ou escrita com as mesmas funções. Arquivos, conexões de rede, buffers em memória e respostas HTTP são todos tratados uniformemente por esse sistema.


Operações simples: os.ReadFile e os.WriteFile

Para arquivos pequenos que cabem confortavelmente na memória, as funções de conveniência do pacote os são a escolha mais direta:

package main

import (
    "fmt"
    "os"
)

func main() {
    // Escrevendo um arquivo completo
    conteudo := []byte("linha 1\nlinha 2\nlinha 3\n")
    err := os.WriteFile("dados.txt", conteudo, 0644)
    if err != nil {
        fmt.Println("Erro ao escrever:", err)
        return
    }

    // Lendo o arquivo completo de uma vez
    dados, err := os.ReadFile("dados.txt")
    if err != nil {
        fmt.Println("Erro ao ler:", err)
        return
    }

    fmt.Print(string(dados))
    // linha 1
    // linha 2
    // linha 3
}

O argumento 0644 define as permissões do arquivo no formato octal Unix: o dono pode ler e escrever, grupo e outros podem apenas ler. Para arquivos executáveis, 0755 é a convenção.

os.ReadFile lê o arquivo inteiro para um []byte em memória. Para arquivos de configuração, templates e dados pequenos, é perfeitamente adequado. Para arquivos de gigabytes, leitura incremental é obrigatória.


Abrindo arquivos com os.Open e os.Create

Para controle mais fino, os.Open abre um arquivo existente para leitura, e os.Create cria ou trunca um arquivo para escrita:

package main

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

func main() {
    // os.Open — somente leitura
    arquivo, err := os.Open("dados.txt")
    if err != nil {
        fmt.Println("Erro:", err)
        return
    }
    defer arquivo.Close() // sempre fechar — defer garante mesmo em caso de erro

    // Lendo em chunks
    buffer := make([]byte, 32)
    for {
        n, err := arquivo.Read(buffer)
        if n > 0 {
            fmt.Print(string(buffer[:n]))
        }
        if err == io.EOF {
            break
        }
        if err != nil {
            fmt.Println("Erro na leitura:", err)
            return
        }
    }
}
// os.Create — cria ou trunca para escrita
func escreverArquivo() error {
    arquivo, err := os.Create("saida.txt")
    if err != nil {
        return fmt.Errorf("criar arquivo: %w", err)
    }
    defer arquivo.Close()

    _, err = fmt.Fprintln(arquivo, "primeira linha")
    if err != nil {
        return fmt.Errorf("escrever: %w", err)
    }

    _, err = arquivo.WriteString("segunda linha\n")
    return err
}

os.OpenFile: controle total sobre o modo de abertura

os.OpenFile é a função mais completa para abrir arquivos, permitindo especificar combinações de flags:

package main

import (
    "fmt"
    "os"
)

func main() {
    // Abrindo para append — adiciona ao final sem truncar
    arquivo, err := os.OpenFile("log.txt",
        os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
    if err != nil {
        fmt.Println("Erro:", err)
        return
    }
    defer arquivo.Close()

    fmt.Fprintln(arquivo, "nova entrada de log")
}

As flags mais usadas:

Flag Significado
os.O_RDONLY Somente leitura
os.O_WRONLY Somente escrita
os.O_RDWR Leitura e escrita
os.O_APPEND Adiciona ao final
os.O_CREATE Cria se não existir
os.O_TRUNC Trunca ao abrir
os.O_EXCL Falha se já existir

bufio: I/O eficiente com buffer

Cada chamada a arquivo.Read() ou arquivo.Write() é potencialmente uma chamada ao sistema operacional — uma operação cara. O pacote bufio envolve qualquer io.Reader ou io.Writer com um buffer em memória, reduzindo drasticamente o número de chamadas ao SO ao agrupar operações pequenas:

package main

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

func lerLinhas(caminho string) error {
    arquivo, err := os.Open(caminho)
    if err != nil {
        return fmt.Errorf("lerLinhas: %w", err)
    }
    defer arquivo.Close()

    scanner := bufio.NewScanner(arquivo)

    numeroLinha := 0
    for scanner.Scan() {
        numeroLinha++
        linha := scanner.Text()
        fmt.Printf("%4d: %s\n", numeroLinha, linha)
    }

    return scanner.Err() // sempre verificar erro ao final
}

func main() {
    // Cria arquivo de exemplo
    os.WriteFile("poema.txt", []byte(`Programar em Go
é como respirar —
simples, natural.`), 0644)

    if err := lerLinhas("poema.txt"); err != nil {
        fmt.Println("Erro:", err)
    }
}

bufio.Scanner é a forma idiomática de ler um arquivo linha por linha. Por padrão, divide por linhas — o token padrão é bufio.ScanLines. Mas pode ser configurado para dividir por palavras, bytes ou qualquer delimitador personalizado:

scanner := bufio.NewScanner(arquivo)
scanner.Split(bufio.ScanWords) // divide por palavras

for scanner.Scan() {
    fmt.Println(scanner.Text())
}

Atenção ao tamanho máximo de linha. Por padrão, bufio.Scanner suporta linhas de até 64 KB. Para linhas maiores, aumente o buffer:

scanner := bufio.NewScanner(arquivo)
buffer := make([]byte, 1024*1024) // buffer de 1 MB
scanner.Buffer(buffer, len(buffer))

bufio.Writer: escrita com buffer

Assim como o Scanner eficientiza a leitura, bufio.Writer agrupa escritas pequenas:

package main

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

func gerarRelatorio(caminho string, dados []string) error {
    arquivo, err := os.Create(caminho)
    if err != nil {
        return fmt.Errorf("criar arquivo: %w", err)
    }
    defer arquivo.Close()

    writer := bufio.NewWriter(arquivo)
    // bufio.NewWriterSize(arquivo, 65536) — tamanho de buffer personalizado

    for i, linha := range dados {
        _, err := fmt.Fprintf(writer, "%04d: %s\n", i+1, linha)
        if err != nil {
            return fmt.Errorf("escrever linha %d: %w", i+1, err)
        }
    }

    // CRÍTICO: Flush descarrega o buffer para o arquivo
    // Sem Flush, dados podem ser perdidos
    if err := writer.Flush(); err != nil {
        return fmt.Errorf("flush: %w", err)
    }

    return nil
}

func main() {
    dados := []string{
        "Relatório de vendas — Março 2024",
        "Produto A: 1500 unidades",
        "Produto B: 890 unidades",
        "Produto C: 2100 unidades",
        "Total: 4490 unidades",
    }

    if err := gerarRelatorio("relatorio.txt", dados); err != nil {
        fmt.Println("Erro:", err)
        return
    }
    fmt.Println("Relatório gerado com sucesso")
}

Lendo arquivos CSV

CSV é um dos formatos de arquivo mais comuns em dados empresariais. O pacote encoding/csv da biblioteca padrão lida com todas as particularidades do formato:

package main

import (
    "encoding/csv"
    "fmt"
    "os"
    "strconv"
)

type Venda struct {
    Produto    string
    Quantidade int
    Preco      float64
}

func lerCSV(caminho string) ([]Venda, error) {
    arquivo, err := os.Open(caminho)
    if err != nil {
        return nil, fmt.Errorf("lerCSV: %w", err)
    }
    defer arquivo.Close()

    reader := csv.NewReader(arquivo)
    reader.Comma = ','         // separador padrão
    reader.Comment = '#'       // linhas começando com # são ignoradas
    reader.TrimLeadingSpace = true

    // Pula o cabeçalho
    if _, err := reader.Read(); err != nil {
        return nil, fmt.Errorf("ler cabeçalho: %w", err)
    }

    var vendas []Venda
    for {
        registro, err := reader.Read()
        if err != nil {
            break // io.EOF encerra o loop
        }

        qtd, _ := strconv.Atoi(registro[1])
        preco, _ := strconv.ParseFloat(registro[2], 64)

        vendas = append(vendas, Venda{
            Produto:    registro[0],
            Quantidade: qtd,
            Preco:      preco,
        })
    }

    return vendas, nil
}

func escreverCSV(caminho string, vendas []Venda) error {
    arquivo, err := os.Create(caminho)
    if err != nil {
        return err
    }
    defer arquivo.Close()

    writer := csv.NewWriter(arquivo)
    defer writer.Flush()

    // Cabeçalho
    writer.Write([]string{"Produto", "Quantidade", "Preco"})

    for _, v := range vendas {
        writer.Write([]string{
            v.Produto,
            strconv.Itoa(v.Quantidade),
            strconv.FormatFloat(v.Preco, 'f', 2, 64),
        })
    }

    return writer.Error()
}

func main() {
    // Cria CSV de exemplo
    os.WriteFile("vendas.csv", []byte(
        "Produto,Quantidade,Preco\n"+
            "Notebook,5,4500.00\n"+
            "Mouse,20,120.00\n"+
            "Teclado,15,250.00\n",
    ), 0644)

    vendas, err := lerCSV("vendas.csv")
    if err != nil {
        fmt.Println("Erro:", err)
        return
    }

    total := 0.0
    for _, v := range vendas {
        subtotal := float64(v.Quantidade) * v.Preco
        fmt.Printf("%-12s %3d x R$ %8.2f = R$ %10.2f\n",
            v.Produto, v.Quantidade, v.Preco, subtotal)
        total += subtotal
    }
    fmt.Printf("%-12s %27s R$ %10.2f\n", "TOTAL", "", total)
}

Copiando arquivos eficientemente

io.Copy transfere dados de um Reader para um Writer sem carregar tudo em memória:

package main

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

func copiarArquivo(origem, destino string) error {
    src, err := os.Open(origem)
    if err != nil {
        return fmt.Errorf("abrir origem: %w", err)
    }
    defer src.Close()

    dst, err := os.Create(destino)
    if err != nil {
        return fmt.Errorf("criar destino: %w", err)
    }
    defer dst.Close()

    bytesCopiados, err := io.Copy(dst, src)
    if err != nil {
        return fmt.Errorf("copiar: %w", err)
    }

    fmt.Printf("copiados %d bytes de %s para %s\n",
        bytesCopiados, origem, destino)
    return nil
}

func main() {
    os.WriteFile("original.txt", []byte("conteúdo do arquivo original\n"), 0644)

    if err := copiarArquivo("original.txt", "copia.txt"); err != nil {
        fmt.Println("Erro:", err)
        return
    }

    dados, _ := os.ReadFile("copia.txt")
    fmt.Print(string(dados))
}

Percorrendo diretórios

Para listar arquivos recursivamente, filepath.WalkDir é a função moderna e eficiente (substitui o filepath.Walk mais antigo):

package main

import (
    "fmt"
    "io/fs"
    "os"
    "path/filepath"
    "strings"
)

func listarArquivosGo(raiz string) error {
    return filepath.WalkDir(raiz, func(caminho string, d fs.DirEntry, err error) error {
        if err != nil {
            return err // erro ao acessar o item
        }

        // Ignora diretórios ocultos (começam com ponto)
        if d.IsDir() && strings.HasPrefix(d.Name(), ".") {
            return filepath.SkipDir // não desce nesse diretório
        }

        // Processa apenas arquivos .go
        if !d.IsDir() && filepath.Ext(caminho) == ".go" {
            info, err := d.Info()
            if err != nil {
                return err
            }
            fmt.Printf("%-50s %8d bytes\n", caminho, info.Size())
        }

        return nil
    })
}

func main() {
    // Cria estrutura de diretórios de exemplo
    os.MkdirAll("projeto/cmd", 0755)
    os.MkdirAll("projeto/internal", 0755)
    os.WriteFile("projeto/main.go", []byte("package main"), 0644)
    os.WriteFile("projeto/cmd/serve.go", []byte("package cmd"), 0644)
    os.WriteFile("projeto/internal/handler.go", []byte("package internal"), 0644)

    fmt.Println("Arquivos Go encontrados:")
    if err := listarArquivosGo("projeto"); err != nil {
        fmt.Println("Erro:", err)
    }

    // Limpeza
    os.RemoveAll("projeto")
}

Arquivos temporários

Para operações que precisam de arquivos temporários sem poluir o sistema de arquivos:

package main

import (
    "fmt"
    "os"
)

func main() {
    // Arquivo temporário — Go garante nome único
    tmp, err := os.CreateTemp("", "processamento-*.json")
    if err != nil {
        fmt.Println("Erro:", err)
        return
    }
    defer os.Remove(tmp.Name()) // remove ao terminar
    defer tmp.Close()

    fmt.Println("Arquivo temporário:", tmp.Name())
    tmp.WriteString(`{"status": "processando"}`)

    // Diretório temporário
    dir, err := os.MkdirTemp("", "cache-*")
    if err != nil {
        fmt.Println("Erro:", err)
        return
    }
    defer os.RemoveAll(dir) // remove recursivamente ao terminar

    fmt.Println("Diretório temporário:", dir)
}

Resumo do que foi coberto

Este artigo apresentou I/O de arquivos em Go de forma abrangente: operações simples com os.ReadFile e os.WriteFile, controle fino com os.Open, os.Create e os.OpenFile, leitura eficiente linha a linha com bufio.Scanner, escrita com buffer via bufio.Writer, leitura e escrita de CSV com encoding/csv, cópia eficiente com io.Copy, travessia recursiva de diretórios com filepath.WalkDir e arquivos temporários. O próximo artigo explora JSON em profundidade — o formato de troca de dados mais usado em APIs modernas.


Referências e leituras complementares

  • Documentação do pacote os — Referência completa de operações de arquivo e sistema. https://pkg.go.dev/os

  • Documentação do pacote bufio — Referência de Scanner, Reader e Writer com buffer. https://pkg.go.dev/bufio

  • Documentação do pacote io — Interfaces Reader, Writer e funções utilitárias. https://pkg.go.dev/io

  • Documentação encoding/csv — Leitura e escrita de arquivos CSV. https://pkg.go.dev/encoding/csv

  • Go by Example: Reading Files — Exemplos práticos de leitura de arquivos. https://gobyexample.com/reading-files

  • Go by Example: Writing Files — Exemplos práticos de escrita de arquivos. https://gobyexample.com/writing-files

Comentários

Mais em Golang

Logging estruturado com slog e Zerolog
Logging estruturado com slog e Zerolog

Logs são a primeira e muitas vezes única fonte de informação quando algo dá e...

Select, timeouts e cancelamento de operações
Select, timeouts e cancelamento de operações

O artigo anterior introduziu o select como mecanismo para aguardar múltiplos...

Métodos em Structs: receivers por valor e por ponteiro
Métodos em Structs: receivers por valor e por ponteiro

No artigo anterior, structs foram apresentadas como agrupamentos de dados. M&...