Hoy vamos a utilizar ficheros estáticos en Go y es que seguro que en más de un caso te has visto en la necesidad de utilizar dichos ficheros en tus aplicaciones Go, véase imágenes, ficheros HTML, JSON o de configuración.
Algo que caracteriza a Go es que tan sólo necesitarás de un binario para distribuir tu aplicación, esto hace que sus binarios sean algo no sean excesivamente pequeños, pero los hacen realmente portables, pero si añadimos ficheros estáticos esto deja de ser una realidad, ya que tendremos que estar pendientes de sus posibles rutas y arrastrar nuestros estáticos con el binario, lo cual pierde un poco la gracia.
Para ello existían soluciones como go-bindata, el cual nos permitía generar un binario de nuestros ficheros, y compilarlo junto al binario de nuestra aplicación, respetando además nuestras rutas, era una solución rudimentaria pero que funcionaba. Sin embargo, esta librería ya no está mantenida por sus creadores.
Así que hoy os queremos presentar una solución que nos ha parecido muy útil y sencilla de usar, creada por el gran Mark Bates, co-fundador también del famoso framework Buffalo, se trata de Pkger, pero antes de explicaros como funciona, expliquemos cómo funciona con la librería estática la inclusión de ficheros estáticos y como Pkger soluciona la papeleta.
Leyendo ficheros estáticos
Vamos a mostrar un ejemplo muy simple de leer e imprimir un fichero CSV y mostrar la información de dicho fichero.
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
}
si ejecutamos nuestro código obtendremos la siguiente salida:
Como podemos ver, tenemos cierta información del fichero así como de su contenido, pero ahora mismo esto no es relevante. Fijémosnos en la primera línea de la función readCsv()
: f, err := os.Open("./files/swapi.csv")
como vemos, estamos utilizando una ruta relativa, en este caso concreto, le estamos diciendo que empiece a mirar desde donde estamos ejecutando nuestro código.
Actualmente tenemos dicha estructura en nuestro repositorio:
Así que no habrá mayor problema. Pero, ¿que sucede si cambiamos a la siguiente organización?
Si ejecutamos go run cmd/reader/main.go
todo nuestro código funcionará como esperamos, pero si nos movemos a cmd/reader/
y lanzamos go run main.go
obtendremos el siguiente error:
A primera vista no parece un gran problema, pero pensemos en tests, y en ficheros compilados, nuestra aplicación debe ser determinista y siempre funcionar de la manera que esperamos, aunque lancemos el binario desde otro lugar.
¿Así qué cómo lo solucionamos?
Paquete markbates/pkger
Aquí es donde entra Mark Bates con su solución, para ello ha desarrollado el paquete markbates/pkger
para que sea completamente compatible con las soluciones ofrecidas por la librería estandar, es más si cogemos nuestro código anterior sólo tendremos que hacer dos pequeños cambios:
f, err := os.Open("./files/swapi.csv")
f, err := pkger.Open("/files/swapi.csv")
Hemos cambiado el paquete os
por pkger
y hemos removido el .
inicial que indicaba que buscará desde donde nos encontrábamos. Teniendo como resultado final:
func main() {
if err := readCsv(); err != nil {
log.Fatal(err)
}
}
func readCsv() error {
f, err := f, err := pkger.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
}
Si probamos a ejecutar ambos casos mencionados antes veremos que todo nos funcionará correctamente.
¿Qué más puede hacer markbates/pkger?
Pkger
intenta cubrir dos interfaces del propio core de Go. La primera de ellas pkging.Pkger, cubre los paquetes os
y path/filepath
.
type Pkger interface {
Parse(p string) (here.Path, error)
Current() (here.Info, error)
Info(p string) (here.Info, error)
Create(name string) (File, error)
MkdirAll(p string, perm os.FileMode) error
Open(name string) (File, error)
Stat(name string) (os.FileInfo, error)
Walk(p string, wf filepath.WalkFunc) error
Remove(name string) error
RemoveAll(path string) error
}
La siguiente interfaz pkging.File, intenta cubrir todo lo relacionado con os.File
.
type File interface {
Close() error
Info() here.Info
Name() string
Open(name string) (http.File, error)
Path() here.Path
Read(p []byte) (int, error)
Readdir(count int) ([]os.FileInfo, error)
Seek(offset int64, whence int) (int64, error)
Stat() (os.FileInfo, error)
Write(b []byte) (int, error)
}
¿Pero cómo compilo mis estáticos con mi binario?
Cierto, al principio del artículo os comentábamos que existía una librería que ahora ya no esta mantenida que compilaba nuestros ficheros estáticos y nos permitía continuar seguir teniendo un binario completamente portable. Pues parece que Mark, también ha pensado en eso ya que la librería pkger
incluye una aplicación de consola para realizar ciertas tareas, entre ellas la mencionada.
Para instalarla basta con ejecutar:
$ go get -u github.com/markbates/pkger/cmd/pkger
Una vez instalada tendremos a nuestra disposición los siguientes comandos:
Para generar nuestros binarios bastará con ejecutar:
$ pkger
Eso nos acabará generando un fichero pkged.go
con toda la información en binario de nuestros ficheros, de forma que podremos compilar nuestro paquete sin preocuparnos de transportar nuestros ficheros estáticos.
Si renombramos o eliminamos nuestro anterior directorio files
, veréis que todo continua funcionando correctamente.
Conclusión
Hemos visto los problemas que nos puede causar utilizar las rutas relativas en nuestros proyectos, esto nos puede obligar a tener que calcular la ruta de nuestra raíz o utilizar variables de entorno para informar cual es el directorio desde el que hay que comenzar a buscar, complicando así nuestra aplicación. Además al no compilarse dichos ficheros con el binario, tendríamos que construir artefactos y moverlos juntos para que todo cuadrara como toca.
Con la librería ofrecida por Mark Bates esto es cosa del pasado, de momento no recomendamos su uso en producción ya que está en su versión v0.10.1
(en el momento de redactar este artículo) y el propio Mark Bates pide ayuda en su artículo, para poder terminar de refinarla, pero sin duda pinta realmente bien y creemos que puede aportar mucho a la comunidad.
Ya sabéis que si tenéis cualquier duda o comentario podéis dejarlo en los comentarios o en nuestro Twitter @FriendsofGoTech.