Nesta aula, vamos explorar como trabalhar com arquivos em Go utilizando o pacote os. O pacote os fornece uma interface independente de plataforma para funcionalidades do sistema operacional, incluindo operações de arquivo. Dominar a leitura e escrita de arquivos é essencial para muitas aplicações, como processamento de dados, logs e configurações.

Além das operações básicas, veremos como definir permissões ao criar arquivos e como tratar erros de forma robusta. Ao final, você será capaz de manipular arquivos com confiança em seus programas Go.

Abrindo e lendo

Para abrir um arquivo em Go, usamos a função os.Open() que retorna um ponteiro para os.File e um erro. O arquivo é aberto no modo somente leitura. É importante sempre fechar o arquivo após o uso, geralmente com defer file.Close().

Existem várias formas de ler o conteúdo de um arquivo. Podemos ler todo o conteúdo de uma vez com os.ReadFile() (disponível desde Go 1.16), ou ler em partes usando um buffer e o método Read() do arquivo. Outra opção é usar um scanner (bufio.Scanner) para ler linha por linha.

Exemplo de leitura completa com os.ReadFile():

package main

import (
    "fmt"
    "os"
)

func main() {
    data, err := os.ReadFile("exemplo.txt")
    if err != nil {
        fmt.Println("Erro ao ler arquivo:", err)
        return
    }
    fmt.Println(string(data))
}

Exemplo de leitura linha por linha com bufio.Scanner:

package main

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

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

    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }
    if err := scanner.Err(); err != nil {
        fmt.Println("Erro ao ler arquivo:", err)
    }
}

Escrevendo

Para escrever em um arquivo, podemos usar os.WriteFile() para escrever todo o conteúdo de uma vez, ou abrir o arquivo com os.Create() (que cria ou trunca o arquivo) e usar métodos como Write() ou WriteString(). Para adicionar conteúdo ao final de um arquivo existente, use os.OpenFile() com a flag os.O_APPEND.

Exemplo de escrita simples com os.WriteFile():

package main

import (
    "fmt"
    "os"
)

func main() {
    conteudo := []byte("Olá, mundo!\n")
    err := os.WriteFile("saida.txt", conteudo, 0644)
    if err != nil {
        fmt.Println("Erro ao escrever arquivo:", err)
        return
    }
    fmt.Println("Arquivo escrito com sucesso")
}

Exemplo de escrita com os.Create() e WriteString():

package main

import (
    "fmt"
    "os"
)

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

    _, err = file.WriteString("Escrevendo em Go\n")
    if err != nil {
        fmt.Println("Erro ao escrever:", err)
        return
    }
    fmt.Println("Escrita bem-sucedida")
}

Para adicionar ao final de um arquivo existente:

package main

import (
    "fmt"
    "os"
)

func main() {
    file, err := os.OpenFile("saida.txt", os.O_APPEND|os.O_WRONLY, 0644)
    if err != nil {
        fmt.Println("Erro ao abrir arquivo:", err)
        return
    }
    defer file.Close()

    _, err = file.WriteString("Adicionando mais texto\n")
    if err != nil {
        fmt.Println("Erro ao escrever:", err)
        return
    }
}

Permissões

Ao criar um arquivo com os.Create() ou os.OpenFile(), você pode especificar permissões no terceiro argumento. As permissões são representadas em octal, como 0644 (leitura/escrita para o dono, leitura para grupo e outros). O pacote os também fornece constantes como os.ModePerm (0777) e funções como os.Chmod() para alterar permissões após a criação.

Exemplo de criação com permissões específicas:

package main

import (
    "fmt"
    "os"
)

func main() {
    file, err := os.OpenFile("privado.txt", os.O_CREATE|os.O_WRONLY, 0600)
    if err != nil {
        fmt.Println("Erro:", err)
        return
    }
    defer file.Close()
    file.WriteString("Conteúdo privado")
}

Alterando permissões de um arquivo existente:

package main

import (
    "fmt"
    "os"
)

func main() {
    err := os.Chmod("privado.txt", 0644)
    if err != nil {
        fmt.Println("Erro ao alterar permissões:", err)
        return
    }
    fmt.Println("Permissões alteradas")
}

É importante lembrar que as permissões são aplicadas de acordo com a máscara de criação de arquivos (umask) do sistema. Em sistemas Unix, a permissão efetiva é a permissão solicitada menos a umask. Em Windows, as permissões são mapeadas de forma diferente.

Tratamento de erros

Todas as operações de arquivo em Go retornam um erro que deve ser verificado. Ignorar erros pode levar a comportamentos inesperados ou falhas silenciosas. É uma boa prática tratar os erros imediatamente, geralmente com uma estrutura if err != nil.

Além disso, podemos usar a função os.IsNotExist() para verificar se um erro específico indica que o arquivo não existe, e os.IsPermission() para verificar se é um erro de permissão.

Exemplo de tratamento robusto:

package main

import (
    "fmt"
    "os"
)

func main() {
    data, err := os.ReadFile("inexistente.txt")
    if err != nil {
        if os.IsNotExist(err) {
            fmt.Println("Arquivo não encontrado")
        } else if os.IsPermission(err) {
            fmt.Println("Permissão negada")
        } else {
            fmt.Println("Erro desconhecido:", err)
        }
        return
    }
    fmt.Println(string(data))
}

Lembre-se de fechar arquivos com defer para garantir que o recurso seja liberado, mesmo em caso de erro. O defer é executado quando a função retorna, independentemente de como ela retorna.

Boas práticas

  • Sempre feche arquivos usando defer file.Close() imediatamente após abri-los.
  • Prefira os.ReadFile() e os.WriteFile() para operações simples, pois eles lidam com abertura e fechamento automaticamente.
  • Use bufio.Scanner para ler arquivos grandes linha por linha, evitando carregar tudo na memória.
  • Verifique erros sempre e trate-os adequadamente, especialmente os.IsNotExist e os.IsPermission.
  • Evite usar caminhos absolutos fixos; prefira caminhos relativos ou variáveis de ambiente.

Referências

Exercícios

  1. Crie um programa que leia o conteúdo de um arquivo chamado "dados.txt" e exiba no terminal. Se o arquivo não existir, exiba uma mensagem de erro amigável.

    ✓ Resposta:
    package main
    
    import (
        "fmt"
        "os"
    )
    
    func main() {
        data, err := os.ReadFile("dados.txt")
        if err != nil {
            if os.IsNotExist(err) {
                fmt.Println("Arquivo dados.txt não encontrado")
            } else {
                fmt.Println("Erro ao ler arquivo:", err)
            }
            return
        }
        fmt.Println(string(data))
    }
  2. Escreva um programa que crie um arquivo "saida.txt" e escreva nele os números de 1 a 10, um por linha.

    ✓ Resposta:
    package main
    
    import (
        "fmt"
        "os"
        "strconv"
    )
    
    func main() {
        file, err := os.Create("saida.txt")
        if err != nil {
            fmt.Println("Erro ao criar arquivo:", err)
            return
        }
        defer file.Close()
    
        for i := 1; i <= 10; i++ {
            _, err := file.WriteString(strconv.Itoa(i) + "\n")
            if err != nil {
                fmt.Println("Erro ao escrever:", err)
                return
            }
        }
        fmt.Println("Números escritos com sucesso")
    }
  3. Modifique o programa anterior para adicionar os números de 11 a 20 ao final do arquivo "saida.txt" (sem sobrescrever).

    ✓ Resposta:
    package main
    
    import (
        "fmt"
        "os"
        "strconv"
    )
    
    func main() {
        file, err := os.OpenFile("saida.txt", os.O_APPEND|os.O_WRONLY, 0644)
        if err != nil {
            fmt.Println("Erro ao abrir arquivo:", err)
            return
        }
        defer file.Close()
    
        for i := 11; i <= 20; i++ {
            _, err := file.WriteString(strconv.Itoa(i) + "\n")
            if err != nil {
                fmt.Println("Erro ao escrever:", err)
                return
            }
        }
        fmt.Println("Números adicionados com sucesso")
    }
  4. Crie um programa que leia um arquivo "config.txt" e exiba suas permissões atuais (em octal). Em seguida, altere as permissões para 0644.

    ✓ Resposta:
    package main
    
    import (
        "fmt"
        "os"
    )
    
    func main() {
        info, err := os.Stat("config.txt")
        if err != nil {
            fmt.Println("Erro ao obter informações:", err)
            return
        }
        fmt.Printf("Permissões atuais: %o\n", info.Mode().Perm())
    
        err = os.Chmod("config.txt", 0644)
        if err != nil {
            fmt.Println("Erro ao alterar permissões:", err)
            return
        }
        fmt.Println("Permissões alteradas para 0644")
    }
  5. Escreva um programa que tente ler um arquivo "restrito.txt" e trate especificamente os erros de arquivo não encontrado e permissão negada.

    ✓ Resposta:
    package main
    
    import (
        "fmt"
        "os"
    )
    
    func main() {
        _, err := os.ReadFile("restrito.txt")
        if err != nil {
            if os.IsNotExist(err) {
                fmt.Println("Arquivo restrito.txt não existe")
            } else if os.IsPermission(err) {
                fmt.Println("Permissão negada para ler restrito.txt")
            } else {
                fmt.Println("Erro desconhecido:", err)
            }
            return
        }
        fmt.Println("Arquivo lido com sucesso")
    }