E/S de Arquivos

Lição 20: E/S de Arquivos

Analogia da Vida

Imagine que você é um bibliotecário:

Assim como as bibliotecas possuem regras rigorosas de empréstimo, os sistemas operacionais possuem controles de permissão para arquivos. Você precisa de um "cartão de biblioteca" (permissões) para ler e escrever arquivos, e deve "devolvê-los" (fechá-los) prontamente quando terminar, caso contrário outros leitores não poderão usá-los.


Conceitos Centrais

Go fornece recursos ricos de E/S de arquivos através de sua biblioteca padrão, envolvendo principalmente os seguintes pacotes:

Pacote Propósito
os Operações de baixo nível com arquivos: abrir, criar, excluir, renomear
io Interfaces genéricas de E/S: Reader, Writer
bufio Leitura e escrita em buffer: Scanner, Reader, Writer
io/ioutil (obsoleto) Funcionalidade movida para o pacote os após Go 1.16
filepath Manipulação de caminhos multiplataforma: juntar, separar, combinar

Fluxo Básico de Operação com Arquivos

TEXT
Abrir arquivo → Operações de Leitura/Escrita → Fechar arquivo
    │                              │
    └── defer f.Close() ──────────┘   // Garantir fechamento ao sair da função

Comparação de Três Métodos de Leitura

Método Características Caso de Uso
os.ReadFile Carrega o arquivo inteiro na memória de uma vez Arquivos pequenos (< 100MB)
bufio.Scanner Escaneamento linha por linha, amigável com memória Arquivos grandes, processamento linha por linha
io.ReadAll Lê tudo em []byte Respostas de rede e outros dados de streaming

Sintaxe Básica e Uso

1. Abrir e Criar Arquivos

GO
package main

import (
    "fmt"
    "os"
)

func main() {
    // Abrir arquivo em modo somente leitura (arquivo deve existir)
    f, err := os.Open("data.txt")
    if err != nil {
        fmt.Println("Falha ao abrir:", err)
        return
    }
    defer f.Close() // 💡 Sempre use defer para fechar arquivos

    // Abrir arquivo em modo leitura/escrita (arquivo deve existir)
    f2, err := os.OpenFile("data.txt", os.O_RDWR, 0644)
    if err != nil {
        fmt.Println("Falha ao abrir:", err)
        return
    }
    defer f2.Close()

    // Criar um novo arquivo (truncar se existir)
    f3, err := os.Create("newfile.txt")
    if err != nil {
        fmt.Println("Falha ao criar:", err)
        return
    }
    defer f3.Close()
}
💡 Dica: os.Open é equivalente a os.OpenFile(name, os.O_RDONLY, 0) — somente leitura, sem escrita.

2. Bits de Flag do OpenFile

GO
// Combinações comuns de bits de flag
os.O_RDONLY  // Somente leitura
os.O_WRONLY  // Somente escrita
os.O_RDWR    // Leitura e escrita
os.O_APPEND  // Modo de adição
os.O_CREATE  // Criar se o arquivo não existir
os.O_TRUNC   // Truncar ao abrir (limpar conteúdo)

// Escrita de adição
f, err := os.OpenFile("log.txt",
    os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
💡 Dica: A permissão 0644 significa que o proprietário pode ler e escrever, outros apenas leem (sistemas Unix).

3. Ler Arquivos

GO
package main

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

func main() {
    // ===== Método 1: Ler o arquivo inteiro de uma vez =====
    data, err := os.ReadFile("config.txt")
    if err != nil {
        fmt.Println("Falha na leitura:", err)
        return
    }
    fmt.Println(string(data))

    // ===== Método 2: Ler linha por linha (recomendado para arquivos grandes) =====
    file, err := os.Open("access.log")
    if err != nil {
        fmt.Println("Falha ao abrir:", err)
        return
    }
    defer file.Close()

    scanner := bufio.NewScanner(file)
    lineNum := 0
    for scanner.Scan() { // Escanear linha por linha
        lineNum++
        fmt.Printf("Linha %d: %s\n", lineNum, scanner.Text())
    }
    if err := scanner.Err(); err != nil {
        fmt.Println("Erro de escaneamento:", err)
    }
}
💡 Dica: bufio.Scanner tem um comprimento máximo de token padrão de 64KB. Para linhas extra-longas, chame scanner.Buffer() para aumentar o tamanho do buffer.

4. Escrever Arquivos

GO
package main

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

func main() {
    // ===== Método 1: Escrever tudo de uma vez =====
    err := os.WriteFile("output.txt", []byte("Hello, Go!\n"), 0644)
    if err != nil {
        fmt.Println("Falha na escrita:", err)
        return
    }

    // ===== Método 2: Usar bufio.Writer (buffered) =====
    f, err := os.Create("buffered.txt")
    if err != nil {
        fmt.Println("Falha ao criar:", err)
        return
    }
    defer f.Close()

    writer := bufio.NewWriter(f)
    for i := 1; i <= 5; i++ {
        fmt.Fprintf(writer, "Conteúdo da linha %d\n", i)
    }
    writer.Flush() // 💡 Deve chamar Flush, caso contrário os dados ficam no buffer e não são gravados no disco

    // ===== Método 3: Escrita direta =====
    f2, _ := os.Create("direct.txt")
    defer f2.Close()
    f2.WriteString("Escrever string diretamente\n")
    f2.Write([]byte("Escrever slice de bytes diretamente\n"))
}
💡 Dica: bufio.Writer melhora significativamente o desempenho para escritas pequenas frequentes, pois reduz o número de chamadas ao sistema. Mas você sempre deve chamar Flush() no final.

5. Excluir e Renomear

GO
// Excluir um arquivo
err := os.Remove("temp.txt")
if err != nil {
    fmt.Println("Falha ao excluir:", err)
}

// Excluir um diretório e todo o seu conteúdo
err = os.RemoveAll("temp_dir/")

// Renomear / mover um arquivo
err = os.Rename("old.txt", "new.txt")

6. Manipulação de Caminhos (Pacote filepath)

GO
package main

import (
    "fmt"
    "path/filepath"
)

func main() {
    // Juntar caminhos (seguro multiplataforma)
    p := filepath.Join("data", "logs", "app.log")
    fmt.Println(p) // data\logs\app.log (Windows) ou data/logs/app.log (Linux)

    // Separar caminho
    dir, file := filepath.Split("/home/user/doc.txt")
    fmt.Println("Diretório:", dir)   // /home/user/
    fmt.Println("Arquivo:", file)    // doc.txt

    // Obter extensão
    ext := filepath.Ext("report.pdf")
    fmt.Println(ext) // .pdf

    // Obter nome do arquivo sem extensão
    name := filepath.Base("report.pdf")
    fmt.Println(name) // report.pdf

    // Caminho absoluto
    abs, _ := filepath.Abs("relative/path")
    fmt.Println(abs)

    // Correspondência de caminho (padrão glob)
    matched, _ := filepath.Match("*.go", "main.go")
    fmt.Println(matched) // true
}
💡 Dica: Sempre use filepath.Join para concatenar caminhos — nunca concatene manualmente / ou \, caso contrário seu código terá problemas entre plataformas.

7. Operações com Diretórios e Travessia

GO
package main

import (
    "fmt"
    "os"
    "path/filepath"
)

func main() {
    // Criar diretórios
    os.Mkdir("logs", 0755)
    os.MkdirAll("logs/2024/01", 0755) // Criação recursiva

    // Ler conteúdo do diretório
    entries, err := os.ReadDir(".")
    if err != nil {
        fmt.Println("Falha ao ler diretório:", err)
        return
    }
    for _, entry := range entries {
        info, _ := entry.Info()
        fmt.Printf("%-10s %8d bytes  %s\n",
            entry.Name(), info.Size(), info.Mode())
    }

    // Percorrer recursivamente a árvore de diretórios
    fmt.Println("\n=== Travessia Recursiva ===")
    filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return err
        }
        prefix := "📄"
        if info.IsDir() {
            prefix = "📁"
        }
        fmt.Printf("%s %s\n", prefix, path)
        return nil
    })
}
💡 Dica: filepath.Walk visita cada arquivo. Para árvores de diretórios muito grandes, use filepath.WalkDir (Go 1.16+) para melhor desempenho, pois reduz chamadas ao sistema stat.


Exemplos

Exemplo: Ferramenta de Cópia de Arquivo (Dificuldade ⭐)

Implementar uma função simples de cópia de arquivo que suporta arquivos de qualquer tamanho.

GO
package main

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

// copyFile copia o arquivo de origem para o caminho de destino
func copyFile(src, dst string) (int64, error) {
    // Abrir arquivo de origem
    sourceFile, err := os.Open(src)
    if err != nil {
        return 0, fmt.Errorf("falha ao abrir arquivo de origem: %w", err)
    }
    defer sourceFile.Close()

    // Obter informações do arquivo de origem (para definir permissões)
    sourceInfo, err := sourceFile.Stat()
    if err != nil {
        return 0, fmt.Errorf("falha ao obter informações do arquivo: %w", err)
    }

    // Criar arquivo de destino (herda permissões do arquivo de origem)
    destFile, err := os.OpenFile(dst,
        os.O_WRONLY|os.O_CREATE|os.O_TRUNC,
        sourceInfo.Mode())
    if err != nil {
        return 0, fmt.Errorf("falha ao criar arquivo de destino: %w", err)
    }
    defer destFile.Close()

    // Usar io.Copy para cópia em streaming (gerencia buffer automaticamente)
    bytesWritten, err := io.Copy(destFile, sourceFile)
    if err != nil {
        return 0, fmt.Errorf("falha ao copiar dados: %w", err)
    }

    return bytesWritten, nil
}

func main() {
    // Criar arquivo de teste
    os.WriteFile("source.txt", []byte("Este é o conteúdo do arquivo de origem.\nUsado para testar a função de cópia.\n"), 0644)

    // Executar cópia
    n, err := copyFile("source.txt", "copy.txt")
    if err != nil {
        fmt.Println("Erro:", err)
        return
    }
    fmt.Printf("Cópia concluída, %d bytes no total\n", n)

    // Verificar resultado
    data, _ := os.ReadFile("copy.txt")
    fmt.Println("Conteúdo copiado:", string(data))

    // Limpeza
    os.Remove("source.txt")
    os.Remove("copy.txt")
}
▶ Experimente
TEXT
Cópia concluída, 45 bytes no total
Conteúdo copiado: Este é o conteúdo do arquivo de origem.
Usado para testar a função de cópia.

Exemplo: Analisador de Arquivo de Log (Dificuldade ⭐⭐)

Ler um arquivo de log, contar por nível e gerar um resumo.

GO
package main

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

// LogStats armazena estatísticas de log
type LogStats struct {
    Total   int
    Error   int
    Warning int
    Info    int
    Debug   int
}

// analyzeLog analisa o arquivo de log e retorna estatísticas
func analyzeLog(filename string) (*LogStats, error) {
    file, err := os.Open(filename)
    if err != nil {
        return nil, fmt.Errorf("falha ao abrir arquivo de log: %w", err)
    }
    defer file.Close()

    stats := &LogStats{}
    scanner := bufio.NewScanner(file)

    // Aumentar tamanho do buffer para linhas extra-longas
    scanner.Buffer(make([]byte, 1024*1024), 1024*1024)

    for scanner.Scan() {
        line := scanner.Text()
        stats.Total++

        switch {
        case strings.Contains(line, "[ERROR]"):
            stats.Error++
        case strings.Contains(line, "[WARNING]"):
            stats.Warning++
        case strings.Contains(line, "[INFO]"):
            stats.Info++
        case strings.Contains(line, "[DEBUG]"):
            stats.Debug++
        }
    }

    if err := scanner.Err(); err != nil {
        return nil, fmt.Errorf("erro ao ler log: %w", err)
    }

    return stats, nil
}

func main() {
    // Criar arquivo de log simulado
    logContent := `2024-01-15 08:00:01 [INFO] Serviço iniciado com sucesso
2024-01-15 08:00:05 [DEBUG] Carregando arquivo de configuração
2024-01-15 08:01:10 [WARNING] Espaço em disco abaixo de 80%
2024-01-15 08:02:30 [ERROR] Timeout de conexão com banco de dados
2024-01-15 08:02:35 [INFO] Tentando reconectar ao banco de dados
2024-01-15 08:03:00 [ERROR] Falha na autenticação: usuário admin
2024-01-15 08:03:01 [INFO] Processamento de requisição concluído
2024-01-15 08:04:00 [DEBUG] Taxa de acerto do cache 95%
2024-01-15 08:05:00 [WARNING] Tempo de resposta da API excedeu 2s
`
    os.WriteFile("app.log", []byte(logContent), 0644)

    // Analisar logs
    stats, err := analyzeLog("app.log")
    if err != nil {
        fmt.Println("Erro:", err)
        return
    }

    // Gerar relatório
    fmt.Println("========== Relatório de Análise de Log ==========")
    fmt.Printf("Total de linhas: %d\n", stats.Total)
    fmt.Printf("ERROR:   %d (%.1f%%)\n", stats.Error,
        float64(stats.Error)/float64(stats.Total)*100)
    fmt.Printf("WARNING: %d (%.1f%%)\n", stats.Warning,
        float64(stats.Warning)/float64(stats.Total)*100)
    fmt.Printf("INFO:    %d (%.1f%%)\n", stats.Info,
        float64(stats.Info)/float64(stats.Total)*100)
    fmt.Printf("DEBUG:   %d (%.1f%%)\n", stats.Debug,
        float64(stats.Debug)/float64(stats.Total)*100)
    fmt.Println("===================================================")

    // Limpeza
    os.Remove("app.log")
}
▶ Experimente
TEXT
========== Relatório de Análise de Log ==========
Total de linhas: 9
ERROR:   2 (22.2%)
WARNING: 2 (22.2%)
INFO:    3 (33.3%)
DEBUG:   2 (22.2%)
===================================================

Exemplo: Ferramenta de Sincronização de Diretórios (Dificuldade ⭐⭐⭐)

Implementar uma função simples de sincronização de diretórios: escanear o diretório de origem e copiar arquivos novos ou modificados para o diretório de destino.

GO
package main

import (
    "fmt"
    "io"
    "os"
    "path/filepath"
    "time"
)

// FileInfo armazena informações do arquivo em cache
type FileInfo struct {
    Path    string
    ModTime time.Time
    Size    int64
}

// scanDir escaneia um diretório e retorna um mapa de informações de arquivos
func scanDir(dir string) (map[string]FileInfo, error) {
    files := make(map[string]FileInfo)

    err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return err
        }
        if info.IsDir() {
            return nil
        }

        // Calcular caminho relativo como chave
        relPath, err := filepath.Rel(dir, path)
        if err != nil {
            return err
        }

        files[relPath] = FileInfo{
            Path:    path,
            ModTime: info.ModTime(),
            Size:    info.Size(),
        }
        return nil
    })

    return files, err
}

// copyFileData copia um único arquivo
func copyFileData(src, dst string) error {
    srcFile, err := os.Open(src)
    if err != nil {
        return err
    }
    defer srcFile.Close()

    // Garantir que o diretório de destino existe
    dstDir := filepath.Dir(dst)
    if err := os.MkdirAll(dstDir, 0755); err != nil {
        return err
    }

    dstFile, err := os.Create(dst)
    if err != nil {
        return err
    }
    defer dstFile.Close()

    _, err = io.Copy(dstFile, srcFile)
    return err
}

// syncDir sincroniza o diretório de origem para o diretório de destino
func syncDir(src, dst string) error {
    fmt.Printf("Sincronização: %s → %s\n\n", src, dst)

    // Escanear ambos os diretórios
    srcFiles, err := scanDir(src)
    if err != nil {
        return fmt.Errorf("falha ao escanear diretório de origem: %w", err)
    }

    dstFiles, err := scanDir(dst)
    if err != nil && !os.IsNotExist(err) {
        return fmt.Errorf("falha ao escanear diretório de destino: %w", err)
    }

    copied, updated, skipped := 0, 0, 0

    // Iterar sobre arquivos do diretório de origem
    for relPath, srcInfo := range srcFiles {
        dstPath := filepath.Join(dst, relPath)

        dstInfo, exists := dstFiles[relPath]

        switch {
        case !exists:
            // Arquivo novo, copiar
            fmt.Printf("  [Novo] %s\n", relPath)
            if err := copyFileData(srcInfo.Path, dstPath); err != nil {
                return fmt.Errorf("falha ao copiar %s: %w", relPath, err)
            }
            copied++

        case srcInfo.ModTime.After(dstInfo.ModTime) || srcInfo.Size != dstInfo.Size:
            // Arquivo modificado, atualizar
            fmt.Printf("  [Atualizado] %s\n", relPath)
            if err := copyFileData(srcInfo.Path, dstPath); err != nil {
                return fmt.Errorf("falha ao atualizar %s: %w", relPath, err)
            }
            updated++

        default:
            // Arquivo inalterado, pular
            skipped++
        }
    }

    fmt.Printf("\nSincronização concluída: %d novos, %d atualizados, %d ignorados\n",
        copied, updated, skipped)
    return nil
}

func main() {
    // Criar estrutura de diretórios de teste
    os.MkdirAll("src_dir/subdir", 0755)
    os.WriteFile("src_dir/main.go", []byte("package main\n"), 0644)
    os.WriteFile("src_dir/readme.txt", []byte("README\n"), 0644)
    os.WriteFile("src_dir/subdir/util.go", []byte("package util\n"), 0644)

    // Criar diretório de destino (com alguns arquivos)
    os.MkdirAll("dst_dir", 0755)
    os.WriteFile("dst_dir/readme.txt", []byte("README antigo\n"), 0644)
    os.WriteFile("dst_dir/old.txt", []byte("Este arquivo não está no diretório de origem\n"), 0644)

    // Executar sincronização
    err := syncDir("src_dir", "dst_dir")
    if err != nil {
        fmt.Println("Erro de sincronização:", err)
        return
    }

    // Verificar resultado
    fmt.Println("\n=== Conteúdo do Diretório de Destino ===")
    filepath.Walk("dst_dir", func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return err
        }
        relPath, _ := filepath.Rel("dst_dir", path)
        prefix := "📁"
        if !info.IsDir() {
            prefix = "📄"
        }
        fmt.Printf("  %s %s\n", prefix, relPath)
        return nil
    })

    // Limpeza
    os.RemoveAll("src_dir")
    os.RemoveAll("dst_dir")
}
▶ Experimente
TEXT
Sincronização: src_dir → dst_dir

  [Novo] main.go
  [Atualizado] readme.txt
  [Novo] subdir\util.go

Sincronização concluída: 2 novos, 1 atualizados, 0 ignorados

=== Conteúdo do Diretório de Destino ===
  📁 .
  📁 subdir
  📄 subdir\util.go
  📄 main.go
  📄 old.txt
  📄 readme.txt

Cenários de Aplicação Prática

Cenário 1: Recarregamento de Configuração em Tempo Real

Monitorar alterações no arquivo de configuração e recarregar automaticamente.

GO
package main

import (
    "encoding/json"
    "fmt"
    "os"
    "time"
)

// Config configuração da aplicação
type Config struct {
    Server   ServerConfig   `json:"server"`
    Database DatabaseConfig `json:"database"`
}

type ServerConfig struct {
    Port int    `json:"port"`
    Host string `json:"host"`
}

type DatabaseConfig struct {
    DSN         string `json:"dsn"`
    MaxOpenConn int    `json:"max_open_conn"`
}

// loadConfig carrega configuração de um arquivo JSON
func loadConfig(filename string) (*Config, error) {
    data, err := os.ReadFile(filename)
    if err != nil {
        return nil, fmt.Errorf("falha ao ler arquivo de configuração: %w", err)
    }

    var config Config
    if err := json.Unmarshal(data, &config); err != nil {
        return nil, fmt.Errorf("falha ao analisar configuração: %w", err)
    }

    return &config, nil
}

// watchConfig monitora alterações no arquivo de configuração e recarrega
func watchConfig(filename string, interval time.Duration, callback func(*Config)) {
    var lastModTime time.Time

    // Carregamento inicial
    info, err := os.Stat(filename)
    if err == nil {
        lastModTime = info.ModTime()
        if config, err := loadConfig(filename); err == nil {
            callback(config)
        }
    }

    ticker := time.NewTicker(interval)
    defer ticker.Stop()

    for range ticker.C {
        info, err := os.Stat(filename)
        if err != nil {
            fmt.Printf("Falha ao verificar arquivo de configuração: %v\n", err)
            continue
        }

        if info.ModTime().After(lastModTime) {
            fmt.Printf("[%s] Alteração no arquivo de configuração detectada, recarregando...\n",
                time.Now().Format("15:04:05"))

            config, err := loadConfig(filename)
            if err != nil {
                fmt.Printf("Falha ao recarregar: %v\n", err)
                continue
            }

            lastModTime = info.ModTime()
            callback(config)
        }
    }
}

func main() {
    // Criar configuração inicial
    configJSON := `{
    "server": {
        "port": 8080,
        "host": "localhost"
    },
    "database": {
        "dsn": "user:pass@tcp(localhost:3306)/mydb",
        "max_open_conn": 25
    }
}`
    os.WriteFile("config.json", []byte(configJSON), 0644)
    defer os.Remove("config.json")

    // Iniciar monitoramento de configuração
    go watchConfig("config.json", 2*time.Second, func(config *Config) {
        fmt.Printf("  Servidor: %s:%d\n", config.Server.Host, config.Server.Port)
        fmt.Printf("  Banco de Dados: %s (Máx. conexões: %d)\n",
            config.Database.DSN, config.Database.MaxOpenConn)
    })

    // Simular execução
    time.Sleep(3 * time.Second)

    // Simular atualização de configuração
    fmt.Println("\n>> Atualizando arquivo de configuração...")
    updatedJSON := `{
    "server": {
        "port": 9090,
        "host": "0.0.0.0"
    },
    "database": {
        "dsn": "user:pass@tcp(db-host:3306)/mydb",
        "max_open_conn": 50
    }
}`
    os.WriteFile("config.json", []byte(updatedJSON), 0644)

    // Aguardar detecção de alteração
    time.Sleep(5 * time.Second)
}
TEXT
  Servidor: localhost:8080
  Banco de Dados: user:pass@tcp(localhost:3306)/mydb (Máx. conexões: 25)

>> Atualizando arquivo de configuração...
[14:30:02] Alteração no arquivo de configuração detectada, recarregando...
  Servidor: 0.0.0.0:9090
  Banco de Dados: user:pass@tcp(db-host:3306)/mydb (Máx. conexões: 50)

Cenário 2: Ferramenta de Renomeação em Lote

Renomear arquivos em um diretório de acordo com regras.

GO
package main

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

// RenameRule regra de renomeação
type RenameRule struct {
    Find    // String a ser encontrada
    Replace string // String de substituição
}

// batchRename renomeia arquivos em lote
func batchRename(dir string, rule RenameRule) ([]string, error) {
    entries, err := os.ReadDir(dir)
    if err != nil {
        return nil, fmt.Errorf("falha ao ler diretório: %w", err)
    }

    var renamed []string

    for _, entry := range entries {
        if entry.IsDir() {
            continue
        }

        oldName := entry.Name()
        newName := strings.ReplaceAll(oldName, rule.Find, rule.Replace)

        if oldName == newName {
            continue // Nenhuma renomeação necessária
        }

        oldPath := filepath.Join(dir, oldName)
        newPath := filepath.Join(dir, newName)

        // Verificar se o arquivo de destino já existe
        if _, err := os.Stat(newPath); err == nil {
            fmt.Printf("  [Ignorado] %s → %s (destino já existe)\n", oldName, newName)
            continue
        }

        if err := os.Rename(oldPath, newPath); err != nil {
            fmt.Printf("  [Erro] %s: %v\n", oldName, err)
            continue
        }

        fmt.Printf("  [Renomeado] %s → %s\n", oldName, newName)
        renamed = append(renamed, newName)
    }

    return renamed, nil
}

func main() {
    // Criar arquivos de teste
    os.MkdirAll("photos", 0755)
    testFiles := []string{
        "IMG_20240115_001.jpg",
        "IMG_20240115_002.jpg",
        "IMG_20240115_003.jpg",
        "IMG_20240116_001.jpg",
        "IMG_20240116_002.jpg",
        "notes.txt",
    }
    for _, name := range testFiles {
        os.WriteFile(filepath.Join("photos", name), []byte(""), 0644)
    }

    // Regra 1: Substituir prefixo
    fmt.Println("=== Regra 1: IMG → Foto ===")
    rule1 := RenameRule{Find: "IMG_", Replace: "Foto_"}
    renamed, _ := batchRename("photos", rule1)
    fmt.Printf("%d arquivos renomeados no total\n\n", len(renamed))

    // Regra 2: Adicionar prefixo de data
    fmt.Println("=== Regra 2: Foto_ → 2024_Ferias_ ===")
    rule2 := RenameRule{Find: "Foto_", Replace: "2024_Ferias_"}
    renamed, _ = batchRename("photos", rule2)
    fmt.Printf("%d arquivos renomeados no total\n", len(renamed))

    // Mostrar resultados finais
    fmt.Println("\n=== Lista Final de Arquivos ===")
    entries, _ := os.ReadDir("photos")
    for _, entry := range entries {
        fmt.Printf("  %s\n", entry.Name())
    }

    // Limpeza
    os.RemoveAll("photos")
}
TEXT
=== Regra 1: IMG → Foto ===
  [Renomeado] IMG_20240115_001.jpg → Foto_20240115_001.jpg
  [Renomeado] IMG_20240115_002.jpg → Foto_20240115_002.jpg
  [Renomeado] IMG_20240115_003.jpg → Foto_20240115_003.jpg
  [Renomeado] IMG_20240116_001.jpg → Foto_20240116_001.jpg
  [Renomeado] IMG_20240116_002.jpg → Foto_20240116_002.jpg
5 arquivos renomeados no total

=== Regra 2: Foto_ → 2024_Ferias_ ===
  [Renomeado] Foto_20240115_001.jpg → 2024_Ferias_20240115_001.jpg
  [Renomeado] Foto_20240115_002.jpg → 2024_Ferias_20240115_002.jpg
  [Renomeado] Foto_20240115_003.jpg → 2024_Ferias_20240115_003.jpg
  [Renomeado] Foto_20240116_001.jpg → 2024_Ferias_20240116_001.jpg
  [Renomeado] Foto_20240116_002.jpg → 2024_Ferias_20240116_002.jpg
5 arquivos renomeados no total

=== Lista Final de Arquivos ===
  2024_Ferias_20240115_001.jpg
  2024_Ferias_20240115_002.jpg
  2024_Ferias_20240115_003.jpg
  2024_Ferias_20240116_001.jpg
  2024_Ferias_20240116_002.jpg
  notes.txt

❓ Perguntas Frequentes

P1: Por que o uso de memória dispara ao ler arquivos grandes?

GO
// ❌ Errado: arquivo inteiro carregado na memória
data, _ := os.ReadFile("huge.log") // Arquivo de 10GB → explosão de memória

// ✅ Correto: ler linha por linha
file, _ := os.Open("huge.log")
defer file.Close()

scanner := bufio.NewScanner(file)
for scanner.Scan() {
    line := scanner.Text()
    // Processar uma linha por vez, uso mínimo de memória
    processLine(line)
}

Ponto-chave: os.ReadFile é adequado para arquivos pequenos (< 100MB). Para arquivos grandes, sempre use bufio.Scanner para processar linha por linha / em pedaços.

P2: O que acontece se defer f.Close() for colocado antes da verificação err != nil?

GO
// ❌ Pode causar pânico de ponteiro nulo
f, err := os.Open("file.txt")
defer f.Close() // Se Open falhar, f é nil, Close causará pânico
if err != nil {
    return err
}

// ✅ Correto: verificar erro primeiro
f, err := os.Open("file.txt")
if err != nil {
    return err
}
defer f.Close() // Garante que f não é nil

P3: Como lidar com diferenças de caminho entre Windows e Linux?

GO
// ❌ Separadores de caminho codificados
path := "data" + "/" + "file.txt"     // Funciona no Linux, arriscado no Windows
path := "data" + "\\" + "file.txt"    // Funciona no Windows, falha no Linux

// ✅ Usar filepath.Join
path := filepath.Join("data", "file.txt")

// ✅ Usar strings brutas (caminhos do Windows)
path := `C:\Users\admin\file.txt`

// ✅ Obter caminho relativo a partir do caminho absoluto
rel, _ := filepath.Rel("/home/user", "/home/user/docs/file.txt")
// rel = "docs/file.txt"

P4: O que fazer quando dados são perdidos durante escritas em arquivo?

GO
// Causa: usou bufio.Writer mas esqueceu de chamar Flush
writer := bufio.NewWriter(f)
writer.WriteString("dados importantes")
// Programa trava ou sai → dados ainda no buffer, não gravados no disco

// ✅ Solução 1: Sempre defer Flush
defer writer.Flush()

// ✅ Solução 2: Escrever dados críticos diretamente
f.WriteString("dados importantes") // Escrita direta, bypass do buffer

// ✅ Solução 3: Sincronizar com o disco imediatamente após a escrita
f.Sync() // Chama a chamada de sistema fsync

📖 Resumo

Esta lição cobriu o conhecimento central de E/S de arquivos em Go:

Ponto de Conhecimento Conclusão Principal
os.Open/Create/Remove Operações de baixo nível com arquivos, requer defer Close() manual
os.ReadFile/WriteFile Método de leitura/escrita recomendado para Go 1.16+
bufio.Scanner Melhor escolha para ler arquivos grandes linha por linha
bufio.Writer Otimização de desempenho para escritas pequenas frequentes, não esqueça do Flush()
filepath.Join A maneira correta de concatenar caminhos multiplataforma
filepath.Walk/WalkDir Percorrer recursivamente árvores de diretórios
io.Copy Cópia em streaming, gerencia buffer automaticamente

Princípios Fundamentais:

  1. Sempre defer f.Close() — após a verificação de erro
  2. Arquivos pequenos usam os.ReadFile, arquivos grandes usam bufio.Scanner
  3. Concatenação de caminhos usa filepath.Join — não concatene separadores manualmente
  4. bufio.Writer deve chamar Flush() — caso contrário os dados podem ser perdidos
  5. O tratamento de erros não pode ser ignorado — erros de operação com arquivos são especialmente importantes

📝 Exercícios

Exercício 1: Contador de Palavras

Escreva um programa que lê um arquivo de texto, conta o número de palavras, linhas e caracteres, e exibe os resultados.

GO
// Dicas:
// - Use bufio.Scanner para ler linha por linha
// - Use strings.Fields() para dividir cada linha em palavras
// - Conte todas as três métricas e formate a saída

Exercício 2: CSV para JSON

Escreva um programa que lê um arquivo CSV (primeira linha são cabeçalhos), converte para formato de array JSON e escreve em um novo arquivo.

GO
// Dicas:
// - Use bufio.Scanner para ler CSV linha por linha
// - Primeira linha serve como chaves para objetos JSON
// - Linhas subsequentes são valores, divida com strings.Split
// - Use encoding/json para serializar a saída

Exercício 3: Calculadora de Tamanho de Diretório

Escreva um programa que calcula recursivamente o tamanho total de um diretório especificado e agrupa estatísticas por tipo de arquivo (extensão).

GO
// Dicas:
// - Use filepath.WalkDir para percorrer o diretório
// - Use map[string]int64 para acumular tamanho por extensão
// - Use formatação legível para saída (ex.: 1.5MB, 230KB)
// - Trate arquivos sem extensão (categorize como "sem extensão")

Próxima Lição

Na próxima lição, aprenderemos sobre Processamento de JSON — como analisar e gerar dados JSON em Go, o que é uma habilidade fundamental para construir APIs e lidar com arquivos de configuração.

👉 Lição 21: Processamento de JSON

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%