Embutindo arquivos (embed)
LidoNesta aula, você aprenderá a embutir arquivos estáticos no binário Go usando a diretiva //go:embed, explorando como servir assets em aplicações web, casos de uso práticos e as limitações dessa funcionalidade. O conteúdo inclui exemplos de código, exercícios com respostas e referências para aprofundamento.
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 []bytePara embutir uma árvore de arquivos, usamos o tipo embed.FS:
import "embed"
//go:embed static/*
var staticFiles embed.FSIsso 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 prefixoall:. - Não é possível embutir arquivos de diretórios fora do módulo (por exemplo, usando caminhos absolutos ou
../). - Variáveis
stringe[]bytesó 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.FSpara múltiplos arquivos estring/[]bytepara 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
embedmesmo 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
- Documentação oficial do pacote embed
- Anúncio do Go 1.16 com embed
- Especificação da linguagem sobre embed
- Documentação de http.FileServer
- Documentação de template.ParseFS
- Documentação sobre módulos Go
Exercícios
- Crie um programa que embuta um arquivo de texto chamado
dados.txtusando//go:embede imprima seu conteúdo no console.✓ Resposta:package main import ( _ "embed" "fmt" ) //go:embed dados.txt var dados string func main() { fmt.Println(dados) } - Escreva um servidor HTTP que sirva arquivos estáticos embutidos de uma pasta
assetsno 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) } - Explique por que a diretiva
//go:embednão pode ser usada com caminhos que contenham...✓ Resposta: A diretiva//go:embedfoi 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. - 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 prefixoall:no padrão. Por exemplo://go:embed all:assetsincluirá todos os arquivos dentro deassets, incluindo os ocultos. Sem o prefixo, esses arquivos são ignorados. - Cite duas limitações do
//go:embede 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.