Embed directive: incorporando arquivos no binário

1. Introdução à diretiva //go:embed

A partir do Go 1.16, a linguagem introduziu uma funcionalidade nativa e elegante para incorporar arquivos diretamente no binário compilado: a diretiva //go:embed. Antes dessa adição, desenvolvedores precisavam recorrer a ferramentas externas como go-bindata, packr ou statik para resolver o problema de distribuir arquivos estáticos junto com a aplicação.

O principal problema resolvido pelo embed é a eliminação da dependência de caminhos relativos e arquivos externos em tempo de execução. Com essa diretiva, o binário gerado torna-se autossuficiente — não importa onde ele seja executado, os arquivos incorporados estarão sempre disponíveis.

A diretiva suporta três tipos de dados para incorporação:
- string: para arquivos de texto simples
- []byte: para dados binários ou quando mutabilidade é necessária
- embed.FS: um sistema de arquivos virtual completo para múltiplos arquivos e subdiretórios

2. Sintaxe básica e tipos de incorporação

Incorporando como string

Ideal para arquivos de texto como JSON, HTML ou configurações:

package main

import (
    _ "embed"
    "fmt"
)

//go:embed version.txt
var version string

func main() {
    fmt.Println("Versão:", version)
}

Incorporando como []byte

Perfeito para dados binários ou quando você precisa modificar o conteúdo:

package main

import (
    _ "embed"
    "fmt"
)

//go:embed logo.png
var logo []byte

func main() {
    fmt.Printf("Tamanho do logo: %d bytes\n", len(logo))
}

Incorporando como embed.FS

Para múltiplos arquivos e subdiretórios, use embed.FS:

package main

import (
    "embed"
    "fmt"
)

//go:embed templates/*.html
var templateFS embed.FS

func main() {
    data, _ := templateFS.ReadFile("templates/index.html")
    fmt.Println(string(data))
}

3. Padrões de caminho e limitações

Os caminhos na diretiva //go:embed são relativos ao diretório do pacote (não ao arquivo fonte). Isso significa que você deve considerar a estrutura do projeto:

meuprojeto/
├── main.go
└── assets/
    ├── css/
    │   └── style.css
    └── js/
        └── app.js
//go:embed assets/css/*.css assets/js/*.js
var assetsFS embed.FS

Padrões curinga suportados:
- *: corresponde a arquivos em um único diretório
- **: corresponde a arquivos em subdiretórios recursivamente

Restrições importantes:
- Não é possível incorporar arquivos fora do módulo
- O caminho deve ser uma constante literal (não variáveis)
- Não é possível usar caminhos absolutos

4. Trabalhando com embed.FS – sistema de arquivos virtual

O embed.FS implementa a interface fs.FS, tornando-o compatível com pacotes como net/http e text/template.

Abertura e leitura de arquivos

package main

import (
    "embed"
    "fmt"
    "io/fs"
)

//go:embed data
var dataFS embed.FS

func main() {
    // Lendo um arquivo específico
    content, err := dataFS.ReadFile("data/config.json")
    if err != nil {
        panic(err)
    }

    // Abrindo para leitura streaming
    file, err := dataFS.Open("data/readme.txt")
    if err != nil {
        panic(err)
    }
    defer file.Close()

    // Listando diretório
    entries, _ := dataFS.ReadDir("data")
    for _, entry := range entries {
        fmt.Println(entry.Name())
    }
}
//go:embed all:data
var dataFS embed.FS

func listFiles() {
    fs.WalkDir(dataFS, ".", func(path string, d fs.DirEntry, err error) error {
        if err != nil {
            return err
        }
        fmt.Println(path)
        return nil
    })
}

5. Casos de uso práticos

Servindo arquivos estáticos via HTTP

package main

import (
    "embed"
    "net/http"
)

//go:embed static
var staticFS embed.FS

func main() {
    http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticFS))))
    http.ListenAndServe(":8080", nil)
}

Incorporando templates HTML

package main

import (
    "embed"
    "html/template"
    "net/http"
)

//go:embed templates
var templateFS embed.FS

func main() {
    tmpl := template.Must(template.ParseFS(templateFS, "templates/*.html"))

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        tmpl.ExecuteTemplate(w, "index.html", nil)
    })
    http.ListenAndServe(":8080", nil)
}

Empacotando migrações de banco de dados

package main

import (
    "embed"
    "fmt"
    "sort"
)

//go:embed migrations/*.sql
var migrationsFS embed.FS

func runMigrations() error {
    entries, err := migrationsFS.ReadDir("migrations")
    if err != nil {
        return err
    }

    sort.Slice(entries, func(i, j int) bool {
        return entries[i].Name() < entries[j].Name()
    })

    for _, entry := range entries {
        sql, _ := migrationsFS.ReadFile("migrations/" + entry.Name())
        fmt.Printf("Executando migração: %s\n%s\n", entry.Name(), string(sql))
    }
    return nil
}

6. Boas práticas e cuidados

Evitando vazamento de arquivos sensíveis

Nunca incorpore arquivos como .env, chaves privadas ou credenciais. Use um .gitignore adequado e seja explícito sobre o que está sendo incorporado:

// ❌ Ruim: incorpora tudo
//go:embed .

// ✅ Bom: explícito
//go:embed public config/templates

Tratamento de erros

Sempre trate erros ao acessar arquivos no embed.FS:

data, err := myFS.ReadFile("config.json")
if err != nil {
    if errors.Is(err, fs.ErrNotExist) {
        // Arquivo não encontrado
    }
    // Outros erros
}

Diferenças entre desenvolvimento e produção

Em desenvolvimento, você pode usar arquivos do sistema de arquivos real; em produção, os embutidos. Considere usar flags de compilação para alternar:

var tmpl *template.Template

func init() {
    if devMode {
        tmpl = template.Must(template.ParseGlob("templates/*.html"))
    } else {
        tmpl = template.Must(template.ParseFS(templateFS, "templates/*.html"))
    }
}

7. Comparação com alternativas

Característica embed go-bindata packr
Dependências Nenhuma Externa Externa
Suporte nativo Sim Não Não
Compressão Não Sim Sim
Hot-reload Não Não Sim
Manutenção Oficial Descontinuado Ativo

Vantagens do embed:
- Zero dependências externas
- Suporte oficial da linguagem
- Integração com fs.FS e pacotes padrão
- Simplicidade de uso

Limitações:
- Sem compressão automática
- Sem hot-reload em desenvolvimento
- Não suporta arquivos fora do módulo

8. Exemplo completo e testes

Vamos construir um servidor HTTP completo com assets embutidos:

package main

import (
    "embed"
    "html/template"
    "net/http"
    "net/http/httptest"
    "strings"
    "testing"
)

//go:embed static
var staticFS embed.FS

//go:embed templates
var templateFS embed.FS

func main() {
    tmpl := template.Must(template.ParseFS(templateFS, "templates/*.html"))

    mux := http.NewServeMux()
    mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticFS))))
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        tmpl.ExecuteTemplate(w, "index.html", nil)
    })

    http.ListenAndServe(":8080", mux)
}

func TestServer(t *testing.T) {
    tmpl := template.Must(template.ParseFS(templateFS, "templates/*.html"))

    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        tmpl.ExecuteTemplate(w, "index.html", nil)
    })

    server := httptest.NewServer(mux)
    defer server.Close()

    resp, _ := http.Get(server.URL)
    if resp.StatusCode != http.StatusOK {
        t.Errorf("esperado 200, obtido %d", resp.StatusCode)
    }

    // Verifica conteúdo
    body := make([]byte, 1024)
    resp.Body.Read(body)
    if !strings.Contains(string(body), "Hello") {
        t.Error("conteúdo esperado não encontrado")
    }
}

Com embed, seu binário se torna verdadeiramente portátil e autossuficiente, eliminando preocupações com caminhos de arquivos e dependências externas em tempo de execução.

Referências