Trabalhando com arquivos
Em Go, podemos criar, ler, escrever e remover arquivos usando principalmente o package os, que oferece funções para interagir com o sistema operacional.
Quando declaramos uma variável em Go, o valor dela normalmente fica disponível apenas durante a execução do programa, ou seja, quando o programa encerra, esses dados deixam de existir. Para manter essas informações, podemos armazenar elas em arquivos, que ficam salvos no sistema de arquivos do sistema operacional.
Por exemplo, podemos criar um arquivo usuarios.txt para guardar nomes de usuários cadastrados:
projeto/
│
├── main.go
└── usuarios.txt
O programa poderá escrever informações nesse arquivo e depois fazer a leitura do conteúdo, mesmo que tenha sido encerrado e executado novamente. Essas operações são conhecidas como entrada e saída de arquivos, porque envolvem a transferência de dados entre o programa e o sistema de arquivos.
O package os oferece funções para essas operações, enquanto o package bufio permite trabalhar com leituras e escritas usando buffers.
Caminhos relativos e absolutos
Antes de trabalhar com arquivos, precisamos entender como o Go identifica onde eles estão armazenados. Um caminho (path) representa a localização de um arquivo ou diretório no sistema operacional.
Podemos trabalhar com caminhos relativos ou absolutos.
Caminho relativo
Um caminho relativo indica a localização do arquivo a partir do diretório de trabalho atual do programa.
Imagine que passamos para o Go apenas o nome do arquivo, como
usuarios.txt. Nesse caso, o Go vai procurar o arquivousuarios.txtno diretório de trabalho atual.
Caminho absoluto
Um caminho absoluto representa a localização completa do arquivo no sistema operacional, exemplo:
No Linux: "/home/usuario/projeto/dados/usuarios.txt"
No Windows: "C:\Users\usuario\projeto\dados\usuarios.txt"
Os caminhos absolutos não dependem do diretório de trabalho atual, mas podem variar entre computadores e sistemas operacionais. Por isso, em muitos projetos, é melhor trabalhar com caminhos relativos ou receber o caminho por uma configuração.
Criando arquivos com os.Create
A função os.Create permite criar um arquivo no sistema operacional, se o arquivo ainda não existir, ele será criado, mas, se já existir, seu conteúdo será apagado, deixando o arquivo vazio.
package main
import (
"fmt"
"os"
)
func main() {
// Cria o arquivo usuarios.txt.
arquivo, err := os.Create("usuarios.txt")
if err != nil {
fmt.Println("Erro ao criar arquivo:", err)
return
}
// Fecha o arquivo quando a função terminar.
defer arquivo.Close()
fmt.Println("Arquivo criado com sucesso!")
}
Depois da execução, teremos:
projeto/
│
├── main.go
└── usuarios.txt
Mas isso depende de onde o programa será executado, principalmente quando compilarmos o código em um binário, que poderá estar em outro diretório.
Observe que os.Create retorna dois valores.
arquivo, err := os.Create("usuarios.txt")
O primeiro representa o arquivo aberto, permitindo realizar operações de leitura ou escrita conforme o modo de abertura. O segundo representa um possível erro.
Se o sistema operacional não permitir a criação do arquivo, por exemplo, por falta de permissão, o Go retornará um erro.
Precisamos fechar o arquivo?
Quando abrimos um arquivo, o sistema operacional disponibiliza um recurso para que o programa consiga trabalhar com ele, esse recurso precisa ser liberado quando não for mais necessário.
Para isso, usamos o método Close().
arquivo.Close()
Também podemos usar defer.
defer arquivo.Close()
O defer faz com que a chamada seja executada quando a função atual estiver terminando. Isso ajuda a garantir que o arquivo seja fechado mesmo quando a função possui vários caminhos de retorno.
O fechamento também pode retornar um erro, quando precisamos garantir que todos os dados foram gravados corretamente, devemos verificar esse erro.
Escrevendo em arquivos com os.WriteFile
Se quisermos apenas criar um arquivo e escrever algum conteúdo nele, podemos usar os.WriteFile. Essa função recebe o caminho do arquivo, os dados que serão escritos e as permissões usadas caso o arquivo precise ser criado.
package main
import (
"fmt"
"os"
)
func main() {
// Texto que será escrito no arquivo.
conteudo := "Ana\nCarlos\nMaria\n"
// Cria ou sobrescreve o arquivo.
err := os.WriteFile("usuarios.txt", []byte(conteudo), 0644)
if err != nil {
fmt.Println("Erro ao escrever arquivo:", err)
return
}
fmt.Println("Arquivo salvo com sucesso!")
}
Depois da execução, o arquivo usuarios.txt terá este conteúdo:
Ana
Carlos
Maria
A parte []byte(conteudo), faz com que o os.WriteFile receba os dados como uma sequência de bytes. Um byte é uma unidade de informação usada para representar dados na memória e em arquivos. Como nosso conteúdo está em uma string, fazemos a conversão para []byte.
Depois, ainda aplicamos a permissão do arquivo:
0644
Em sistemas como Linux, 0644 normalmente significa que o dono pode ler e escrever, enquanto os demais usuários podem apenas ler. A permissão final também depende das regras do sistema, como a umask.
Se o arquivo já existir, os.WriteFile sobrescreve seu conteúdo e não altera suas permissões existentes. Diferentemente de os.Create, não precisamos chamar Close() nesse exemplo, porque os.WriteFile abre, escreve e fecha o arquivo internamente.
Adicionando conteúdo com os.OpenFile
O os.WriteFile é muito útil quando queremos substituir todo o conteúdo de um arquivo, porém, às vezes precisamos adicionar informações sem apagar os dados existentes.
Para isso, podemos usar os.OpenFile:
package main
import (
"fmt"
"os"
)
func main() {
// Abre o arquivo para escrita no final.
arquivo, err := os.OpenFile(
"usuarios.txt",
os.O_APPEND|os.O_CREATE|os.O_WRONLY,
0644,
)
if err != nil {
fmt.Println("Erro ao abrir arquivo:", err)
return
}
defer arquivo.Close()
// Adiciona uma nova linha ao arquivo.
_, err = arquivo.WriteString("Pedro\n")
if err != nil {
fmt.Println("Erro ao escrever:", err)
return
}
fmt.Println("Usuário adicionado!")
}
Se o arquivo já tiver:
Ana
Carlos
Maria
Depois da execução, teremos:
Ana
Carlos
Maria
Pedro
A diferença está nas opções passadas para os.OpenFile: os.O_APPEND | os.O_CREATE | os.O_WRONLY.
O
os.O_APPENDindica que os novos dados serão escritos no final do arquivo.O
os.O_CREATEpermite criar o arquivo caso ele ainda não exista.O
os.O_WRONLYabre o arquivo apenas para escrita.
O operador | combina essas opções em uma única configuração, nesse exemplo, WriteString escreve diretamente uma string no arquivo aberto.
Uma diferença interessante é que arquivo.WriteString() recebe uma string diretamente, enquanto os.WriteFile() precisa receber os dados como []byte. Isso acontece porque WriteString foi criado para receber textos diretamente, enquanto os.WriteFile trabalha com bytes, permitindo escrever tanto textos quanto dados binários.
Internamente, o WriteString também escreve bytes no arquivo, a diferença é que não precisamos fazer a conversão manualmente. Também precisamos lembrar que WriteString é um método de um arquivo já aberto, enquanto os.WriteFile abre, escreve e fecha o arquivo automaticamente.
Lendo arquivos com os.ReadFile
Para ler o conteúdo de um arquivo, podemos usar os.ReadFile. Essa função recebe o caminho do arquivo e retorna seu conteúdo como []byte, além de um possível erro. Por exemplo:
package main
import (
"fmt"
"os"
)
func main() {
// Lê todo o conteúdo do arquivo.
dados, err := os.ReadFile("usuarios.txt")
if err != nil {
fmt.Println("Erro ao ler arquivo:", err)
return
}
// Converte os bytes para string.
conteudo := string(dados)
fmt.Print(conteudo)
}
O Go lê o conteúdo do arquivo e retorna uma sequência de bytes. Depois, convertemos esses bytes para string.
conteudo := string(dados)
Isso permite trabalhar com os dados como texto. O os.ReadFile é uma boa opção para arquivos pequenos, porque lê todo o conteúdo de uma vez e o mantém na memória.
Para arquivos muito grandes, isso pode consumir bastante memória, então podemos fazer a leitura aos poucos.
Lendo arquivos linha por linha
Quando precisamos processar o conteúdo de um arquivo linha por linha, podemos usar bufio.Scanner. Esse recurso já foi mostrado quando estudamos a leitura de dados do terminal. A diferença é que agora vamos criar um scanner para um arquivo, em vez de usar os.Stdin.
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
// Abre o arquivo para leitura.
arquivo, err := os.Open("usuarios.txt")
if err != nil {
fmt.Println("Erro ao abrir arquivo:", err)
return
}
defer arquivo.Close()
// Cria um scanner para ler o arquivo.
scanner := bufio.NewScanner(arquivo)
// Percorre as linhas do arquivo.
for scanner.Scan() {
linha := scanner.Text()
fmt.Println("Usuário:", linha)
}
// Verifica se ocorreu algum erro durante a leitura.
if err := scanner.Err(); err != nil {
fmt.Println("Erro durante a leitura:", err)
}
}
O os.Open abre um arquivo existente para leitura, sem modificar seu conteúdo. Depois, criamos o scanner.
scanner := bufio.NewScanner(arquivo)
O scanner recebe o arquivo como fonte de dados e permite percorrer seu conteúdo linha por linha.
for scanner.Scan() {
linha := scanner.Text()
fmt.Println(linha)
}
A cada chamada de Scan(), o scanner tenta ler a próxima linha. Quando não existem mais linhas, o método retorna false e o laço termina. O Scanner também pode interromper a leitura por causa de um erro, por isso verificamos scanner.Err() depois do laço.
O bufio.Scanner possui um limite padrão para o tamanho de cada token lido. Para arquivos com linhas muito grandes, podemos configurar esse limite ou usar um bufio.Reader.
Por que precisamos tratar erros?
As operações com arquivos dependem do sistema operacional e podem falhar por vários motivos. Por exemplo, o arquivo pode não existir, o programa pode não ter permissão para acessar o arquivo ou o armazenamento pode estar sem espaço disponível.
Por isso, as funções do package os normalmente retornam um valor error, permitindo verificar se a operação foi concluída corretamente.
Em Go, não devemos ignorar esses erros, principalmente quando estamos lendo ou salvando informações importantes.
Verificando se um arquivo existe
Quando tentamos abrir um arquivo que não existe, o Go retorna um erro. Podemos identificar especificamente esse problema com errors.Is e os.ErrNotExist.
package main
import (
"errors"
"fmt"
"os"
)
func main() {
// Tenta ler um arquivo.
dados, err := os.ReadFile("usuarios.txt")
if err != nil {
// Verifica se o arquivo não existe.
if errors.Is(err, os.ErrNotExist) {
fmt.Println("O arquivo não existe.")
return
}
// Trata outros erros de leitura.
fmt.Println("Erro ao ler arquivo:", err)
return
}
fmt.Println(string(dados))
}
Se o arquivo não existir, teremos:
O arquivo não existe.
Nesse exemplo, usamos:
errors.Is(err, os.ErrNotExist)
A função errors.Is verifica se o erro recebido corresponde a um erro específico, mesmo quando ele está encapsulado em outro erro.
Já os.ErrNotExist representa a condição em que um arquivo ou diretório não foi encontrado. Isso permite diferenciar um arquivo inexistente de outros problemas, como falta de permissão.
Tratando erros de permissão
O sistema operacional controla quais programas e usuários podem ler ou modificar determinados arquivos. Se o programa tentar acessar um arquivo sem a permissão necessária, a operação poderá falhar.
Podemos verificar esse caso com os.ErrPermission:
package main
import (
"errors"
"fmt"
"os"
)
func main() {
dados, err := os.ReadFile("usuarios.txt")
if err != nil {
if errors.Is(err, os.ErrNotExist) {
fmt.Println("Arquivo não encontrado.")
return
}
if errors.Is(err, os.ErrPermission) {
fmt.Println("Sem permissão para acessar o arquivo.")
return
}
fmt.Println("Erro desconhecido:", err)
return
}
fmt.Println(string(dados))
}
Assim, conseguimos apresentar mensagens diferentes de acordo com o problema encontrado. Essa abordagem também ajuda durante o desenvolvimento, porque facilita a identificação da causa de uma falha.
Tratando erros ao escrever arquivos
Também precisamos verificar se os dados foram escritos corretamente, exemplo:
package main
import (
"fmt"
"os"
)
func main() {
conteudo := []byte("Ana\nCarlos\nMaria\n")
// Tenta salvar o conteúdo no arquivo.
err := os.WriteFile("usuarios.txt", conteudo, 0644)
if err != nil {
fmt.Println("Não foi possível salvar o arquivo:", err)
return
}
fmt.Println("Arquivo salvo com sucesso!")
}
Nesse exemplo, a mensagem de sucesso só será apresentada se os.WriteFile retornar nil, indicando que não encontrou erro durante a operação.
Mesmo assim, em aplicações que precisam garantir a persistência dos dados após uma falha de energia ou do sistema, podem ser necessárias medidas adicionais, como sincronização com o armazenamento e escrita em arquivo temporário.
Para nossos exemplos iniciais, verificar o erro retornado por os.WriteFile já é suficiente. Também precisamos lembrar que Close() pode retornar um erro, especialmente quando o arquivo foi aberto para escrita.
Quando precisamos verificar esse erro, podemos fechar o arquivo explicitamente depois de terminar a escrita.
package main
import (
"fmt"
"os"
)
func main() {
arquivo, err := os.Create("usuarios.txt")
if err != nil {
fmt.Println("Erro ao criar arquivo:", err)
return
}
// Escreve o conteúdo.
_, err = arquivo.WriteString("Ana\n")
if err != nil {
fmt.Println("Erro ao escrever:", err)
// Fecha o arquivo antes de retornar.
_ = arquivo.Close()
return
}
// Verifica se ocorreu erro ao fechar.
err = arquivo.Close()
if err != nil {
fmt.Println("Erro ao fechar arquivo:", err)
return
}
fmt.Println("Arquivo salvo com sucesso!")
}
Nesse exemplo, verificamos tanto a escrita quanto o fechamento do arquivo. Isso é útil quando precisamos ter mais controle sobre o resultado das operações.