go:embed en Go: incluir ficheros estáticos en el binario y go generate

Desde Go 1.16, la directiva //go:embed permite incluir ficheros, imágenes, plantillas y cualquier otro asset dentro del binario en tiempo de compilación. El resultado es un ejecutable completamente autocontenido que no depende de rutas relativas ni de la presencia de ficheros en el sistema donde se ejecuta. go generate complementa esto automatizando la generación de código repetitivo antes de compilar.

Incrustar un fichero de texto

package main

import (
    _ "embed"
    "fmt"
)

//go:embed version.txt
var version string

func main() {
    fmt.Println("Versión:", version) // contenido de version.txt en tiempo de compilación
}

La directiva debe ir en la línea inmediatamente anterior a la variable. La variable puede ser string, []byte o embed.FS.

Incrustar múltiples ficheros con embed.FS

embed.FS es el tipo adecuado cuando necesitas varios ficheros. Admite globs y subdirectorios:

package main

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

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

//go:embed assets/css assets/js assets/img
var assetsFS embed.FS

func main() {
    // Listar ficheros embebidos
    fs.WalkDir(templateFS, ".", func(path string, d fs.DirEntry, err error) error {
        if !d.IsDir() {
            fmt.Println(path)
        }
        return nil
    })

    // Leer un fichero concreto
    data, _ := templateFS.ReadFile("templates/index.html")
    fmt.Println(string(data))
}

Servidor web self-contained

Combinando embed.FS con http.FileServer obtienes un servidor que distribuye sus propios assets sin depender de ningún directorio externo:

package main

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

//go:embed web/static
var staticFS embed.FS

//go:embed web/templates
var templateFS embed.FS

func main() {
    // Servir ficheros estáticos desde el FS embebido
    staticSub, _ := fs.Sub(staticFS, "web/static")
    http.Handle("/static/", http.StripPrefix("/static/",
        http.FileServer(http.FS(staticSub))))

    // Cargar templates embebidos
    tmpl := template.Must(template.ParseFS(templateFS, "web/templates/*.html"))

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        tmpl.ExecuteTemplate(w, "index.html", map[string]any{
            "Titulo": "App Go embebida",
        })
    })

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

Embeber configuración

import (
    _ "embed"
    "encoding/json"
)

//go:embed config/defaults.json
var configDefaults []byte

type Config struct {
    Puerto   int    `json:"puerto"`
    LogLevel string `json:"log_level"`
    Debug    bool   `json:"debug"`
}

func CargarConfigDefaults() (*Config, error) {
    var cfg Config
    if err := json.Unmarshal(configDefaults, &cfg); err != nil {
        return nil, err
    }
    return &cfg, nil
}

go generate: automatizar la generación de código

go generate busca comentarios con la directiva //go:generate y ejecuta los comandos indicados. Es un paso previo a la compilación que automatiza la generación de código repetitivo:

//go:generate stringer -type=Estado
//go:generate mockgen -destination=mocks/servicio_mock.go -package=mocks . Servicio
//go:generate protoc --go_out=. --go-grpc_out=. proto/api.proto

type Estado int

const (
    Pendiente Estado = iota
    Procesando
    Completado
    Error
)

// Tras ejecutar: go generate ./...
// Se genera automáticamente estado_string.go con:
// func (s Estado) String() string { ... }

stringer: generar String() automáticamente

// Instalar la herramienta
// go install golang.org/x/tools/cmd/stringer@latest

//go:generate stringer -type=Direccion
type Direccion int

const (
    Norte Direccion = iota
    Sur
    Este
    Oeste
)

// Genera direction_string.go con:
// func (i Direccion) String() string {
//     return [...]string{"Norte", "Sur", "Este", "Oeste"}[i]
// }

La combinación de go:embed y go generate permite construir aplicaciones que se distribuyen como un único binario sin instaladores, sin gestionar rutas de datos y con el código generado siempre actualizado antes de cada compilación.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP