DEV Community

Marcelo Matz
Marcelo Matz

Posted on

Manipulando arquivos: um CRUD de arquivos em Go

Como Criar um CRUD de arquivos em Go

Um guia básico e passo a passo

Uma das coisas que dev mais faz no seu dia a dia é resolver problemas aplicando os fundamentos da programação.

Um destes fundamentos é a capacidade de ler e escrever em arquivos.

Quem trabalha desenvolvendo software, acaba trabalhando com manipulação de arquivos em algum momento.

Neste artigo eu vou mostrar como criar um CRUD (Create, Read, Update, Delete) de arquivo usando a linguagem de programação Go.

Vale ressaltar que neste exemplo eu vou usar os próprios recursos do Go sem a necessidade importar nenhuma biblioteca externa para o meu software.

Caso você queria pesquisar mais a fundo sobre cada biblioteca usada na criação deste exemplo, você pode consultar a documentação da Standard Library do Go.

Se você preferir ir direto para o repositório do código ao invés de ler o artigo, você pode acessar o repositório no GitHub.


Show me the code

O ponto de partida que qualquer software em Go é a partir da função main contida no pacote main. Eu vou escrever todo o código dentro da mesma função main.

Abaixo temos a estrutura base do nosso software:

package main

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

func main() {
    // Agora só falta implementar
    // Duas barras e um texto indica um comentário
}
Enter fullscreen mode Exit fullscreen mode

O CRUD na prática

Create

Eu vou começar implementando o Create do CRUD e para fazer isso em Go eu vou usar o método nativo da linguagem os.Create().

O pacote os do Go permite acessar diversos recursos do sistema operacional, e como nós queremos trabalhar com arquivos, nada mais justo do que usar o pacote que permite isso.

f, err := os.Create("test.txt")
if err != nil {
    panic(err)
}
// Não esqueça de fechar o arquivo quando a criação for concluída.
defer func(f *os.File) {
    err := f.Close()
    if err != nil {
        panic(err)
    }
}(f)
Enter fullscreen mode Exit fullscreen mode

Dentro da função Main nós temos uma variável f que recebe como valor o os.Create().

Note que além da variável f existe uma outra variável chamada err que também foi criada.

Em Go quando nós temos algumaCoisa := "valorDeAlgumaCoisa" significa que nós estamos criando uma variável e ao mesmo tempo estamos atribuindo um valor. O := é o operador Marmota (logo do Go) usado para criação e inferência de tipo.

O processo se resume em:

1) Criar a variável f

(ou file, ou arquivo ou o nome que você quiser) e criar também uma variável para receber o erro err.

O método (função) Create do pacote os do Go funciona da seguinte forma: ele recebe um nome como parâmetro e retorna dois valores: O primeiro retorno é o arquivo em si (no caso um ponteiro para o arquivo, mas não vou entrar no mérito dos ponteiros aqui) e também recebe como retorno um erro.

2) Usar o método defer para garantir que o arquivo vai ser fechado depois que ele for criado.

O defer em Go é utilizado para garantir que uma função seja executada posteriormente.

A instrução defer agenda uma função a ser chamada após a função que a contém ser concluída.

Pode parecer confuso, mas você vai perceber que em outras palavras, a função é "adiada" até que o resto do código seja executado.

Um uso comum para defer é justamente para fechar um arquivo, garantindo que, independentemente do que acontecer no código após a abertura do arquivo, ele (o arquivo) será fechado quando a função for terminada.
Por exemplo:

file, err := os.Open("file.txt")
if err != nil {
    log.Fatal(err)
}

defer file.Close()

// Outras operações no arquivo...
// O arquivo será fechado quando a função terminar
Enter fullscreen mode Exit fullscreen mode

Neste exemplo, file.Close() só será chamado depois que o resto do código na função for executado. Isso é útil porque mesmo se uma operação subsequente no arquivo resultar em um erro e você retornar da função mais cedo do que o esperado, o arquivo ainda será fechado corretamente.

Defer e Closure

Outra maneira de usar o defer neste caso é em conjunto com uma função anônima. Closure é uma função anônima.

Muitas vezes é recomendado usar closures com a declaração defer, principalmente quando estamos lidando com operações que também envolvem erros, como fechamento de arquivos ou conexões em Go.

Um dos principais motivos para combinar defer com uma closure é que a closure pode acessar e manipular as variáveis definidas no escopo onde ela foi criada.

Portanto, ela pode lidar com valores ou estados que possam ter mudado ao longo do tempo antes de o defer ser realmente executado.
Aqui está um exemplo de fechamento de um arquivo usando closure e defer:

f, err := os.Open("file.txt")
if err != nil {
    log.Fatal(err)
}

defer func(f *os.File) {
    if err := f.Close(); err != nil {
        log.Fatal(err) // ou qualquer outra manipulação de erro
    }
}(f)

// Outras operações no arquivo...
Enter fullscreen mode Exit fullscreen mode

Em resumo, após criarmos um arquivo usando os.Create("nomeDoArquivo.extensaoDoArquivo") nós precisamos fechar ele. De acordo com a implementação do método na standard library, na hora que passamos o nome do arquivo como parâmetro para a função Create, caso o arquivo não exista ele vai ser criado e, caso ele já exista, o Go faz um truncate do arquivo.


Read

Ler do Arquivo

A leitura do arquivo é feita em duas etapas. Primeiro, abrimos o arquivo com os.Open() e em seguida lemos o arquivo utilizando bufio.NewReader().

f, err := os.Open("test.txt")
if err != nil {
    panic(err)
}
defer func(f *os.File) {
    err := f.Close()
    if err != nil {
        panic(err)
    }
}(f)

reader := bufio.NewReader(f)
buffer := make([]byte, 10)
for {
    n, err := reader.Read(buffer)
    if err != nil && err != io.EOF {
        panic(err)
    }
    if n == 0 {
        break
    }
    fmt.Print(string(buffer[:n]))
}
Enter fullscreen mode Exit fullscreen mode

Agora mais duas variáveis foram criadas. Uma reader e uma buffer. _Você pode abrir um arquivo sem buffer, mas lembre-se que dependendo do tamanho do arquivo ele pode exceder a memória disponível e acabe gerando um erro.

A variável reader agora armazena o retorno do método bufio.NewReader(f) onde o f nós estamos passando como parâmetro para este método.

O mais legal de trabalhar com buffer é você poder definir o tamanho da fatia que você quer ler de cada vez. No exemplo eu estou usando o método make para criar um slice de byte de tamanho 10.

buffer := make([]byte, 10)

A função make é função built-in do Go que aloca e inicializa um objeto do tipo slice, map ou char. No caso eu estou usando o tipo slice.

Você pode consultar a documentação do Go para saber mais sobre o pacote de tipos em Go.

Continuando com a explicação do exemplo, depois disso eu uso um laço for para fazer algumas coisas:

  • n, err := reader.Read(buffer) Aqui eu estou acessando o método Read da função bufio.NewReader(f) que eu usei para criar o meu reader e salvando na variável n. _A variável de erro eu já expliquei antes como funciona.

Observe no código exemplo como lidamos com o io.EOF (End of File).

if err != nil && err != io.EOF {
        panic(err)
    }
Enter fullscreen mode Exit fullscreen mode

É importante lembrar de fazer isso porque a leitura pode retornar io.EOF a qualquer momento, por isso já tratamos isso no for e é assim que nós sabemos quando terminamos de ler o arquivo.

Sobre o io.EOF (End Of File): ele é um erro que é comum em operações de arquivo e é usado para indicar que o fim de um arquivo foi atingido.

Como uma convenção em programação, sempre que uma função está lendo de um arquivo e atinge o fim do arquivo, essa função retorna io.EOF.

Isso é útil para que o código que chamou a função de leitura possa saber quando parar de ler.

O nosso exemplo é bastante simples mas ele mostra como usamos o io.Reader (uma interface que abstrai a leitura de dados de uma fonte).

Nós usamos um loop (for) para tentar ler 10 bytes do reader de cada vez em nosso buffer.

Se o reader.Read() encontrar um erro que não seja io.EOF, o programa entra em pânico.

Uma coisa que eu sempre gostei em Go foi poder definir que ele vai entrar em pânico caso algum erro aconteça porque literalmente a função que trata isso se chama panic 😂

Se nenhum byte for lido (n == 0), isso significa que atingimos io.EOF e o arquivo não tem mais nada a ser lido, então saímos do loop.

Caso contrário, processamos os bytes que foram lidos com sucesso.

Então, resumindo, io.EOF é usado em Go (e muitas outras linguagens) para sinalizar que a leitura de um arquivo ou outra fonte de entrada chegou ao fim. Ele é útil para controlar loops e outras estruturas que continuam a ler até que não haja mais nada para ler.


Update

Escrever no Arquivo

Depois de criar o arquivo, podemos escrever nele. Para fazer isso, utilizamos a função f.Write(), que aceita uma fatia de bytes como argumento.

size, err := f.Write([]byte("Criando conteúdo do arquivo"))
if err != nil {
    panic(err)
}
Enter fullscreen mode Exit fullscreen mode

Assegure-se de sincronizar os dados no disco usando f.Sync() para garantir que todos os dados pendentes sejam efetivamente escritos no arquivo.

err = f.Sync()
if err != nil {
    panic(err)
}
Enter fullscreen mode Exit fullscreen mode

Escrever em um arquivo é algo bem simples. No exemplo eu usei uma variável chamada size para armazenar o retorno da função Write e uma variável de erro, como já de costume.

Abaixo eu mostro como imprimir no console essa informação usando o pacote fmt e a função Printf para formatar a saída de texto usando o %s(de string) que vai ser substituído pelo conteúdo do arquivo, convertido em string.

Além disso eu também mostro o tamanho do arquivo em bytes.

fmt.Printf("Tamanho: %d bytes\n", size)
    fmt.Printf("Conteúdo do arquivo: %s\n", string(arquivo))
Enter fullscreen mode Exit fullscreen mode

Existe um outro método chamado writeString onde eu não precisaria passar um slice de bytes, porém, entretanto, contudo, eu teria que garantir que em 100% das vezes o que tipo de dado que vai trafegar ali é do tipo string. Se eu tenho a opção de trabalhar com uma cadeia de bytes, sejam eles em algum momento um string ou não, eu vou optar por isso.

De qualquer forma, escrever dados em um arquivo é algo que se torna muito fácil quando aproveitamos os recursos da linguagem.


Delete

Apagando um arquivo

Apagar um arquivo usando Go é extremamente simples. Para deletar um arquivo, usamos a função os.Remove().

err = os.Remove("test.txt")
if err != nil {
    panic(err)
}
fmt.Println("\nArquivo deletado com sucesso!")
Enter fullscreen mode Exit fullscreen mode

Deployando

Neste tutorial eu te mostrei como, fundamentalmente, fazer operações CRUD em arquivos no Go.

Abaixo eu deixo um exemplo do código completo. No início do post tem o link para o repositório no GitHub.

package main

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

func main() {
    // Criar um novo arquivo
    f, err := os.Create("test.txt")
    if err != nil {
        panic(err)
    }
    defer func(f *os.File) {
        err := f.Close()
        if err != nil {
            panic(err)
        }
    }(f)

    // Escrevendo no arquivo criado
    size, err := f.Write([]byte("Criando conteúdo do arquivo"))
    if err != nil {
        panic(err)
    }

    // Sincroniza os dados no disco
    err = f.Sync()
    if err != nil {
        panic(err)
    }

    arquivo, err := os.ReadFile("test.txt")
    if err != nil {
        panic(err)
    }

    fmt.Println("Arquivo criado com sucesso!")
    fmt.Printf("Tamanho: %d bytes\n", size)
    fmt.Printf("Conteúdo do arquivo: %s\n", string(arquivo))

    // Abrindo um arquivo existente
    f2, err := os.Open("test.txt")
    if err != nil {
        panic(err)
    }
    defer func(arquivo2 *os.File) {
        err := arquivo2.Close()
        if err != nil {
            panic(err)
        }
    }(f2)

    // Lendo arquivo usando bufio
    reader := bufio.NewReader(f2)
    buffer := make([]byte, 10)
    for {
        n, err := reader.Read(buffer)
        if err != nil && err != io.EOF {
            panic(err)
        }
        if n == 0 {
            break
        }
        fmt.Print(string(buffer[:n]))
    }

    // Deletando um arquivo
    /*
        err = os.Remove("test.txt")
        if err != nil {
            panic(err)
        }
        fmt.Println("\nArquivo deletado com sucesso!")
    */
}

Enter fullscreen mode Exit fullscreen mode

Assim como em qualquer linguagem de programação, existem muitas nuances que nós podemos explorar mas eu considero este um ótimo ponto de partida.

Espero que este guia tenha sido útil e encorajo a todos a continuar explorando e aprendendo Go!

Top comments (0)