Golang: Manipulação de arquivos (Parte 17)

·12 min de leitura

Golang: Manipulação de arquivos (Parte 17)

A manipulação de arquivos é uma tarefa fundamental em muitas aplicações. Em Go, o pacote os fornece a maioria das funções necessárias para interagir com o sistema de arquivos. Este tutorial abordará as operações mais comuns, com exemplos práticos e explicações detalhadas.

1. O Pacote os

O pacote os é o principal ponto de entrada para operações de sistema de arquivos em Go. Ele oferece funções para criar, abrir, ler, escrever, renomear, remover arquivos e diretórios, além de obter informações sobre eles.

2. Criando Arquivos

Para criar um novo arquivo, você pode usar os.Create(). Se o arquivo já existir, os.Create() o truncará (limpará seu conteúdo).

package main

import (
	"fmt"
	"os"
)

func main() {
	// Criar um novo arquivo
	file, err := os.Create("exemplo.txt")
	if err != nil {
		fmt.Println("Erro ao criar o arquivo:", err)
		return
	}
	defer file.Close() // Garante que o arquivo será fechado no final

	fmt.Println("Arquivo 'exemplo.txt' criado com sucesso!")
}

Explicação:

  • os.Create("exemplo.txt"): Tenta criar um arquivo chamado exemplo.txt. Retorna um ponteiro para os.File e um erro.
  • defer file.Close(): É crucial fechar o arquivo após o uso para liberar os recursos do sistema. defer garante que file.Close() será chamado antes que a função main retorne, mesmo que ocorra um erro.

3. Escrevendo em Arquivos

Existem várias maneiras de escrever em um arquivo em Go.

3.1. Usando file.WriteString()

Ideal para escrever strings.

package main

import (
	"fmt"
	"os"
)

func main() {
	file, err := os.Create("dados.txt")
	if err != nil {
		fmt.Println("Erro ao criar o arquivo:", err)
		return
	}
	defer file.Close()

	conteudo := "Olá, Go!\nEste é um exemplo de escrita em arquivo."
	bytesEscritos, err := file.WriteString(conteudo)
	if err != nil {
		fmt.Println("Erro ao escrever no arquivo:", err)
		return
	}

	fmt.Printf("Escritos %d bytes no arquivo 'dados.txt'\n", bytesEscritos)
}

3.2. Usando file.Write()

Para escrever slices de bytes ([]byte).

package main

import (
	"fmt"
	"os"
)

func main() {
	file, err := os.Create("bytes.txt")
	if err != nil {
		fmt.Println("Erro ao criar o arquivo:", err)
		return
	}
	defer file.Close()

	dados := []byte("Escrevendo bytes diretamente.\n")
	bytesEscritos, err := file.Write(dados)
	if err != nil {
		fmt.Println("Erro ao escrever no arquivo:", err)
		return
	}

	fmt.Printf("Escritos %d bytes no arquivo 'bytes.txt'\n", bytesEscritos)
}

3.3. Usando io.WriteString() e bufio.Writer para Eficiência

Para grandes volumes de dados, usar um bufio.Writer pode melhorar a performance, pois ele armazena os dados em um buffer antes de escrevê-los no disco.

package main

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

func main() {
	file, err := os.Create("buffered_write.txt")
	if err != nil {
		fmt.Println("Erro ao criar o arquivo:", err)
		return
	}
	defer file.Close()

	writer := bufio.NewWriter(file)
	_, err = writer.WriteString("Linha 1 com buffer.\n")
	if err != nil {
		fmt.Println("Erro ao escrever no buffer:", err)
		return
	}
	_, err = writer.WriteString("Linha 2 com buffer.\n")
	if err != nil {
		fmt.Println("Erro ao escrever no buffer:", err)
		return
	}

	// É crucial chamar Flush() para garantir que todos os dados no buffer sejam gravados no arquivo.
	err = writer.Flush()
	if err != nil {
		fmt.Println("Erro ao descarregar o buffer:", err)
		return
	}

	fmt.Println("Dados escritos em 'buffered_write.txt' usando buffer.")
}

4. Lendo Arquivos

Go oferece várias maneiras de ler o conteúdo de um arquivo.

4.1. Lendo o Arquivo Inteiro com os.ReadFile() (Go 1.16+)

A maneira mais simples para arquivos pequenos.

package main

import (
	"fmt"
	"os"
)

func main() {
	// Criar um arquivo para leitura
	err := os.WriteFile("leitura.txt", []byte("Conteúdo do arquivo para leitura."), 0644)
	if err != nil {
		fmt.Println("Erro ao preparar o arquivo:", err)
		return
	}

	// Ler o arquivo inteiro
	conteudo, err := os.ReadFile("leitura.txt")
	if err != nil {
		fmt.Println("Erro ao ler o arquivo:", err)
		return
	}

	fmt.Println("Conteúdo do arquivo 'leitura.txt':")
	fmt.Println(string(conteudo))
}

Explicação:

  • os.WriteFile(): Uma função de conveniência para escrever dados em um arquivo. O 0644 são as permissões do arquivo (leitura/escrita para o proprietário, leitura para grupo e outros).
  • os.ReadFile("leitura.txt"): Lê todo o conteúdo do arquivo e o retorna como um slice de bytes.

4.2. Lendo Linha por Linha com bufio.Scanner

Ideal para arquivos grandes, onde ler o arquivo inteiro na memória não é viável.

package main

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

func main() {
	// Criar um arquivo com múltiplas linhas para leitura
	file, err := os.Create("linhas.txt")
	if err != nil {
		fmt.Println("Erro ao criar o arquivo:", err)
		return
	}
	_, err = file.WriteString("Primeira linha.\nSegunda linha.\nTerceira linha.")
	if err != nil {
		fmt.Println("Erro ao escrever no arquivo:", err)
		file.Close()
		return
	}
	file.Close() // Fechar o arquivo após a escrita

	// Abrir o arquivo para leitura
	file, err = os.Open("linhas.txt")
	if err != nil {
		fmt.Println("Erro ao abrir o arquivo:", err)
		return
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	linhaNum := 1
	for scanner.Scan() {
		fmt.Printf("Linha %d: %s\n", linhaNum, scanner.Text())
		linhaNum++
	}

	if err := scanner.Err(); err != nil {
		fmt.Println("Erro ao ler o arquivo com scanner:", err)
	}
}

Explicação:

  • os.Open("linhas.txt"): Abre um arquivo para leitura.
  • bufio.NewScanner(file): Cria um novo scanner que lê do arquivo. Por padrão, ele divide a entrada por linhas.
  • scanner.Scan(): Avança o scanner para a próxima linha. Retorna true se houver mais linhas, false caso contrário (fim do arquivo ou erro).
  • scanner.Text(): Retorna a linha atual como uma string.
  • scanner.Err(): Verifica se ocorreu algum erro durante a leitura.

4.3. Lendo em Chunks com file.Read()

Para controle mais granular sobre a leitura, lendo blocos de bytes.

package main

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

func main() {
	// Criar um arquivo para leitura em chunks
	err := os.WriteFile("chunks.txt", []byte("Este é um arquivo para leitura em pedaços."), 0644)
	if err != nil {
		fmt.Println("Erro ao preparar o arquivo:", err)
		return
	}

	file, err := os.Open("chunks.txt")
	if err != nil {
		fmt.Println("Erro ao abrir o arquivo:", err)
		return
	}
	defer file.Close()

	buffer := make([]byte, 5) // Buffer de 5 bytes
	fmt.Println("Lendo 'chunks.txt' em pedaços:")

	for {
		n, err := file.Read(buffer)
		if err != nil {
			if err == io.EOF {
				break // Fim do arquivo
			}
			fmt.Println("Erro ao ler o arquivo:", err)
			return
		}
		fmt.Printf("Lidos %d bytes: %s\n", n, string(buffer[:n]))
	}
}

Explicação:

  • buffer := make([]byte, 5): Cria um slice de bytes para atuar como buffer.
  • file.Read(buffer): Tenta ler até len(buffer) bytes no buffer. Retorna o número de bytes lidos (n) e um erro.
  • io.EOF: É o erro retornado quando o fim do arquivo é alcançado.

5. Abrindo Arquivos com os.OpenFile()

os.OpenFile() oferece mais controle sobre como um arquivo é aberto, permitindo especificar o modo de abertura (somente leitura, escrita, etc.) e as permissões.

package main

import (
	"fmt"
	"os"
)

func main() {
	// Criar um arquivo para demonstração
	os.WriteFile("config.txt", []byte("config=valor"), 0644)

	// Abrir para leitura (somente)
	fileRead, err := os.OpenFile("config.txt", os.O_RDONLY, 0644)
	if err != nil {
		fmt.Println("Erro ao abrir para leitura:", err)
		return
	}
	defer fileRead.Close()
	fmt.Println("Arquivo 'config.txt' aberto para leitura.")

	// Abrir para escrita (truncar se existir, criar se não existir)
	fileWrite, err := os.OpenFile("log.txt", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
	if err != nil {
		fmt.Println("Erro ao abrir para escrita:", err)
		return
	}
	defer fileWrite.Close()
	fmt.Println("Arquivo 'log.txt' aberto para escrita (truncado/criado).")

	// Abrir para escrita (apenas adicionar, criar se não existir)
	fileAppend, err := os.OpenFile("historico.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		fmt.Println("Erro ao abrir para adição:", err)
		return
	}
	defer fileAppend.Close()
	fmt.Println("Arquivo 'historico.txt' aberto para adição.")
}

Explicação dos flags:

  • os.O_RDONLY: Abre o arquivo somente para leitura.
  • os.O_WRONLY: Abre o arquivo somente para escrita.
  • os.O_RDWR: Abre o arquivo para leitura e escrita.
  • os.O_APPEND: Adiciona dados ao final do arquivo.
  • os.O_CREATE: Cria o arquivo se ele não existir.
  • os.O_TRUNC: Trunca o arquivo (limpa o conteúdo) se ele já existir e for aberto para escrita.
  • os.O_EXCL: Usado com os.O_CREATE, retorna um erro se o arquivo já existir (garante criação exclusiva).

6. Adicionando Conteúdo a Arquivos (Append)

Para adicionar conteúdo ao final de um arquivo existente, use os.OpenFile() com a flag os.O_APPEND.

package main

import (
	"fmt"
	"os"
)

func main() {
	// Criar ou garantir que o arquivo exista
	file, err := os.OpenFile("log_eventos.txt", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
	if err != nil {
		fmt.Println("Erro ao abrir/criar o arquivo:", err)
		return
	}
	defer file.Close()

	_, err = file.WriteString("Evento: Usuário logado.\n")
	if err != nil {
		fmt.Println("Erro ao adicionar conteúdo:", err)
		return
	}

	fmt.Println("Conteúdo adicionado a 'log_eventos.txt'.")

	_, err = file.WriteString("Evento: Item adicionado ao carrinho.\n")
	if err != nil {
		fmt.Println("Erro ao adicionar conteúdo:", err)
		return
	}
	fmt.Println("Mais conteúdo adicionado a 'log_eventos.txt'.")
}

7. Obtendo Informações do Arquivo (os.Stat())

os.Stat() retorna um os.FileInfo que contém metadados sobre o arquivo ou diretório.

package main

import (
	"fmt"
	"os"
	"time"
)

func main() {
	// Criar um arquivo para obter informações
	err := os.WriteFile("info.txt", []byte("Algum texto."), 0644)
	if err != nil {
		fmt.Println("Erro ao preparar o arquivo:", err)
		return
	}

	info, err := os.Stat("info.txt")
	if err != nil {
		fmt.Println("Erro ao obter informações do arquivo:", err)
		return
	}

	fmt.Println("Informações do arquivo 'info.txt':")
	fmt.Printf("Nome: %s\n", info.Name())
	fmt.Printf("Tamanho: %d bytes\n", info.Size())
	fmt.Printf("É um diretório? %t\n", info.IsDir())
	fmt.Printf("Modo de permissão: %s\n", info.Mode())
	fmt.Printf("Última modificação: %s\n", info.ModTime().Format(time.RFC1123))
	fmt.Printf("Sistema (dependente do SO): %+v\n", info.Sys()) // Informações específicas do sistema operacional
}

8. Deletando Arquivos (os.Remove())

Para remover um arquivo ou diretório vazio.

package main

import (
	"fmt"
	"os"
)

func main() {
	// Criar um arquivo para ser removido
	err := os.WriteFile("para_remover.txt", []byte("Este arquivo será deletado."), 0644)
	if err != nil {
		fmt.Println("Erro ao preparar o arquivo:", err)
		return
	}
	fmt.Println("Arquivo 'para_remover.txt' criado.")

	// Remover o arquivo
	err = os.Remove("para_remover.txt")
	if err != nil {
		fmt.Println("Erro ao remover o arquivo:", err)
		return
	}

	fmt.Println("Arquivo 'para_remover.txt' removido com sucesso!")

	// Tentar remover um arquivo que não existe (resultará em erro)
	err = os.Remove("nao_existe.txt")
	if err != nil {
		fmt.Println("Erro ao tentar remover 'nao_existe.txt':", err) // Saída esperada: "remove nao_existe.txt: no such file or directory"
	}
}

9. Renomeando/Movendo Arquivos (os.Rename())

os.Rename() pode ser usado para renomear um arquivo ou movê-lo para um novo local no mesmo sistema de arquivos.

package main

import (
	"fmt"
	"os"
)

func main() {
	// Criar um arquivo para renomear
	err := os.WriteFile("antigo_nome.txt", []byte("Conteúdo do arquivo antigo."), 0644)
	if err != nil {
		fmt.Println("Erro ao preparar o arquivo:", err)
		return
	}
	fmt.Println("Arquivo 'antigo_nome.txt' criado.")

	// Renomear o arquivo
	err = os.Rename("antigo_nome.txt", "novo_nome.txt")
	if err != nil {
		fmt.Println("Erro ao renomear o arquivo:", err)
		return
	}
	fmt.Println("Arquivo renomeado de 'antigo_nome.txt' para 'novo_nome.txt'.")

	// Mover o arquivo para um subdiretório (se o diretório existir)
	// Primeiro, criar o diretório
	err = os.Mkdir("subdiretorio", 0755)
	if err != nil && !os.IsExist(err) { // Ignorar erro se o diretório já existir
		fmt.Println("Erro ao criar subdiretorio:", err)
		return
	}

	err = os.Rename("novo_nome.txt", "subdiretorio/arquivo_movido.txt")
	if err != nil {
		fmt.Println("Erro ao mover o arquivo:", err)
		return
	}
	fmt.Println("Arquivo 'novo_nome.txt' movido para 'subdiretorio/arquivo_movido.txt'.")

	// Limpeza
	os.RemoveAll("subdiretorio")
}

10. Trabalhando com Diretórios

10.1. Criando Diretórios (os.Mkdir(), os.MkdirAll())

  • os.Mkdir(path, perm): Cria um único diretório. Retorna um erro se o diretório pai não existir.
  • os.MkdirAll(path, perm): Cria um diretório e quaisquer diretórios pais necessários.
package main

import (
	"fmt"
	"os"
)

func main() {
	// Criar um único diretório
	err := os.Mkdir("meu_diretorio", 0755)
	if err != nil {
		if os.IsExist(err) {
			fmt.Println("Diretório 'meu_diretorio' já existe.")
		} else {
			fmt.Println("Erro ao criar 'meu_diretorio':", err)
			return
		}
	} else {
		fmt.Println("Diretório 'meu_diretorio' criado.")
	}

	// Criar diretórios aninhados
	err = os.MkdirAll("pasta/subpasta/final", 0755)
	if err != nil {
		if os.IsExist(err) {
			fmt.Println("Diretório 'pasta/subpasta/final' já existe.")
		} else {
			fmt.Println("Erro ao criar 'pasta/subpasta/final':", err)
			return
		}
	} else {
		fmt.Println("Diretórios 'pasta/subpasta/final' criados.")
	}

	// Limpeza
	os.RemoveAll("meu_diretorio")
	os.RemoveAll("pasta")
}

10.2. Removendo Diretórios (os.Remove(), os.RemoveAll())

  • os.Remove(path): Remove um diretório vazio.
  • os.RemoveAll(path): Remove um diretório e todo o seu conteúdo (arquivos e subdiretórios). Use com cautela!
package main

import (
	"fmt"
	"os"
)

func main() {
	// Criar diretório vazio para remover com os.Remove
	os.Mkdir("diretorio_vazio", 0755)
	fmt.Println("Diretório 'diretorio_vazio' criado.")
	err := os.Remove("diretorio_vazio")
	if err != nil {
		fmt.Println("Erro ao remover 'diretorio_vazio':", err)
	} else {
		fmt.Println("Diretório 'diretorio_vazio' removido.")
	}

	// Criar diretório com conteúdo para remover com os.RemoveAll
	os.MkdirAll("diretorio_com_conteudo/sub", 0755)
	os.WriteFile("diretorio_com_conteudo/arquivo.txt", []byte("teste"), 0644)
	fmt.Println("Diretório 'diretorio_com_conteudo' e seu conteúdo criados.")

	err = os.RemoveAll("diretorio_com_conteudo")
	if err != nil {
		fmt.Println("Erro ao remover 'diretorio_com_conteudo':", err)
	} else {
		fmt.Println("Diretório 'diretorio_com_conteudo' e seu conteúdo removidos.")
	}
}

11. Tratamento de Erros

Em Go, o tratamento de erros é explícito. Quase todas as funções de manipulação de arquivos retornam um error como segundo valor. É crucial verificar esses erros para garantir a robustez da sua aplicação.

package main

import (
	"fmt"
	"os"
)

func main() {
	// Exemplo de tratamento de erro ao abrir um arquivo que não existe
	_, err := os.Open("arquivo_inexistente.txt")
	if err != nil {
		if os.IsNotExist(err) {
			fmt.Println("Erro: O arquivo não existe.")
		} else {
			fmt.Println("Erro inesperado ao abrir o arquivo:", err)
		}
	}

	// Exemplo de tratamento de erro ao tentar escrever em um arquivo somente leitura
	// Primeiro, crie um arquivo e defina-o como somente leitura
	os.WriteFile("somente_leitura.txt", []byte(""), 0444) // 0444 = somente leitura para todos
	file, err := os.OpenFile("somente_leitura.txt", os.O_WRONLY, 0644)
	if err != nil {
		// No Windows, pode ser "Access is denied."
		// No Linux, pode ser "permission denied"
		fmt.Println("Erro ao tentar abrir para escrita um arquivo somente leitura:", err)
	} else {
		defer file.Close()
		_, err = file.WriteString("Tentando escrever.")
		if err != nil {
			fmt.Println("Erro ao escrever em arquivo somente leitura:", err)
		}
	}
	os.Remove("somente_leitura.txt") // Limpeza
}

Funções úteis para tratamento de erros do os:

  • os.IsExist(err): Retorna true se o erro indica que um arquivo ou diretório já existe.
  • os.IsNotExist(err): Retorna true se o erro indica que um arquivo ou diretório não existe.
  • os.IsPermission(err): Retorna true se o erro indica uma permissão negada.

Conclusão

A manipulação de arquivos em Go é poderosa e flexível, principalmente através do pacote os. Compreender as diferentes funções e como lidar com erros é essencial para construir aplicações robustas que interagem com o sistema de arquivos. Lembre-se sempre de fechar os arquivos abertos usando defer file.Close() para evitar vazamento de recursos.