Entrada e saída em Go: arquivos, diretórios e leitura eficiente

Capítulo 12

Tempo estimado de leitura: 9 minutos

+ Exercício

Visão geral de I/O em Go

Em Go, operações de entrada e saída (I/O) são feitas principalmente com três pacotes: os (interação com sistema de arquivos e processos), io (abstrações de leitura/escrita via interfaces) e bufio (bufferização para eficiência). A ideia central é trabalhar com io.Reader e io.Writer: arquivos, buffers, conexões e streams costumam “parecer” iguais quando expostos por essas interfaces.

Para I/O eficiente e com baixo consumo de memória, prefira processamento em streaming (ler e tratar aos poucos) em vez de carregar o arquivo inteiro em memória.

Abrindo, criando e fechando arquivos com os

Abrir para leitura: os.Open

os.Open abre um arquivo em modo leitura. Ele retorna um *os.File, que implementa io.Reader.

f, err := os.Open("dados.txt"); if err != nil { return err } defer f.Close()

Passo a passo prático:

  • Chame os.Open com o caminho do arquivo.
  • Verifique err.
  • Use defer f.Close() imediatamente após abrir para garantir fechamento mesmo em retornos antecipados.

Criar/truncar para escrita: os.Create

os.Create cria o arquivo (ou trunca se já existir) e abre para escrita. Ele retorna um *os.File que implementa io.Writer.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

Baixar o aplicativo

out, err := os.Create("saida.txt"); if err != nil { return err } defer out.Close()

Abrir com flags e permissões: os.OpenFile

Quando você precisa controlar comportamento (append, criar se não existir, leitura e escrita, etc.), use os.OpenFile com flags e permissões.

f, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644); if err != nil { return err } defer f.Close()

Flags comuns:

  • os.O_RDONLY, os.O_WRONLY, os.O_RDWR
  • os.O_CREATE (cria se não existir)
  • os.O_TRUNC (zera o arquivo ao abrir)
  • os.O_APPEND (escreve sempre no fim)

Permissões (modo) comuns em Unix-like:

  • 0o644: dono lê/escreve; grupo/outros leem
  • 0o600: somente dono lê/escreve
  • 0o755: executável (mais comum para diretórios e binários)

Observação: em Windows, permissões são tratadas de forma diferente; ainda assim, o código é portável e o modo pode ser parcialmente ignorado pelo sistema.

Diretórios: criar, listar e garantir existência

Criar diretório: os.Mkdir e os.MkdirAll

os.Mkdir cria um diretório único; os.MkdirAll cria toda a árvore necessária.

err := os.MkdirAll("data/saida", 0o755); if err != nil { return err }

Listar conteúdo: os.ReadDir

os.ReadDir retorna entradas de diretório de forma prática e eficiente.

entries, err := os.ReadDir("data"); if err != nil { return err } for _, e := range entries { name := e.Name(); _ = name }

Checar existência e tipo: os.Stat

Use os.Stat para obter metadados e diferenciar arquivo/diretório.

info, err := os.Stat("data"); if err != nil { if os.IsNotExist(err) { return err } return err } if info.IsDir() { /* é diretório */ }

Manipulação de caminhos com path/filepath

Para montar caminhos de forma portável (Windows/Linux/macOS), use filepath. Evite concatenar com "/" manualmente.

Montar caminhos: filepath.Join

p := filepath.Join("data", "logs", "app.log")

Normalizar e obter partes do caminho

  • filepath.Clean: remove .., . e duplicações
  • filepath.Dir e filepath.Base: diretório e nome final
  • filepath.Ext: extensão
  • filepath.Abs: caminho absoluto
raw := "data/../data/logs/./app.log" clean := filepath.Clean(raw) dir := filepath.Dir(clean) base := filepath.Base(clean) ext := filepath.Ext(clean) _, _, _, _ = clean, dir, base, ext

Leitura eficiente: io e bufio

Ler tudo de uma vez (quando faz sentido): io.ReadAll

io.ReadAll lê todo o conteúdo para memória. Use apenas quando o arquivo for pequeno e você tiver certeza do tamanho.

b, err := io.ReadAll(f); if err != nil { return err } _ = b

Copiar streams: io.Copy

io.Copy transfere dados de um io.Reader para um io.Writer usando buffer interno, ideal para cópias sem carregar tudo em memória.

in, err := os.Open("entrada.bin"); if err != nil { return err } defer in.Close() out, err := os.Create("saida.bin"); if err != nil { return err } defer out.Close() if _, err := io.Copy(out, in); err != nil { return err }

Leitura linha a linha com bufio.Scanner

Para arquivos de texto (logs, CSV simples, listas), bufio.Scanner é a forma mais direta de ler linha a linha com baixo consumo de memória.

f, err := os.Open("app.log"); if err != nil { return err } defer f.Close() sc := bufio.NewScanner(f) for sc.Scan() { line := sc.Text(); _ = line } if err := sc.Err(); err != nil { return err }

Ponto importante: o Scanner tem um limite padrão de token (linha) de 64K. Para logs com linhas maiores, aumente o buffer:

sc := bufio.NewScanner(f) buf := make([]byte, 0, 64*1024) sc.Buffer(buf, 1024*1024) // até 1MB por linha

Leitura com bufio.Reader para mais controle

Quando você precisa lidar com linhas muito longas, ou quer ler por delimitador, use bufio.Reader e métodos como ReadString ou ReadBytes.

r := bufio.NewReader(f) for { line, err := r.ReadString('\n') if err != nil { if errors.Is(err, io.EOF) { if len(line) > 0 { /* processa última linha sem \n */ } break } return err } _ = line }

Escrita eficiente: bufio.Writer e fmt.Fprintln

Escrever em arquivo chamando Write muitas vezes pode gerar muitas syscalls. Use bufio.Writer para agrupar escritas e chame Flush ao final.

out, err := os.Create("saida.txt"); if err != nil { return err } defer out.Close() w := bufio.NewWriter(out) defer w.Flush() for i := 0; i < 3; i++ { if _, err := fmt.Fprintln(w, "linha", i); err != nil { return err } }

Dica: o defer w.Flush() deve ocorrer após criar o writer. Se você retornar antes do flush, pode perder dados no buffer.

Tratamento de erros e defer no contexto de I/O

Em I/O, erros podem acontecer por permissões, arquivo inexistente, disco cheio, caminho inválido, arquivo em uso, etc. Boas práticas:

  • Cheque erro imediatamente após cada operação que pode falhar.
  • Use defer para fechar arquivos assim que abrir.
  • Ao ler com Scanner, sempre verifique sc.Err() após o loop.
  • Ao escrever com bufio.Writer, garanta Flush e cheque o erro de escrita durante o loop.

Exemplo com fechamento e captura de erro de Close quando necessário (especialmente em escrita):

out, err := os.Create("saida.txt"); if err != nil { return err } defer func() { cerr := out.Close(); if err == nil && cerr != nil { err = cerr } }() w := bufio.NewWriter(out) defer func() { ferr := w.Flush(); if err == nil && ferr != nil { err = ferr } }()

Esse padrão é útil quando você quer retornar o erro de Flush/Close caso nenhum outro erro tenha ocorrido.

Exercício prático: processar um arquivo de log com contagem e filtragem (streaming)

Objetivo

Você vai criar um programa que lê um arquivo de log linha a linha, sem carregar tudo na memória, e produz um relatório:

  • Total de linhas
  • Quantidade de linhas contendo ERROR e WARN
  • Filtrar linhas com ERROR para um arquivo separado
  • Contar ocorrências por “módulo” (ex.: texto entre colchetes [auth], [db])

Formato de log assumido (exemplo)

2026-01-25T10:00:00Z [auth] INFO login ok user=123 2026-01-25T10:01:00Z [db] WARN slow query ms=1200 2026-01-25T10:02:00Z [auth] ERROR invalid password user=456

Passo a passo

  • Abra o arquivo de entrada com os.Open.
  • Crie o arquivo de saída para erros com os.OpenFile (append ou trunc, você decide).
  • Use bufio.Scanner para ler linha a linha.
  • Para cada linha: incremente contadores, detecte nível (ERROR/WARN) e extraia o módulo entre [ e ].
  • Se for ERROR, escreva a linha no arquivo de erros usando bufio.Writer.
  • Ao final, imprima um resumo e uma tabela simples por módulo.

Código base (um arquivo main.go)

package main  import ( "bufio" "errors" "flag" "fmt" "io" "os" "path/filepath" "sort" "strings" )  func extractModule(line string) string { // procura algo como [auth] i := strings.IndexByte(line, '[') if i == -1 { return "" } j := strings.IndexByte(line[i+1:], ']') if j == -1 { return "" } return line[i+1 : i+1+j] }  func main() { inPath := flag.String("in", "app.log", "caminho do arquivo de log") outDir := flag.String("outdir", "out", "diretório de saída") flag.Parse()  if err := os.MkdirAll(*outDir, 0o755); err != nil { fmt.Fprintln(os.Stderr, "erro ao criar diretório:", err) os.Exit(1) }  errPath := filepath.Join(*outDir, "errors.log")  in, err := os.Open(*inPath) if err != nil { fmt.Fprintln(os.Stderr, "erro ao abrir entrada:", err) os.Exit(1) } defer in.Close()  errFile, err := os.OpenFile(errPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644) if err != nil { fmt.Fprintln(os.Stderr, "erro ao criar arquivo de erros:", err) os.Exit(1) } defer errFile.Close()  w := bufio.NewWriter(errFile) defer w.Flush()  sc := bufio.NewScanner(in) // aumenta limite para linhas maiores (ajuste conforme seu log) buf := make([]byte, 0, 64*1024) sc.Buffer(buf, 1024*1024)  var total, warn, errCount int byModule := map[string]int{}  for sc.Scan() { line := sc.Text() total++  if strings.Contains(line, "WARN") { warn++ } if strings.Contains(line, "ERROR") { errCount++ if _, e := w.WriteString(line + "\n"); e != nil { fmt.Fprintln(os.Stderr, "erro ao escrever arquivo de erros:", e) os.Exit(1) } }  mod := extractModule(line) if mod != "" { byModule[mod]++ } } if e := sc.Err(); e != nil { fmt.Fprintln(os.Stderr, "erro ao ler log:", e) os.Exit(1) }  // garante que o buffer foi para o disco if e := w.Flush(); e != nil { fmt.Fprintln(os.Stderr, "erro no flush:", e) os.Exit(1) }  fmt.Println("Resumo") fmt.Println("Total de linhas:", total) fmt.Println("WARN:", warn) fmt.Println("ERROR:", errCount) fmt.Println("Arquivo com erros:", errPath)  // imprime módulos ordenados para ficar estável mods := make([]string, 0, len(byModule)) for m := range byModule { mods = append(mods, m) } sort.Strings(mods)  fmt.Println("\nOcorrências por módulo") for _, m := range mods { fmt.Printf("%-10s %d\n", m, byModule[m]) } }  // Observação: se você quiser processar logs gigantescos e o Scanner não for ideal, // substitua por bufio.Reader e leia por '\n'. Exemplo de loop com Reader: func readWithReader(r io.Reader) error { br := bufio.NewReader(r) for { line, err := br.ReadString('\n') if err != nil { if errors.Is(err, io.EOF) { if len(line) > 0 { /* processa última linha */ } return nil } return err } _ = line // processa } }

Por que esse exercício é eficiente?

  • Streaming: lê uma linha por vez, mantendo memória praticamente constante.
  • Bufferização: bufio.Scanner e bufio.Writer reduzem overhead de I/O.
  • Saída incremental: linhas de erro são gravadas conforme aparecem, sem acumular em slices.

Extensões sugeridas

  • Adicionar flag -level para filtrar apenas um nível (INFO/WARN/ERROR).
  • Gerar um CSV com módulo,contagem usando fmt.Fprintf no writer.
  • Processar um diretório de logs: usar os.ReadDir e aplicar o mesmo processamento para cada arquivo .log encontrado (com filepath.Ext).

Agora responda o exercício sobre o conteúdo:

Ao processar um arquivo de log grande, qual abordagem mantém baixo consumo de memória e aproveita I/O eficiente em Go?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

A leitura em streaming com bufio.Scanner processa o arquivo aos poucos, evitando carregar tudo em memória. Para escrita eficiente, bufio.Writer reduz syscalls e o Flush garante que o buffer seja persistido.

Próximo capitúlo

JSON em Go: encoding/json, marshaling, unmarshaling e validação

Arrow Right Icon
Capa do Ebook gratuito Go (Golang) para Iniciantes: Fundamentos, Concorrência e Estrutura de Projetos
67%

Go (Golang) para Iniciantes: Fundamentos, Concorrência e Estrutura de Projetos

Novo curso

18 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.