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