Recursos incrustados con go:embed

Avanzado
Go
Go
Actualizado: 19/04/2026

La directiva //go:embed incrusta ficheros del directorio del paquete dentro del binario producido por la compilación. Permite distribuir aplicaciones autocontenidas: un único ejecutable que incluye plantillas HTML, scripts SQL, certificados, imágenes y cualquier otro activo necesario, sin depender de rutas externas en tiempo de ejecución.

Forma de uso y tipos soportados

La directiva es un comentario que se escribe inmediatamente antes de una declaración de variable a nivel de paquete. El compilador convierte esa variable en el contenido del fichero indicado. Para que funcione hay que importar el paquete embed, incluso si no se usa su identificador directamente:

package main

import (
    _ "embed"
)

//go:embed version.txt
var version string

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

La variable anotada puede tener tres tipos: string, []byte o embed.FS. Los dos primeros corresponden a un fichero único cuyo contenido se copia tal cual. El tercero representa un sistema de ficheros de solo lectura que puede contener varios ficheros y carpetas:

import "embed"

//go:embed plantillas/*
var plantillasFS embed.FS

La sintaxis admite patrones con comodines (*, **) y puede referirse a directorios completos. Todas las rutas son relativas al fichero fuente donde aparece la directiva. No se permiten rutas absolutas ni rutas que salgan del paquete con ...

La directiva debe ir pegada a la declaración, sin línea en blanco entre el comentario y el var. Si hay una línea en blanco, el compilador la interpreta como comentario normal y no incrusta nada.

Incrustar un solo fichero como string o bytes

Para ficheros pequeños de configuración o de texto suele bastar con un string o un []byte. Esto simplifica el acceso porque no requiere abrir nada:

package main

import (
    _ "embed"
    "fmt"
)

//go:embed version.txt
var version string

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

func main() {
    fmt.Printf("Versión: %s\n", strings.TrimSpace(version))
    fmt.Printf("Config (%d bytes)\n", len(defaultConfig))
}

Este patrón es muy usado para:

  • 1. Versión del binario extraída en tiempo de build (version.txt).
  • 2. Configuración por defecto que el usuario puede sobrescribir con un fichero externo.
  • 3. Scripts SQL de seeding que se ejecutan al arrancar la aplicación.
  • 4. Pequeñas plantillas sueltas cuando no queremos una carpeta completa.

Sistemas de ficheros con embed.FS

Cuando necesitas varios ficheros organizados en carpetas, el tipo embed.FS expone un sistema de ficheros inmutable que implementa la interfaz fs.FS de la biblioteca estándar. Esto permite reutilizar APIs existentes (como http.FileServer o template.ParseFS) sin tocar el binario en ejecución:

//go:embed migraciones/*.sql
var migracionesFS embed.FS

func CargarMigraciones() ([]string, error) {
    entradas, err := migracionesFS.ReadDir("migraciones")
    if err != nil {
        return nil, err
    }
    var scripts []string
    for _, e := range entradas {
        data, err := migracionesFS.ReadFile("migraciones/" + e.Name())
        if err != nil {
            return nil, err
        }
        scripts = append(scripts, string(data))
    }
    return scripts, nil
}

Este código carga todos los scripts .sql empaquetados con el binario, ordenándolos alfabéticamente por nombre. Una convención común es prefijar cada fichero con un número (001_create_users.sql, 002_add_email.sql) para garantizar que las migraciones se aplican en orden.

Servir ficheros estáticos por HTTP

La integración más elegante es servir un frontend completo desde Go. embed.FS se pasa directamente a http.FileServerFS o, para compatibilidad previa, a http.FileServer con http.FS:

//go:embed web/*
var webFS embed.FS

func main() {
    // Aislamos la subcarpeta para que / sirva directamente web/index.html
    sub, _ := fs.Sub(webFS, "web")

    mux := http.NewServeMux()
    mux.Handle("/", http.FileServerFS(sub))
    mux.HandleFunc("GET /api/version", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, version)
    })

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

El método fs.Sub crea un sistema de ficheros virtual enraizado en la subcarpeta indicada. Esto evita que las URLs incluyan el prefijo de la carpeta embebida. Gracias a esta construcción, la aplicación es un único binario que sirve su propio frontend y su API, sin directorios externos.

flowchart LR
    A[go build] --> B[embed.FS en el binario]
    B --> C[http.FileServerFS]
    C --> D[Cliente recibe assets]
    B --> E[template.ParseFS]
    E --> F[Render HTML dinámico]

Plantillas HTML y de texto

Los paquetes html/template y text/template tienen variantes ParseFS que aceptan directamente un embed.FS. Esto permite mantener las plantillas en un directorio de código fuente y renderizarlas sin acceder al disco:

//go:embed plantillas/*.html
var plantillasFS embed.FS

var tpl = template.Must(template.ParseFS(plantillasFS, "plantillas/*.html"))

type PaginaUsuario struct {
    Nombre string
    Email  string
}

func paginaUsuarioHandler(w http.ResponseWriter, r *http.Request) {
    datos := PaginaUsuario{Nombre: "Ana", Email: "ana@acme.com"}
    if err := tpl.ExecuteTemplate(w, "usuario.html", datos); err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
    }
}

Este patrón es tradicional en aplicaciones server-side rendered (SSR) como la documentación interna, dashboards y panales de administración. Al ir dentro del binario, el despliegue se reduce a copiar un único fichero al servidor.

Patrones de inclusión y exclusión

La directiva admite varias rutas o patrones y una línea con varios //go:embed suma entradas al mismo sistema de ficheros. Existe también //go:embed all: para incluir directorios ocultos (los que empiezan por . o _, que por defecto se omiten):

// Varias entradas en una sola directiva
//go:embed static/* templates/* migrations/*
var recursosFS embed.FS

// Incluir .env.example que por defecto se excluye (empieza por .)
//go:embed all:ejemplo/.env.example
var envExample []byte

Cuando necesitas ignorar un subconjunto, Go no ofrece exclusión explícita dentro de la directiva. La solución habitual es reorganizar los ficheros de modo que los excluidos queden fuera del patrón, o mover los incluidos a una carpeta específica para embed.

Build tags y ficheros opcionales

Si un mismo proyecto se compila para varios entornos (por ejemplo, servidor y CLI) y solo uno necesita los assets, se combinan build tags con la directiva. La directiva se escribe en un fichero aparte con su propia restricción de build:

// Fichero assets_web.go
//go:build web

package main

import "embed"

//go:embed web/*
var webFS embed.FS

Con el tag web activado (go build -tags web) se incluye el frontend. Sin el tag, el binario resulta más pequeño porque no incrusta los assets.

Restricciones y consideraciones de tamaño

Aunque go:embed es versátil, conviene usarlo con criterio:

  • 1. El tamaño del binario crece en la misma magnitud que los assets. Videos o imágenes grandes inflan el ejecutable y lo hacen más lento de transferir.

  • 2. Los cambios en los assets requieren recompilar el binario. Si los contenidos cambian con frecuencia independientemente del código, es preferible una distribución separada.

  • 3. La variable es de solo lectura al ser embed.FS. No se pueden modificar los ficheros en caliente. Para recursos mutables, combina los assets embebidos con una carpeta externa consultada en segundo lugar.

  • 4. El contenido viaja en el binario sin compresión adicional. Si el asset ya está comprimido (ZIP, JPEG), no ganas nada; si es texto plano extenso, plantéate distribuirlo en formato comprimido y descomprimirlo en memoria al arrancar.

Un punto de equilibrio habitual es embeber plantillas, ficheros de migración SQL y un frontend ligero, mientras que las imágenes, vídeos y documentos grandes se entregan desde un CDN o bucket. De ese modo el binario sigue siendo ligero y la aplicación arranca con todo lo imprescindible.

Ejemplo integrador

El siguiente ejemplo agrupa los patrones más frecuentes en una pequeña aplicación que sirve un frontend estático, un endpoint JSON y aplica migraciones SQL al arrancar:

package main

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

//go:embed web/*
var webFS embed.FS

//go:embed plantillas/*.html
var plantillasFS embed.FS

//go:embed migraciones/*.sql
var migracionesFS embed.FS

//go:embed version.txt
var version string

func main() {
    log := slog.Default()

    if err := aplicarMigraciones(migracionesFS); err != nil {
        log.Error("migraciones", "err", err)
        return
    }

    sub, _ := fs.Sub(webFS, "web")

    mux := http.NewServeMux()
    mux.Handle("/", http.FileServerFS(sub))
    mux.HandleFunc("GET /api/version", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "text/plain")
        w.Write([]byte(strings.TrimSpace(version)))
    })

    log.Info("servidor arrancado", "puerto", 8080, "version", strings.TrimSpace(version))
    http.ListenAndServe(":8080", mux)
}

La aplicación resultante se distribuye como un único binario sin directorios adicionales: los assets, las migraciones y la versión viven dentro del ejecutable, lo que simplifica enormemente el despliegue en contenedores y servidores sin dependencias del sistema de ficheros.

Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Go es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Go

Explora más contenido relacionado con Go y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

Usar la directiva go:embed para incluir ficheros sueltos, cadenas, slices de bytes y sistemas de ficheros completos, integrarlos con http.FileServer y html/template, y aprovechar fs.Sub para aislar subárboles.