Manipular arquivos é uma tarefa fundamental em muitas aplicações, seja para persistir dados, ler configurações ou gerar relatórios. Em Go, o pacote os fornece funções de baixo nível para operações de arquivo, enquanto o pacote io e bufio oferecem abstrações adicionais. Nesta aula, focaremos nas funções essenciais do pacote os para abrir, ler, escrever e definir permissões em arquivos, sempre com atenção ao tratamento de erros, que é uma prática obrigatória em Go.

O pacote os segue o padrão de retornar um valor de erro como último valor de retorno, permitindo que o programador trate explicitamente qualquer falha. Veremos desde a abertura de um arquivo existente até a criação de novos arquivos com permissões específicas, além de operações de leitura e escrita em blocos de bytes.

Abrindo e lendo

Para abrir um arquivo existente, usamos os.Open, que retorna um ponteiro para os.File e um erro. O arquivo é aberto no modo somente leitura. Para leitura, podemos usar métodos como Read (lê um slice de bytes) ou ReadAll do pacote io para ler todo o conteúdo. É importante fechar o arquivo com defer file.Close() para liberar recursos.

Exemplo: abrir um arquivo, ler seu conteúdo e exibir no console.

package main

import (
    "fmt"
    "os"
)

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

    // Lê o arquivo em um slice de bytes
    data := make([]byte, 100)
    count, err := file.Read(data)
    if err != nil {
        fmt.Println("Erro ao ler arquivo:", err)
        return
    }
    fmt.Printf("Lidos %d bytes: %s\n", count, data[:count])
}

Para ler todo o arquivo de uma vez, podemos usar io.ReadAll ou os.ReadFile (disponível a partir do Go 1.16). os.ReadFile é mais simples, pois abre, lê e fecha o arquivo automaticamente.

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))
}

Escrevendo

Para escrever em um arquivo, podemos usar os.Create (cria ou trunca um arquivo existente) ou os.OpenFile com flags para controle mais fino. O método Write escreve um slice de bytes, e WriteString escreve uma string. Assim como na leitura, é fundamental fechar o arquivo após a escrita.

Exemplo: criar um arquivo e escrever uma string.

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()

    bytesWritten, err := file.WriteString("Olá, mundo!\n")
    if err != nil {
        fmt.Println("Erro ao escrever:", err)
        return
    }
    fmt.Printf("Escritos %d bytes\n", bytesWritten)
}

Também podemos usar os.WriteFile para escrever diretamente um slice de bytes em um arquivo, similar ao ReadFile.

package main

import (
    "fmt"
    "os"
)

func main() {
    data := []byte("Conteúdo escrito com WriteFile\n")
    err := os.WriteFile("saida2.txt", data, 0644)
    if err != nil {
        fmt.Println("Erro:", err)
        return
    }
    fmt.Println("Arquivo escrito com sucesso")
}

Note que os.WriteFile aceita um terceiro argumento para as permissões do arquivo (veremos a seguir).

Permissões

As permissões em sistemas Unix são representadas por um valor octal de 9 bits (rwx para dono, grupo e outros). Em Go, o tipo os.FileMode encapsula essas permissões. Constantes como 0644 (dono pode ler/escrever, grupo e outros só leitura) são comuns. Ao criar um arquivo com os.Create, as permissões padrão são 0666 (antes da máscara umask). Para definir permissões específicas, use os.OpenFile com a flag os.O_CREATE e o modo desejado.

Exemplo: criar um arquivo com permissões restritas (apenas dono pode ler/escrever).

package main

import (
    "fmt"
    "os"
)

func main() {
    // Abre ou cria o arquivo com permissões 0600
    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")
}

Para alterar permissões de um arquivo existente, use os.Chmod.

package main

import (
    "fmt"
    "os"
)

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

Tratamento de erros

Em Go, o tratamento de erros é explícito. Quase todas as funções do pacote os retornam um erro como último valor. Você deve verificar esse erro imediatamente. Ignorar erros pode levar a comportamentos inesperados ou panics. Use a declaração if err != nil para tratar o erro adequadamente, seja exibindo uma mensagem, retornando ou tomando uma ação corretiva.

Exemplo: tratamento de erro ao abrir um arquivo que não existe.

package main

import (
    "fmt"
    "os"
)

func main() {
    _, err := os.Open("inexistente.txt")
    if err != nil {
        // Verifica se o erro é do tipo *os.PathError
        if os.IsNotExist(err) {
            fmt.Println("Arquivo não existe")
        } else {
            fmt.Println("Erro desconhecido:", err)
        }
        return
    }
    fmt.Println("Arquivo aberto com sucesso")
}

Funções como os.IsNotExist e os.IsPermission ajudam a identificar a natureza do erro. Sempre feche o arquivo com defer para garantir a liberação de recursos, mesmo que ocorra um erro após a abertura.

Boas práticas e observações finais

Sempre use defer file.Close() logo após abrir o arquivo, para garantir que ele será fechado ao final da função. Prefira os.ReadFile e os.WriteFile para operações simples, pois elas gerenciam a abertura e fechamento automaticamente. Para arquivos grandes, use leitura/escrita em partes com bufio para melhor desempenho. Lembre-se de que as permissões são aplicadas apenas na criação; para arquivos existentes, use os.Chmod. Trate todos os erros de forma consistente, especialmente ao trabalhar com arquivos, onde falhas podem ocorrer por diversos motivos (disco cheio, permissões, etc.).

Referências

Exercícios

  1. Escreva um programa que leia o conteúdo de um arquivo chamado "dados.txt" e imprima na tela o número de linhas do arquivo. Considere que cada linha termina com '\n'.

    ✓ Resposta:
    package main
    
    import (
        "fmt"
        "os"
        "strings"
    )
    
    func main() {
        data, err := os.ReadFile("dados.txt")
        if err != nil {
            fmt.Println("Erro:", err)
            return
        }
        lines := strings.Split(string(data), "\n")
        fmt.Println("Número de linhas:", len(lines)-1) // última linha vazia
    }
    
  2. Crie um programa que escreva os números de 1 a 10, um por linha, em um arquivo chamado "numeros.txt".

    ✓ Resposta:
    package main
    
    import (
        "fmt"
        "os"
    )
    
    func main() {
        file, err := os.Create("numeros.txt")
        if err != nil {
            fmt.Println("Erro:", err)
            return
        }
        defer file.Close()
    
        for i := 1; i <= 10; i++ {
            _, err := file.WriteString(fmt.Sprintf("%d\n", i))
            if err != nil {
                fmt.Println("Erro ao escrever:", err)
                return
            }
        }
        fmt.Println("Arquivo criado com sucesso")
    }
    
  3. Implemente uma função que copie um arquivo de origem para um destino. Use os pacotes os e io.

    ✓ Resposta:
    package main
    
    import (
        "io"
        "os"
    )
    
    func copyFile(src, dst string) error {
        sourceFile, err := os.Open(src)
        if err != nil {
            return err
        }
        defer sourceFile.Close()
    
        destFile, err := os.Create(dst)
        if err != nil {
            return err
        }
        defer destFile.Close()
    
        _, err = io.Copy(destFile, sourceFile)
        if err != nil {
            return err
        }
        return destFile.Sync() // garante que os dados foram gravados
    }
    
    func main() {
        err := copyFile("origem.txt", "destino.txt")
        if err != nil {
            panic(err)
        }
    }
    
  4. Escreva um programa que leia um arquivo "config.json" e, se ele não existir, crie um com conteúdo padrão {"nome": "default"}. Use permissões 0644.

    ✓ Resposta:
    package main
    
    import (
        "fmt"
        "os"
    )
    
    func main() {
        _, err := os.Stat("config.json")
        if os.IsNotExist(err) {
            data := []byte(`{"nome": "default"}`)
            err := os.WriteFile("config.json", data, 0644)
            if err != nil {
                fmt.Println("Erro ao criar config.json:", err)
                return
            }
            fmt.Println("Arquivo config.json criado")
        } else if err != nil {
            fmt.Println("Erro ao verificar arquivo:", err)
        } else {
            data, err := os.ReadFile("config.json")
            if err != nil {
                fmt.Println("Erro ao ler config.json:", err)
                return
            }
            fmt.Println("Conteúdo:", string(data))
        }
    }
    
  5. Modifique o programa do exercício 1 para que, em vez de ler o arquivo inteiro, leia linha por linha usando um buffer (por exemplo, com bufio.Scanner).

    ✓ Resposta:
    package main
    
    import (
        "bufio"
        "fmt"
        "os"
    )
    
    func main() {
        file, err := os.Open("dados.txt")
        if err != nil {
            fmt.Println("Erro:", err)
            return
        }
        defer file.Close()
    
        scanner := bufio.NewScanner(file)
        lineCount := 0
        for scanner.Scan() {
            lineCount++
        }
        if err := scanner.Err(); err != nil {
            fmt.Println("Erro ao ler:", err)
            return
        }
        fmt.Println("Número de linhas:", lineCount)
    }