Embed estáticos fácil ahora en Go
Como seguro que sabréis trabajar con ficheros estáticos en Go no es una tarea muy sencilla, siempre requiere de librerías de terceros y aprender como funcionan dichas librerías.
Hace algún tiempo os hablábamos de Pkger en este artículo, el cual era una solución que nos ayudaba a la hora de resolver el problema.
Pero desde la versión 1.16 tenemos una solución out of the box es decir integrada en el propio core de Go, y como no podía ser de otra forma queremos explicaros como funciona.
Leyendo ficheros estáticos
Vamos a recurrir a los ejemplos del artículo que comentábamos para realizar la explicación.
Así pues lo primero que vamos a hacer es leer un csv:
func main() {
if err := readCsv(); err != nil {
log.Fatal(err)
}
}
func readCsv() error {
f, err := os.Open("./files/swapi.csv")
if err != nil {
return err
}
defer f.Close()
data, err := f.Stat()
if err != nil {
return err
}
fmt.Println("File name: ", data.Name())
fmt.Println("File size: ", data.Size())
fmt.Println("File mode: ", data.Mode())
fmt.Println("Modification time: ", data.ModTime().Format(time.RFC3339))
fmt.Println()
if _, err := io.Copy(os.Stdout, f); err != nil {
return err
}
return nil
}
Hasta aquí todo es maravilloso y perfecto ya que si ejecutamos nuestro código, funcionará sin problemas, pero ¿qué pasaría si compilamos nuestro código a otro lugar?
2021/03/14 15:30:35 open ./files/swapi.csv: no such file or directory
exit status 1
Como ya comentamos estos errores, no sólo son producidos a al mover nuestro binario, ya que podríamos arrastrar los ficheros con ellos, pero pensemos en test también, al final estamos rompiendo el determinismo de nuestra aplicación la cual puede sufrir comportamientos no deseados por haber movido un fichero de sitio.
¡Embed al rescate! 💪
Como comentaba al principio del artículo ahora no necesitaremos más que tener actualizada nuestra versión de Go a la 1.16 o más si nos estás leyendo en el futuro, para tener todo lo necesario para hacer que estos ficheros estáticos pasen por estar compilados con nuestro binario.
Y los cambios que tendremos que hacer en nuestro código para ello son mínimos:
package main
import (
"embed"
"fmt"
"io"
"log"
"os"
"time"
)
//go:embed files/swapi.csv
var swapiFile embed.FS
func main() {
if err := readCsv(); err != nil {
log.Fatal(err)
}
}
func readCsv() error {
f, err := swapiFile.Open("files/swapi.csv")
if err != nil {
return err
}
defer f.Close()
data, err := f.Stat()
if err != nil {
return err
}
fmt.Println("File name: ", data.Name())
fmt.Println("File size: ", data.Size())
fmt.Println("File mode: ", data.Mode())
fmt.Println("Modification time: ", data.ModTime().Format(time.RFC3339))
fmt.Println()
if _, err := io.Copy(os.Stdout, f); err != nil {
return err
}
return nil
}
Vamos a analizar los cambios lo primero que vemos es que pasamos a tener importado el nuevo paquete embed
, tras eso veremos que declaramos una nueva variable var swapiFile embed.FS
que a su vez tiene uno de esos comentarios mágicos de Go, //go:embed files/swapi.csv
Y es que aquí tenemos dos cosas gracias al comentario //go:embed
podemos indicar el path
de nuestro fichero, el cual sólo puede ser dentro del mismo directorio o subdirectorios, pero como veremos un poco más adelante no acaba ahí la magia, de momento quedémonos con eso. Así pues dicho comando internamente lo que hará es compilar nuestro fichero y tenerlo disponible como parte del código, al igual que hacia go-bindata, a partir de ahí podremos tratar con él de distintas formas.
Dichas formas son de tres tipos, string
, []byte
o embed.FS
. Después veremos ejemplos de cada uno, vamos a centrarnos en nuestro nuevo tipo embed.FS
.
Básicamente lo que hace este embed.FS
es obtener una variable que cumple con tres interfaces distintas, FS
, ReadDirFS
y ReadFileFS
, lo que se traduciría en que tenemos 3 métodos diferentes disponibles.
Open(name string) (File, error)
ReadDir(name string) ([]DirEntry, error)
ReadFile(name string) ([]byte, error)
Al ver estos métodos seguro que estaréis pensando en una colección, ya que nos piden que pasemos nombres, pero nosotros sólo tenemos un fichero a leer, y es que si, embed.FS
es una colección de ficheros, una cosa muy importante también a tener en cuenta es que sus valores son de sólo lectura con lo cual es seguro utilizarlo con nuestras gorrutinas.
Así que sabiendo eso podemos ver que al empezar nuestra función readCSV
hemos cambiado ligeramente la línea por, f, err := swapiFile.Open("files/swapi.csv")
, es decir hemos dejado de usar el paquete os
para utilizar nuestra variable swapiFile
y el resto del código continua exactamente igual.
¿Útil verdad?
Pero aquí no acaba la cosa como dijimos podemos cargar más de un fichero en la variable, y usar diferentes tipos de variable así que vamos a verlo.
Utilizando embded files con string
Pongamos que el contenido de un fichero ya nos es suficiente con leerlo como string, por ejemplo podría ser la versión de nuestro programa.
#version.txt
v0.1.0
Es decir en el fichero version.txt
sólo tenemos el texto v0.1.0
, entonces queremos imprimir eso por pantalla al poner el flag --version
.
package main
import (
_ "embed"
"flag"
"fmt"
)
//go:embed version.txt
var versionStr string
func main() {
version := flag.Bool("version", false, "")
flag.Parse()
if *version {
fmt.Println(versionStr)
}
}
Lo primero que vemos es que hemos pasado de utilizar el paquete embed
directamente a utilizar sólo su inicialización con el _
, posteriormente hemos declarado la variable versionStr
com string
y simplemente la hemos mostrado en pantalla si nos llegaba el flag --version
.
Hay que añadir que utilizar tanto string
como []bye
sólo se permite para un sólo fichero.
Utilizando embed files con []byte
Para este ejemplo partiremos de que queremos cargar una configuración que tengamos guardada por ejemplo en json
, vamos a ver como hacerlo.
Partiremos del fichero config.json
{
"version": "v0.1.0",
"name": "Friends of Go super APP",
"build": 19
}
Y cargaremos la config con el siguiente código.
package main
import (
_ "embed"
"encoding/json"
"fmt"
"log"
)
//go:embed config.json
var configFile []byte
type config struct {
Version string `json:"version"`
Name string `json:"name"`
Build int `json:"build"`
}
func main() {
var c config
if err := json.Unmarshal(configFile, &c); err != nil {
log.Fatal(err)
}
fmt.Println(c)
}
Como veis lo único que hacemos es volver a declarar una variable, esta vez del tipo []byte
, go se encargará igual que antes de leer toda la información y proporcionarnos la cadena de bytes
pertinente. Una vez tenemos el slice de byte
lo único que tenemos que hacer es hacer uso del json.Unrmarshal
para transformarlo a nuestro struct, y ¡magia!
Pero… tengo varios ficheros ¿qué hago?
Pongamos el caso de que queremos montar una web haciendo uso de todo esto que hemos aprendido, spoiler, no vamos a montar nada muy épico sólo algo sencillito para ilustrar el potencial que tiene esta nueva funcionalidad, pero vuestra imaginación es vuestra aliada.
Para hacer este ejemplo hemos necesitado lo siguiente:
|- assets/
|- images/
|- templates/
|- main.go
Dentro de la carpeta images
tenemos una imagen llamada fogo.png
, y dentro de templates
, tenemos un un fichero index.tmpl
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{ .Title }}</title>
</head>
<body>
<h1>This is a demo</h1>
<p>Some text here {{ .Name }}</p>
<center><img src="/public/assets/images/fogo.png"></img></center>
</body>
</html>
Como ya advertí es una web muy rudimentaria pero cumple la función de enseñaros a utilizar el embed
a otro nivel.
Ahora que ya sabemos que estáticos tenemos, vamos a por el código Go que seguro que os esperáis algo muy complejo ¿no?
package main
import (
"embed"
"html/template"
"net/http"
"github.com/gin-gonic/gin"
)
//go:embed assets/* templates/*
var files embed.FS
func main() {
router := gin.Default()
templ := template.Must(template.New("").ParseFS(files, "templates/*.tmpl"))
router.SetHTMLTemplate(templ)
router.StaticFS("/public", http.FS(files))
router.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.tmpl", struct{
Name string
Title string
}{
Name: "Friends of go Web",
Title: "Friends of Go the best web ever",
})
})
router.Run(":8080")
}
Vamos por parte, lo primero que nos encontramos es con el siguiente bloque.
//go:embed assets/* templates/*
var files embed.FS
Como os comentaba, podemos cargar más de un fichero, pero es que podemos decirle que cargue todo un árbol de directorio o varios, una completa gozada a mi entender.
Vamos a utilizar gin-conic para todo lo de servir HTTP
ya que nos ofrece facilidad para tratar con FS
y templates
.
router := gin.Default()
Preparamos nuestro servidor HTTP
, y acto seguido cargamos nuestros templates
, que son simples templates de go.
templ := template.Must(template.New("").ParseFS(files, "templates/*.tmpl"))
router.SetHTMLTemplate(templ)
Tras eso informamos a gin
bajo que ruta queremos que se carguen nuestros ficheros estáticos.
router.StaticFS("/public", http.FS(files))
Es decir que si queremos acceder a la imagen que hemos cargado tendremos una ruta como, public/assets/images/logo.png
Y para terminar, preparamos el endpoint que queremos que sirva nuestro contenido, y arrancamos el servidor en este caso en el puerto 8080
router.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.tmpl", struct{
Name string
Title string
}{
Name: "Friends of go Web",
Title: "Friends of Go the best web ever",
})
})
router.Run(":8080")
Conclusión
Pues hasta aquí ha llegado este artículo de embed
un nuevo interesante paquete que nos permite dejar de tener miedo por esos ficheros estáticos en Go y empezar a utilizarlo sin tener que preocuparnos en demasía, aunque controlar el tamaño de vuestros binarios.
¿Qué más se te ocurre que se podría hacer con este nuevo paquete? Puedes contárnoslo en nuestro twitter oficial FriendsOfGoTech, y ahora además por si el Twitter os sabe a poco, nos tenéis en Youtube y en Twitch.