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.Opencom 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.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
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_RDWRos.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 leem0o600: somente dono lê/escreve0o755: 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çõesfilepath.Direfilepath.Base: diretório e nome finalfilepath.Ext: extensãofilepath.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, extLeitura 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 } _ = bCopiar 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 linhaLeitura 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
deferpara fechar arquivos assim que abrir. - Ao ler com
Scanner, sempre verifiquesc.Err()após o loop. - Ao escrever com
bufio.Writer, garantaFlushe 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
ERROReWARN - Filtrar linhas com
ERRORpara 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=456Passo 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.Scannerpara 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 usandobufio.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.Scannerebufio.Writerreduzem overhead de I/O. - Saída incremental: linhas de erro são gravadas conforme aparecem, sem acumular em slices.
Extensões sugeridas
- Adicionar flag
-levelpara filtrar apenas um nível (INFO/WARN/ERROR). - Gerar um CSV com
módulo,contagemusandofmt.Fprintfno writer. - Processar um diretório de logs: usar
os.ReadDire aplicar o mesmo processamento para cada arquivo.logencontrado (comfilepath.Ext).