Em Go, desde a versão 1.16, a diretiva //go:embed permite incorporar arquivos e pastas diretamente no binário compilado. Isso é extremamente útil para distribuir aplicações como um único executável, sem a necessidade de arquivos externos. Nesta aula, vamos explorar como usar //go:embed para embutir arquivos, como servir esses assets em aplicações web, casos de uso comuns e as limitações que você deve conhecer.

A diretiva //go:embed é processada pelo compilador durante a compilação, e os arquivos são incluídos no binário como dados imutáveis. Isso significa que, uma vez compilado, o executável contém todos os arquivos embutidos, tornando a distribuição mais simples e segura. Vamos começar entendendo a sintaxe básica.

//go:embed

A diretiva //go:embed é colocada imediatamente acima de uma variável do tipo string, []byte ou embed.FS. Ela aceita um ou mais padrões de caminho relativos ao diretório do arquivo fonte. O padrão pode ser um nome de arquivo específico, um diretório com all: prefixo, ou curingas como * e **.

Vejamos um exemplo simples: suponha que temos um arquivo hello.txt no mesmo diretório do nosso código. Para embuti-lo como string, usamos:

package main

import (
    _ "embed"
    "fmt"
)

//go:embed hello.txt
var hello string

func main() {
    fmt.Println(hello)
}

Note que importamos o pacote embed de forma implícita (com underscore) apenas para ativar a funcionalidade. A variável hello receberá o conteúdo do arquivo como string. Se o arquivo for binário, podemos usar []byte:

//go:embed image.png
var imageData []byte

Para embutir uma árvore de arquivos, usamos o tipo embed.FS:

import "embed"

//go:embed static/*
var staticFiles embed.FS

Isso criará um sistema de arquivos virtual contendo todos os arquivos dentro da pasta static. O padrão static/* inclui apenas arquivos diretamente em static, mas não subdiretórios. Para incluir recursivamente, use all:static (ou static/all: dependendo da versão). O prefixo all: é usado para incluir arquivos que começam com _ ou ., que são ignorados por padrão.

Servindo assets

Uma das aplicações mais comuns de //go:embed é servir arquivos estáticos em uma aplicação web. Com embed.FS, podemos usar o pacote net/http para servir esses arquivos diretamente. O tipo embed.FS implementa a interface fs.FS, que é aceita por http.FileServer e http.FileServerFS (a partir de Go 1.22).

Exemplo básico de servidor que serve arquivos estáticos embutidos:

package main

import (
    "embed"
    "net/http"
)

//go:embed static/*
var staticFiles embed.FS

func main() {
    // Serve o conteúdo de static/ na raiz
    http.Handle("/", http.FileServer(http.FS(staticFiles)))
    http.ListenAndServe(":8080", nil)
}

Com isso, se você tiver static/index.html, ele será servido em /index.html. Para servir na raiz, você pode usar http.FileServerFS e montar em um subpath, ou usar http.StripPrefix para remover o prefixo. Por exemplo, se você quiser servir os arquivos em /assets/:

http.Handle("/assets/", http.StripPrefix("/assets/", http.FileServer(http.FS(staticFiles))))

Além disso, você pode acessar arquivos individuais usando fs.ReadFile ou fs.Open. Isso é útil para templates ou configurações embutidas.

Casos de uso

Os casos de uso para //go:embed são diversos:

  • Distribuição de binários únicos: Ferramentas CLI que precisam de arquivos de configuração ou dados podem embuti-los, facilitando a instalação.
  • Aplicações web: Embutir HTML, CSS, JavaScript e imagens elimina a necessidade de copiar arquivos estáticos ao fazer deploy.
  • Bancos de dados SQL: Embeder arquivos de migração ou esquemas SQL para que estejam sempre disponíveis.
  • Configurações padrão: Incluir arquivos de configuração padrão que podem ser sobrescritos pelo usuário.
  • Jogos e aplicações gráficas: Embutir texturas, sons e outros assets.

Vejamos um exemplo mais completo de uma aplicação web que usa templates embutidos:

package main

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

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

var tmpl = template.Must(template.ParseFS(templatesFS, "templates/*.html"))

func handler(w http.ResponseWriter, r *http.Request) {
    tmpl.ExecuteTemplate(w, "index.html", nil)
}

func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServe(":8080", nil)
}

Nesse caso, os templates são carregados na inicialização e podem ser usados normalmente. Isso é muito útil para aplicações que precisam ser autocontidas.

Limitações

Apesar das vantagens, //go:embed tem algumas limitações importantes:

  • Os arquivos embutidos são somente leitura. Não é possível modificá-los em tempo de execução.
  • O padrão de caminho não pode conter .. (subir diretórios) e deve ser relativo ao pacote.
  • Arquivos que começam com _ ou . são ignorados por padrão, a menos que você use o prefixo all:.
  • Não é possível embutir arquivos de diretórios fora do módulo (por exemplo, usando caminhos absolutos ou ../).
  • Variáveis string e []byte só podem embutir um único arquivo, não múltiplos.
  • O tamanho do binário aumenta conforme o tamanho dos arquivos embutidos.

Além disso, ao usar embed.FS, o sistema de arquivos é virtual e não reflete alterações no disco após a compilação. Isso é ótimo para produção, mas pode ser inconveniente em desenvolvimento, pois você precisa recompilar para ver mudanças. Algumas ferramentas de desenvolvimento contornam isso usando um sistema de arquivos que alterna entre embutido e diretório local, mas isso requer configuração adicional.

Boas práticas e observações finais

Para aproveitar ao máximo o //go:embed, considere as seguintes boas práticas:

  • Organize seus arquivos embutidos em pastas claras, como static/, templates/, data/.
  • Use embed.FS para múltiplos arquivos e string/[]byte para arquivos únicos.
  • Para desenvolvimento, você pode criar uma interface que abstrai o acesso aos arquivos, permitindo trocar entre o sistema embutido e o sistema de arquivos local.
  • Evite embutir arquivos muito grandes se o tamanho do binário for uma preocupação.
  • Lembre-se de importar o pacote embed mesmo que não use explicitamente, pois é necessário para a diretiva funcionar.

Com essas ferramentas, você pode criar aplicações Go mais portáteis e fáceis de distribuir.

Referências

Exercícios

  1. Crie um programa que embuta um arquivo de texto chamado dados.txt usando //go:embed e imprima seu conteúdo no console.

    ✓ Resposta:
    package main
    
    import (
        _ "embed"
        "fmt"
    )
    
    //go:embed dados.txt
    var dados string
    
    func main() {
        fmt.Println(dados)
    }
  2. Escreva um servidor HTTP que sirva arquivos estáticos embutidos de uma pasta assets no caminho /static/.

    ✓ Resposta:
    package main
    
    import (
        "embed"
        "net/http"
    )
    
    //go:embed assets/*
    var assets embed.FS
    
    func main() {
        http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(assets))))
        http.ListenAndServe(":8080", nil)
    }
  3. Explique por que a diretiva //go:embed não pode ser usada com caminhos que contenham ...

    ✓ Resposta: A diretiva //go:embed foi projetada para ser segura e determinística. Permitir caminhos com .. poderia permitir acesso a arquivos fora do diretório do pacote, o que quebraria o princípio de encapsulamento e poderia causar problemas de segurança ou inconsistências no build. Portanto, os caminhos devem ser relativos ao diretório do arquivo fonte e não podem subir na hierarquia.
  4. Como você faria para embutir um diretório incluindo arquivos ocultos (que começam com ponto)?

    ✓ Resposta: Para incluir arquivos ocultos ou que começam com _, você deve usar o prefixo all: no padrão. Por exemplo: //go:embed all:assets incluirá todos os arquivos dentro de assets, incluindo os ocultos. Sem o prefixo, esses arquivos são ignorados.
  5. Cite duas limitações do //go:embed e como elas podem impactar o desenvolvimento de uma aplicação.

    ✓ Resposta: Duas limitações são: (1) Os arquivos embutidos são somente leitura, então não é possível alterá-los em tempo de execução; isso pode ser problemático se você precisar de configurações dinâmicas. (2) O binário aumenta de tamanho, o que pode ser um problema para aplicações que precisam ser pequenas ou que embutem muitos arquivos grandes. Além disso, em desenvolvimento, você precisa recompilar para ver mudanças nos arquivos embutidos, o que pode desacelerar o ciclo de desenvolvimento.