E/S de Arquivos
Lição 20: E/S de Arquivos
Analogia da Vida
Imagine que você é um bibliotecário:
- Abrir um arquivo = Pegar um livro da estante e abrir para ler
- Criar um arquivo = Pegar um caderno em branco e começar a escrever conteúdo
- Ler um arquivo = Folhear as páginas e ler o texto
- Escrever um arquivo = Usar uma caneta para escrever novo conteúdo no papel
- Fechar um arquivo = Fechar o livro e devolvê-lo à estante
- Excluir um arquivo = Remover permanentemente o livro da estante
- Percorrer um diretório = Visitar todas as estantes e seções da biblioteca
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
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
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()
}
os.Open é equivalente a os.OpenFile(name, os.O_RDONLY, 0) — somente leitura, sem escrita.
2. Bits de Flag do OpenFile
// 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)
0644 significa que o proprietário pode ler e escrever, outros apenas leem (sistemas Unix).
3. Ler Arquivos
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)
}
}
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
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"))
}
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
// 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)
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
}
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
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
})
}
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.
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")
}
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.
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")
}
========== 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.
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")
}
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.
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)
}
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.
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")
}
=== 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?
// ❌ 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?
// ❌ 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?
// ❌ 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?
// 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:
- Sempre
defer f.Close()— após a verificação de erro - Arquivos pequenos usam
os.ReadFile, arquivos grandes usambufio.Scanner - Concatenação de caminhos usa
filepath.Join— não concatene separadores manualmente bufio.Writerdeve chamarFlush()— caso contrário os dados podem ser perdidos- 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.
// 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.
// 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).
// 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.



