← Voltar para o blog

Go Embed: Embutindo Arquivos no Binário

Aprenda a usar go:embed para embutir arquivos, templates e assets estáticos no binário Go. Exemplos práticos com embed.FS, net/http e html/template.

Desde o Go 1.16, a diretiva //go:embed permite embutir arquivos e diretórios inteiros dentro do binário compilado. Isso significa que seu deploy se resume a um único arquivo executável — sem se preocupar com caminhos de templates, arquivos de configuração ou assets estáticos que podem estar faltando em produção.

Neste guia, você vai aprender a usar embed.FS, embutir templates HTML, servir assets estáticos via HTTP e combinar tudo em aplicações reais.

Por que Embutir Arquivos no Binário

Em Go, o binário compilado já é autocontido — sem runtime, sem dependências. Mas quando sua aplicação precisa de templates HTML, arquivos SQL de migração, configurações padrão ou assets de frontend, você volta a depender de arquivos externos no filesystem. A diretiva //go:embed resolve isso:

  • Deploy simplificado: um binário, zero arquivos avulsos
  • Reprodutibilidade: o binário sempre inclui a versão correta dos arquivos
  • Imagens Docker menores: sem copiar diretórios extras para a imagem final
  • Segurança: arquivos embutidos não podem ser modificados em produção

Embutindo um Arquivo Simples

O caso mais básico — embutir um único arquivo como string ou []byte:

package main

import (
	_ "embed"
	"fmt"
)

//go:embed version.txt
var version string

//go:embed config-default.json
var defaultConfig []byte

func main() {
	fmt.Println("Versão:", version)
	fmt.Printf("Config padrão: %s\n", defaultConfig)
}

Crie o arquivo version.txt com o conteúdo 1.0.0 e config-default.json com um JSON qualquer. Ao compilar, os arquivos são incorporados ao binário:

go build -o myapp .
rm version.txt config-default.json  # binário já contém os dados
./myapp  # funciona normalmente

Note o import _ "embed" — ele é obrigatório quando você usa //go:embed com tipos primitivos (string ou []byte). Sem ele, o compilador retorna erro.

Embutindo Diretórios com embed.FS

Para múltiplos arquivos ou diretórios inteiros, use o tipo embed.FS:

package main

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

//go:embed templates/*
var templateFiles embed.FS

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

func main() {
	// Listar arquivos embutidos
	fs.WalkDir(templateFiles, ".", func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if !d.IsDir() {
			fmt.Println("Embutido:", path)
		}
		return nil
	})

	// Ler um arquivo específico
	data, err := templateFiles.ReadFile("templates/index.html")
	if err != nil {
		panic(err)
	}
	fmt.Printf("Template: %s\n", data)
}

O padrão templates/* embute todos os arquivos no diretório templates/. Você pode usar múltiplos padrões:

//go:embed static/css/* static/js/* static/images/*
var assets embed.FS

Servindo Assets Estáticos via HTTP

Combinar embed.FS com o net/http da standard library é uma das aplicações mais práticas. Perfeito para aplicações HTMX ou SPAs com frontend embutido:

package main

import (
	"embed"
	"io/fs"
	"log"
	"net/http"
)

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

func main() {
	// Remover o prefixo "static/" para servir na raiz
	staticFS, err := fs.Sub(staticFiles, "static")
	if err != nil {
		log.Fatal(err)
	}

	mux := http.NewServeMux()
	mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticFS))))
	mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("API Go com assets embutidos!"))
	})

	log.Println("Servidor rodando em :8080")
	log.Fatal(http.ListenAndServe(":8080", mux))
}

A função fs.Sub() cria um sub-filesystem que remove o prefixo do diretório. Sem ela, os caminhos dos arquivos incluiriam static/ — e as URLs ficariam como /static/static/style.css.

Templates HTML com embed.FS

Para aplicações web que usam templates server-side, combine embed.FS com html/template. Se você trabalha com templates type-safe, veja também o templ:

package main

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

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

var tmpl *template.Template

func init() {
	tmpl = template.Must(template.ParseFS(templateFiles, "templates/*.html"))
}

type PageData struct {
	Title string
	Items []string
}

func homeHandler(w http.ResponseWriter, r *http.Request) {
	data := PageData{
		Title: "Minha App Go",
		Items: []string{"Item 1", "Item 2", "Item 3"},
	}
	if err := tmpl.ExecuteTemplate(w, "index.html", data); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /", homeHandler)
	log.Fatal(http.ListenAndServe(":8080", mux))
}

A função template.ParseFS() aceita embed.FS diretamente — sem precisar ler arquivos do disco. Os templates são parseados uma vez no init e reutilizados em cada requisição.

Migrações SQL Embutidas

Outro caso de uso popular é embutir arquivos SQL para migrações de banco de dados. Perfeito para projetos que usam PostgreSQL ou outros bancos relacionais com sqlc:

package migrations

import (
	"embed"
	"io/fs"
	"sort"
	"strings"
)

//go:embed sql/*.sql
var migrationFiles embed.FS

func GetMigrations() ([]string, error) {
	entries, err := fs.ReadDir(migrationFiles, "sql")
	if err != nil {
		return nil, err
	}

	var migrations []string
	for _, e := range entries {
		if !e.IsDir() && strings.HasSuffix(e.Name(), ".sql") {
			migrations = append(migrations, e.Name())
		}
	}
	sort.Strings(migrations)
	return migrations, nil
}

func ReadMigration(name string) (string, error) {
	data, err := migrationFiles.ReadFile("sql/" + name)
	if err != nil {
		return "", err
	}
	return string(data), nil
}

Organize os arquivos com prefixo numérico (001_create_users.sql, 002_add_index.sql) para garantir a ordem correta.

CLI com Help Embutido

Para ferramentas de linha de comando, embutir textos de ajuda e exemplos mantém o binário autocontido:

package main

import (
	_ "embed"
	"fmt"
	"os"
)

//go:embed help.txt
var helpText string

//go:embed examples/basic.yaml
var basicExample string

func main() {
	if len(os.Args) > 1 && os.Args[1] == "--help" {
		fmt.Print(helpText)
		return
	}
	if len(os.Args) > 1 && os.Args[1] == "--example" {
		fmt.Print(basicExample)
		return
	}
	fmt.Println("Executando aplicação...")
}

Regras e Limitações do go:embed

Antes de usar //go:embed, conheça as regras:

  1. A diretiva deve estar imediatamente acima da declaração da variável — sem linhas em branco entre elas
  2. Variáveis devem ser do tipo string, []byte ou embed.FS — outros tipos não são aceitos
  3. Apenas arquivos no módulo atual — não é possível embutir arquivos de fora do módulo
  4. Arquivos ocultos (começando com . ou _) são ignorados por padrão — use all: para incluí-los: //go:embed all:templates
  5. O padrão * não inclui subdiretórios — use ** ou liste explicitamente: //go:embed templates/**
  6. Caminhos são relativos ao package, não ao módulo raiz
// CORRETO: diretiva seguida da variável
//go:embed data.json
var data []byte

// ERRADO: linha em branco entre diretiva e variável
//go:embed data.json

var data []byte  // erro de compilação

Performance: Embed vs Leitura em Disco

Arquivos embutidos são mapeados na memória do binário — a leitura é instantânea, sem syscalls de I/O. Mas considere os trade-offs:

Aspectoembed.FSLeitura em disco
Velocidade de leituraInstantânea (memória)Depende do disco
Tamanho do binárioAumentaNão afeta
AtualizaçãoRequer recompilaçãoEditar arquivo basta
DeployUm arquivoBinário + diretórios

Quando usar embed: templates, migrações SQL, assets de frontend, arquivos de configuração padrão, textos de ajuda de CLI.

Quando usar disco: arquivos grandes (vídeos, datasets), conteúdo que muda sem redeploy, uploads de usuários.

Para aplicações web com WebAssembly, embutir o .wasm compilado no servidor Go é uma combinação poderosa — o servidor serve tanto a API quanto o binário WASM.

Combinando com Table-Driven Tests

Embutir fixtures de teste é outro uso prático. Combine com table-driven tests para iterar sobre múltiplos arquivos de entrada:

package parser

import (
	"embed"
	"io/fs"
	"strings"
	"testing"
)

//go:embed testdata/valid/*.json
var validFixtures embed.FS

//go:embed testdata/invalid/*.json
var invalidFixtures embed.FS

func TestParseValidFiles(t *testing.T) {
	entries, _ := fs.ReadDir(validFixtures, "testdata/valid")
	for _, e := range entries {
		t.Run(e.Name(), func(t *testing.T) {
			data, _ := validFixtures.ReadFile("testdata/valid/" + e.Name())
			_, err := Parse(data)
			if err != nil {
				t.Errorf("arquivo válido %s retornou erro: %v", e.Name(), err)
			}
		})
	}
}

Esse padrão é extensível — adicionar um novo caso de teste é simplesmente adicionar um arquivo JSON na pasta testdata/.

Exemplo Completo: Web Server com Frontend Embutido

Um servidor Go completo que serve uma SPA com assets embutidos e uma API REST:

package main

import (
	"embed"
	"encoding/json"
	"io/fs"
	"log"
	"net/http"
)

//go:embed frontend/dist/*
var frontendFiles embed.FS

func main() {
	frontendFS, err := fs.Sub(frontendFiles, "frontend/dist")
	if err != nil {
		log.Fatal(err)
	}

	mux := http.NewServeMux()

	// API
	mux.HandleFunc("GET /api/health", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
	})

	// Frontend (SPA)
	mux.Handle("GET /", http.FileServer(http.FS(frontendFS)))

	log.Println("Servidor rodando em :8080")
	log.Fatal(http.ListenAndServe(":8080", mux))
}

Com Docker multi-stage build, compile o frontend no primeiro estágio, copie para o diretório do Go, e o binário final inclui tudo. O resultado é uma imagem Docker mínima com API e frontend em um único executável.

Próximos Passos

//go:embed transforma seu binário Go em uma unidade autossuficiente de deploy. Combine com Docker multi-stage builds para imagens mínimas, OpenTelemetry para observabilidade, e Kubernetes para orquestração.

Para gerenciar a configuração em runtime (não embutida), veja como usar Cobra e Viper. E para entender como iteradores podem ajudar a percorrer arquivos embutidos de forma mais elegante, confira nosso guia sobre range over func.

Quer ver como outras linguagens resolvem o empacotamento de assets? Veja como Rust usa crates como include_str! e rust-embed para embutir arquivos em tempo de compilação, ou como Zig resolve isso com @embedFile — uma abordagem minimalista que é built-in na linguagem.

FAQ

O que é go:embed e desde quando está disponível?

A diretiva //go:embed foi introduzida no Go 1.16 (fevereiro de 2021). Ela permite embutir arquivos e diretórios no binário compilado usando os tipos string, []byte ou embed.FS, sem dependências externas ou ferramentas de geração de código.

Embutir arquivos aumenta muito o tamanho do binário?

Sim, o tamanho do binário aumenta proporcionalmente ao tamanho dos arquivos embutidos. Para assets de frontend (HTML, CSS, JS minificados), o impacto geralmente é de poucos MB. Para arquivos grandes como vídeos ou datasets, prefira leitura em disco.

Posso embutir arquivos de fora do módulo Go?

Não. A diretiva //go:embed só aceita caminhos relativos ao package atual, e os arquivos devem estar dentro do módulo Go. Não é possível usar caminhos absolutos ou ../ para sair do módulo.

Como embutir arquivos ocultos (dotfiles)?

Por padrão, arquivos começando com . ou _ são ignorados pelo //go:embed. Para incluí-los, use o prefixo all: na diretiva: //go:embed all:templates — isso inclui .gitkeep, _helpers.html e similares.