Construyendo una herramienta de cliente en GO
Seguro que en tu día a día te has encontrado con que utilizas multitud de herramientas de línea de comandos como desarrollador, véase Git o Docker, entre otras. Incluso puede que hayas creado las tuyas propias en algún momento de tu vida.
Pero, ¿cómo se hacen en GO? Por suerte para nosotros, y para variar, GO viene con la solución bajo el brazo, sin tener que recurrir a librerías externas.
En este artículo crearemos una simple herramienta con interfaz de línea de comandos (a partir de ahora command-line), en la que utilizaremos la API de Bacon Ipsum para generar bloques de textos, que luego podríamos usar como boilerplate en nuestras demos de aplicaciones web o donde queramos.
Para centrarnos en el tema, ignoraremos toda la parte correspondiente a las llamadas a la API, dando por implementados los métodos responsables de ello. Sin embargo, al final del artículo os ofreceremos el repositorio entero donde podréis ver todo el ejercicio.
Inicializando el proyecto
Ya que vamos a implementar un command-line, aprovechemos por crear un poco de estructura de proyecto, nosotros recomendamos la siguiente:
bacon-ipsum
- cmd
-- bacon-ipsum
--- main.go
- generator.go
- LICENSE
- README.md
Es decir, lo que viene a ser nuestra interfaz de línea de comandos irá dentro del directorio cmd
y los ficheros que se necesitarán para consumir la API irán en el directorio raíz, creando paquetes específicos si se considera necesario.
De momento nuestro main.go
tendrá un aspecto similar a éste:
package main
import "fmt"
func main() {
fmt.Println("Hello bacon ipsum")
}
Y podemos probarlo simplemente utilizando go run
$ go run cmd/bacon-ipsum/main.go
Hello bacon ipsum
Recogiendo los parámetros
Hasta ahora estaréis pensando, muy bonito pero eso ya lo sé hacer, es el Hello World de GO, sí, lo sé, pero tenemos que inicializar nuestra aplicación.
Para tratar con los argumentos recibidos tenemos dos formas:
- O bien nos lo picamos todo a mano, usando el paquete
os
y recogiendo los parámetros conos.Args
:
package main
import (
"fmt"
"os"
)
func main() {
fmt.Println("Hello", os.Args[1])
}
$ go run cmd/bacon-ipsum/main.go FriendsOfGo
Hello FriendsOfGo
- O bien podemos utilizar el paquete flag, el cual ya nos ofrece una serie de métodos para parsear los inputs de nuestro command-line.
Flags: String, Bool, Int
El paquete flag
nos ofrece diferentes tipos de parseos, nosostros nos vamos a quedar con los más sencillos (y quizás los más utilizados) para este ejercicio.
Hay que tener en cuenta que, cuándo leemos estas opciones en nuestra consola basándonos en el paquete flag
, éstos serán retornados como punteros, pero tranquilos, si no estáis muy familiarizados con el funcionamiento de los punteros, tampoco se os hará costoso de entender y ni usar aquí.
Vamos a picar un poco de código para ver como funciona este paquete.
package main
import (
"flag"
)
func main() {
textTypePtr := flag.String("type", "", "Type of the text to generate")
parasPtr := flag.Int("paras", 5, "number of paragraphs")
sentencesPtr := flag.Int("sentences", 0, "number of senteces (this overrides paragraphs)")
withLorem := flag.Bool("withLorem", false, "if it is true the first paragraph start with 'Bacon dolor sit amet'")
flag.Parse()
}
Como podemos ver en el código anterior, la forma de parsear cada opción de nuestra aplicación se hace de la misma manera, cambiando sólo el tipo de la variable que representará el argumento que vamos a parsear.
Por ejemplo:
textTypePtr := flag.String("type", "", "Type of the text to generate")
Aquí podemos ver que el primer parámetro de la función es el nombre de la opción, es decir, que cuándo ejecutemos nuestro cliente, lo haremos añadiendo la opción type ./bacon-ipsum -type meat-and-filler
.
El paquete flag
entiende ambas opciones: tanto si usamos un sólo guión -type
como si usamos dos --type
.
El segundo parámetro de la función es el valor por defecto, es decir, que valor tendrá nuestro puntero si no especificamos esa opción. Y, por último, una descripción acerca de la utilidad que nos proporciona dicha opción.
Toda esta explicación sirve para los argumentos de tipo String
e Int
¿pero qué pasa con los de tipo Bool
? Éstos no necesitan que se les especifique un valor a la hora de ejecutar nuestro cliente, sino que con sólo usarlos ya se sobreentiende qué tienen valor true
.
Por ejemplo: ./bacon-ipsum -type meat-and-filler -withLorem
Para que todo esto ocurra, y nuestros punteros tengan la información que el usuario está pasando por input, tenemos que ejecutar la sentencia flag.Parse()
, ya que, si no lanzamos dicha sentencia, simplemente nuestros punteros valdrán siempre el valor por defecto que hemos indicado.
Podemos probar que, lo que tenemos hasta ahora está funcionando, añadiendo un Print
a nuestro anterior código:
fmt.Println(
"type:", *textTypePtr,
"paras:", *parasPtr,
"sentences:", *sentencesPtr,
"withLorem:", *withLorem,
)
Vamos a jugar
En este punto, ya tenemos los conocimientos necesarios para poder desarrollar nuestra aplicación final. Nosotros, como dijimos al principio, hemos creado toda una capa de contacto con la API de Bacon Ipsum y os enseñaremos como darle forma a un cliente de command-line con cara y ojos.
Para que nuestra aplicación funcione correctamente deberemos de comprobar que ciertas cosas se cumplen, como por ejemplo que el type
no venga vacío o sea de ciertos tipos específicos.
Veamos como lo solucionamos.
// Parsing textTypePtr to type TextType
textType := bacon.TextType(*textTypePtr)
// Check if is a valid textType or empty
if !textType.Validate() {
flag.PrintDefaults()
os.Exit(2)
}
}
Lo que hemos hecho es, primeramente, convertir el textTypePtr
que hasta ahora era un string en un TextType
de nuestra librería (en otro artículo explicaremos como funcionan los Custom Types
, de momento ¡tened fe!) y además hemos creado una función que nos valida si es del tipo correcto, con lo cual, nos ahorramos comprobar que no sea empty
.
Lo importante en este bloque de código es lo que pasa cuando lo que nos introduce el usuario no es correcto.
El flag.PrintDefaults()
nos imprimirá toda la información acerca del funcionamiento de nuestro cliente:
-paras int
number of paragraphs (default 5)
-sentences int
number of senteces (this overrides paragraphs)
-type string
Type of the text to generate (Required) [Valid options: all-meat, meat-and-filler]
-withLorem
if it is true the first paragraph start with 'Bacon dolor sit amet'
En este caso, además, usamos os.Exit(2)
para indicar que se ha producido un error.
El comando help
Toda buena aplicación que se precie dispone de un comando help
, ¿cómo lo hacemos aquí?
El paquete flag
nos facilita una función llamada Usage
con la que podemos pintar la descripción de nuestra aplicación y que aparecerá cuándo hagamos -help
o --help
en nuestra ejecución.
Para que esto sea posible, tenemos que implementar la función usage
para nuestro cliente, pues, en caso contrario, no dispondrá de la opción de ayuda (help
).
Vamos a ver como se hace:
var appName = "bacon-ipsum"
func main() {
[...]
flag.Usage = usage
flag.Parse()
if !textType.Validate() {
flag.Usage()
os.Exit(2)
}
[...]
}
func usage() {
msg := fmt.Sprintf(`usage: %s [OPTIONS]
%s is a simple tool to generate random text based on a bacon ipsum API
`, appName, appName)
fmt.Println(msg)
flag.PrintDefaults()
}
Simple, ¿verdad? Sólo tenemos que crear una función que imprima lo que queremos mostrar como descripción y luego llamar al flag.PrintDefaults()
para que imprima toda la información sobre las opciones que tiene nuestro cliente.
Además, en la comprobación, usaremos la función Usage()
para que se comporte igual que el --help
.
Conclusión
Con esto ya tendremos nuestra primera aplicación de consola funcionando, nos quedarían por ver algunas cosas más de este paquete, como los Flagset
, muy útiles si queremos añadir subcomandos, pero eso lo veremos mejor en otro artículo. Además, también os enseñaremos a utilizar la librería Cobra, la cual nos facilitará el trabajo a la hora de realizar aplicaciones de command-line como la que hemos desarrollado a lo largo de este artículo.
Ahora es vuestro turno, queremos ver vuestros proyectos, y queremos ver si de verdad os están resultando útiles nuestros artículos. Así qué, vamos a abrir un pequeño concurso: queremos que creéis un cliente de consola, usando lo aprendido hoy. Cuándo lo tengáis, compartid vuestro repositorio mencionando nuestra cuenta de Twitter @FriendsofGoTech con el hashtag #QuieroLaTazaDeFriendsOfGo, y, así, le echaremos un ojo a vuestros proyectos, ni que decir que debéis seguirnos en twitter para que contemos vuestra participación.
El proyecto que nos parezca más original entre los recibidos, desde hoy, hasta el 28 de Febrero de 2019, se llevará de regalo una fantástica taza de Friends of Go Como esta:
Podéis coger de inspiración el repositorio que hemos creado para este tutorial: https://github.com/friendsofgo/bacon-ipsum.
¡Ya tenemos ganas de ir viendo vuestros proyectitos!