19 Building, versionamiento y control de calidad

En esta sección del libro vamos a cambiar nuestro enfoque de escribir código a gestionar y mantener nuestro proyecto, y tomar medidas para ayudar a automatizar tareas comunes y preparar nuestra API para su implementación.

Específicamente, aprenderás cómo:

  • Utilizar un makefile para automatizar tareas comunes en tu proyecto, como crear y ejecutar migraciones.
  • Realizar controles de calidad de tu código utilizando las herramientas go vet y staticcheck.
  • Construir y ejecutar binarios ejecutables para tus aplicaciones, reducir su tamaño y compilar binarios para diferentes plataformas.
  • Incrustar un número de versión y hora de construcción en tu aplicación al compilar el binario.
  • Aprovechar Git para generar números de versión automáticos como parte de tu proceso de construcción.

19.1 Creando y usando Makefiles

En este primer capítulo vamos a ver cómo utilizar la utilidad GNU make y los makefiles para ayudar a automatizar tareas comunes en tu proyecto. La herramienta make debería estar preinstalada en la mayoría de las distribuciones de Linux, pero si no está en tu máquina, deberías poder instalarla a través de tu gestor de paquetes. Por ejemplo, si tu sistema operativo soporta el gestor de paquetes apt (como Debian y Ubuntu), puedes instalarlo con:

$ sudo apt install make

También es probable que ya esté en tu máquina si utilizas macOS, pero si no lo está, puedes usar brew para instalarlo:

$ brew install make

En máquinas con Windows puedes instalar make utilizando el gestor de paquetes Chocolatey con el comando:

> choco install make

19.1 Un simple makefile

Ahora que tienes instalada la utilidad make en tu sistema, vamos a crear nuestra primera iteración de un makefile. Empezaremos de manera sencilla y luego iremos construyendo las cosas paso a paso.

Un makefile es básicamente un archivo de texto que contiene una o más reglas que la utilidad make puede ejecutar. Cada regla tiene un objetivo y contiene una secuencia de comandos secuenciales que se ejecutan cuando se activa la regla. En general, las reglas del makefile tienen la siguiente estructura:

# comment (optional)
target:
  command
  command
...

Importante: Ten en cuenta que cada comando en una regla de un makefile debe comenzar con un carácter de tabulación, no con espacios. Si estás leyendo esto en formato PDF, la tabulación aparecerá como un único espacio en el fragmento anterior, pero asegúrate de que sea un carácter de tabulación en tu propio código.

Si has estado siguiendo, deberías tener ya un Makefile vacío en la raíz de tu directorio de proyecto. Vamos a crear una regla que ejecute el comando go run ./cmd/api para ejecutar nuestra aplicación de API. Sería así:

run:
  go run ./cmd/api

Asegúrate de que el Makefile esté guardado, y luego puedes ejecutar una regla específica ejecutando $ make <target> desde tu terminal.

Adelante, vamos a llamar a make run para iniciar la API:

$ make run
go run ./cmd/api
time=2023-09-10T10:59:13.722+02:00 level=INFO msg="database connection pool established"
time=2023-09-10T10:59:13.722+02:00 level=INFO msg="starting server" addr=:4000 env=development

Genial, eso ha funcionado bien. Cuando escribimos make run, la utilidad make busca un archivo llamado Makefile o makefile en el directorio actual y luego ejecuta los comandos asociados con el objetivo run. Una cosa a tener en cuenta: de forma predeterminada, make muestra los comandos en la salida del terminal. Podemos verlo en el código anterior, donde la primera línea en la salida es el comando echo go run ./cmd/api. Si lo deseas, es posible suprimir la visualización de los comandos agregando el carácter @ delante de ellos.

19.1 Variables de entorno

Cuando ejecutamos una regla de make, cada variable de entorno que está disponible para make cuando se inicia se transforma en una variable de make con el mismo nombre y valor. Luego podemos acceder a estas variables utilizando la sintaxis ${NOMBRE_VARIABLE} en nuestro makefile.

Para ilustrar esto, creemos dos reglas adicionales: una regla psql para conectarnos a nuestra base de datos y una regla up para ejecutar nuestras migraciones de base de datos. Si has estado siguiendo, ambas reglas necesitarán acceder al valor del DSN de la base de datos desde tu variable de entorno GREENLIGHT_DB_DSN.

Adelante, actualiza tu Makefile para incluir estas dos nuevas reglas de la siguiente manera:

run:
  go run ./cmd/api
psql:
  psql ${GREENLIGHT_DB_DSN}
up:
  @echo &#39;Running up migrations...&#39;
  migrate -path ./migrations -database ${GREENLIGHT_DB_DSN} up

Observa cómo hemos utilizado el carácter @ en la regla up para evitar que el comando echo sea mostrado cuando se ejecuta. Bien, probemos esto ejecutando make up para ejecutar nuestras migraciones de base de datos:

$ make up
Running up migrations...
migrate -path ./migrations -database postgres://greenlight:pa55word@localhost/greenlight up
no change

Deberías ver en la salida que el valor de tu variable de entorno GREENLIGHT_DB_DSN se extrae con éxito y se utiliza en la regla de make. Si has estado siguiendo, no debería haber ninguna migración pendiente para aplicar, por lo que esto debería salir exitosamente sin más acciones.

También estamos empezando a ver los beneficios de usar un makefile aquí: poder escribir make up es una gran mejora en lugar de tener que recordar y usar el comando completo para ejecutar nuestras migraciones de “up”.

Del mismo modo, si lo deseas, también puedes intentar ejecutar make psql para conectarte a la base de datos de Greenlight con psql.

19.1 Pasando argumentos

La utilidad make también te permite pasar argumentos con nombre al ejecutar una regla en particular. Para ilustrar esto, agreguemos una regla de migración a nuestro makefile para generar un nuevo par de archivos de migración. La idea es que cuando ejecutemos esta regla, pasaremos el nombre de los archivos de migración como un argumento, similar a esto:

$ make migration name=create_example_table

La sintaxis para acceder al valor de los argumentos con nombre es exactamente la misma que para acceder a las variables de entorno. Entonces, en el ejemplo anterior, podríamos acceder al nombre del archivo de migración a través de ${name} en nuestro makefile. Adelante, actualiza el makefile para incluir esta nueva regla de migración, de la siguiente manera:

run:
  go run ./cmd/api
psql:
  psql ${GREENLIGHT_DB_DSN}
migration:
  @echo &#39;Creating migration files for ${name}...&#39;
  migrate create -seq -ext=.sql -dir=./migrations ${name}
up:
  @echo &#39;Running up migrations...&#39;
  migrate -path ./migrations -database ${GREENLIGHT_DB_DSN} up

Y si ejecutas esta nueva regla con el argumento name=create_example_table, deberías ver la siguiente salida:

$ make migration name=create_example_table
Creating migration files for create_example_table ...
migrate create -seq -ext=.sql -dir=./migrations create_example_table
/home/alex/Projects/greenlight/migrations/000007_create_example_table.up.sql
/home/alex/Projects/greenlight/migrations/000007_create_example_table.down.sql

Ahora también tendrás dos nuevos archivos de migración vacíos con el nombre create_example_table en tu carpeta de migraciones. Sería así:

$ ls ./migrations/
000001_create_movies_table.down.sql000004_create_users_table.up.sql
000001_create_movies_table.up.sql000005_create_tokens_table.down.sql
000002_add_movies_check_constraints.down.sql
000002_add_movies_check_constraints.up.sql000005_create_tokens_table.up.sql
000006_add_permissions.down.sql
000003_add_movies_indexes.down.sql000006_add_permissions.up.sql
000003_add_movies_indexes.up.sql000007_create_example_table.down.sql
000004_create_users_table.down.sql000007_create_example_table.up.sql

Si estás siguiendo, en realidad no vamos a utilizar estos dos nuevos archivos de migración, así que siéntete libre de eliminarlos:

$ rm migrations/000007*

Nota: Los nombres de variables en los makefiles son sensibles a mayúsculas y minúsculas, por lo que foo, FOO y Foo se refieren a variables diferentes. La documentación de make sugiere usar letras minúsculas para los nombres de variables que sirven únicamente para propósitos internos en el makefile, y usar letras mayúsculas para otros nombres de variables.

19.1 Namespacing targets

A medida que tu makefile siga creciendo, es posible que desees comenzar a nombrar tus objetivos de manera que proporcionen cierta diferenciación entre las reglas y ayuden a organizar el archivo. Por ejemplo, en un makefile grande, en lugar de tener el nombre del objetivo como up, sería más claro darle el nombre db/migrations/up.

Recomiendo usar el carácter / como separador de espacio de nombres, en lugar de un punto, guión o el carácter :. De hecho, el carácter : debe evitarse estrictamente en los nombres de los objetivos, ya que puede causar problemas al usar requisitos de objetivo (algo que cubriremos en un momento).

Actualicemos nuestros nombres de objetivos para usar algunos espacios de nombres sensatos, de la siguiente manera:

run/api:
 go run ./cmd/api
db/psql:
 psql ${GREENLIGHT_DB_DSN}
db/migrations/new:
 @echo &#39;Creating migration files for ${name}...&#39;
 migrate create -seq -ext=.sql -dir=./migrations ${name}
db/migrations/up:
 @echo &#39;Running up migrations...&#39;
 migrate -path ./migrations -database ${GREENLIGHT_DB_DSN} up

Y deberías poder ejecutar las reglas escribiendo el nombre completo del objetivo al ejecutar make. Por ejemplo:

$ make run/api
go run ./cmd/api
time=2023-09-10T10:59:13.722+02:00 level=INFO msg=&quot;database connection pool established&quot;
time=2023-09-10T10:59:13.722+02:00 level=INFO msg=&quot;starting server&quot; addr=:4000 env=development

Una característica útil de usar el carácter / como separador de espacio de nombres es que obtienes completado de pestañas en el terminal al escribir nombres de objetivos. Por ejemplo, si escribes make db/migrations/ y luego presionas la tecla de tabulación en tu teclado, se listarán los objetivos restantes bajo ese espacio de nombres. Sería así:

$ make db/migrations/
new up

19.1 Objetivos previos y solicitud de confirmación

La sintaxis general para una regla de makefile que proporcioné al comienzo de este capítulo fue una ligera simplificación, ya que también es posible especificar objetivos previos.

target: prerequisite-target-1 prerequisite-target-2 ...
  command
  command
...

Cuando especificas un objetivo previo para una regla, los comandos correspondientes para los objetivos previos se ejecutarán antes de ejecutar los comandos reales del objetivo. Leveremos esta funcionalidad para pedir al usuario que confirme antes de continuar antes de ejecutar nuestra regla db/migrations/up. Para hacer esto, crearemos un nuevo objetivo de confirmación que pregunte al usuario “¿Está seguro? [y/N]” y salga con un error si no ingresan y. Luego usaremos este nuevo objetivo de confirmación como un objetivo previo para db/migrations/up. Ve adelante y actualiza tu Makefile de la siguiente manera:

# Crear el nuevo objetivo de confirmación.
confirm:
    @echo -n &#39;¿Estás seguro? [y/N] &#39; &amp;&amp; read ans &amp;&amp; [ $${ans:-N} = y ]

run/api:
    go run ./cmd/api

db/psql:
    psql ${GREENLIGHT_DB_DSN}

db/migrations/new:
    @echo &#39;Creando archivos de migración para ${name}...&#39;
    migrate create -seq -ext=.sql -dir=./migrations ${name}

# Inclúyelo como un objetivo previo.
db/migrations/up: confirm
    @echo &#39;Ejecutando migraciones hacia arriba...&#39;
    migrate -path ./migrations -database ${GREENLIGHT_DB_DSN} up

El código en el objetivo confirm se toma de esta publicación en StackOverflow. Básicamente, lo que sucede aquí es que preguntamos al usuario "¿Estás seguro? [y/N]" y luego leemos la respuesta. Luego utilizamos el código [ $${ans:-N} = y ] para evaluar la respuesta; esto devolverá verdadero si el usuario ingresa y y falso si ingresa cualquier otra cosa. Si un comando en un makefile devuelve falso, entonces make dejará de ejecutar la regla y saldrá con un mensaje de error, deteniendo efectivamente la regla en su curso. Además, es importante notar que hemos establecido confirm como un prerrequisito para el objetivo db/migrations/up. ¡Probemos esto y veamos qué sucede cuando ingresamos y:

$ make db/migrations/up
Are you sure? [y/N] y
Running up migrations...
migrate -path ./migrations -database postgres://greenlight:pa55word@localhost/greenlight up
no change

Eso parece bien: los comandos en nuestra regla db/migrations/up se han ejecutado como esperábamos.

En contraste, intentemos hacer lo mismo otra vez pero ingresando cualquier otra letra cuando se nos pida confirmación. Esta vez, make debería salir sin ejecutar nada en la regla db/migrations/up. Como esto:

$ make db/migrations/up
Are you sure? [y/N] n
make: *** [Makefile:3: confirm] Error 1

Utilizar un objetivo de confirmación como un objetivo previo de esta manera es un patrón reutilizable muy útil. Cada vez que tengas una regla de makefile que realiza algo destructivo o peligroso, ahora puedes simplemente incluir confirm como un objetivo previo para pedir al usuario confirmación para continuar.

19.1 Mostrando informacion de ayuda

Otra pequeña cosa que podemos hacer para que nuestro makefile sea más fácil de usar es incluir algunos comentarios y funcionalidades de ayuda. Específicamente, vamos a agregar un comentario antes de cada regla en nuestro makefile con el siguiente formato:

## &lt;example target call&gt;: &lt;help text&gt;

Entonces crearemos un nuevo objetivo de ayuda que analizará el propio makefile, extraerá el texto de ayuda de los comentarios utilizando sed, los formateará en una tabla y luego los mostrará al usuario. Si estás siguiendo, adelante y actualiza tu Makefile para que se vea así:

## help: print this help message
help:
	@echo &#39;Usage:&#39;
	@sed -n &#39;s/^##//p&#39; ${MAKEFILE_LIST} | column -t -s &#39;:&#39; | sed -e &#39;s/^/  /&#39;

confirm:
	@echo -n &#39;Are you sure? [y/N] &#39; &amp;&amp; read ans &amp;&amp; [ $${ans:-N} = y ]

## run/api: run the cmd/api application
run/api:
	go run ./cmd/api

## db/psql: connect to the database using psql
db/psql:
	psql ${GREENLIGHT_DB_DSN}

## db/migrations/new name=$1: create a new database migration
db/migrations/new:
	@echo &#39;Creating migration files for ${name}...&#39;
	migrate create -seq -ext=.sql -dir=./migrations ${name}

## db/migrations/up: apply all up database migrations
db/migrations/up: confirm
	@echo &#39;Running up migrations...&#39;
	migrate -path ./migrations -database ${GREENLIGHT_DB_DSN} up

Nota: MAKEFILE_LIST es una variable especial que contiene el nombre del makefile que está siendo analizado por make.

También debo señalar que posicionar la regla help como la primera en el Makefile es un movimiento deliberado. Si ejecutas make sin especificar un objetivo, entonces por defecto ejecutará la primera regla en el archivo.

Por lo tanto, esto significa que si intentas ejecutar make sin un objetivo, ahora se te presentará la información de ayuda, así:

$ make
Usage:
help          print this help message
run/api       run the cmd/api application
db/psql       connect to the database using psql
db/migrations/new name=$1 create a new database migration
db/migrations/up apply all up database migrations

19.1 Objectivos ficticios

El objetivo de un Makefile es ayudar a crear archivos en el disco donde el nombre de un objetivo es el nombre de un archivo creado por la regla. Sin embargo, si estamos utilizando make principalmente para ejecutar acciones, esto puede causar un problema si hay un archivo en el directorio del proyecto con el mismo nombre que un objetivo.

Podemos demostrar este problema creando un archivo llamado ./run/api en el directorio raíz de nuestro proyecto:

$ mkdir run &amp;&amp; touch run/api

Entonces, si ejecutas make run/api, en lugar de que nuestra aplicación de API se inicie, obtendrás el siguiente mensaje:

$ make run/api
make: &#39;run/api&#39; is up to date.

Esto se debe a que ya tenemos un archivo en el disco en ./run/api, y la herramienta make considera que esta regla ya se ha ejecutado, por lo que devuelve el mensaje que vemos arriba sin tomar ninguna acción adicional.

Para resolver esto, podemos declarar nuestros objetivos en el makefile como objetivos ficticios (phony targets):

.PHONY: target

target: prerequisite-target-1 prerequisite-target-2 ...
    command
    command
    ...

Hagamos esto para todos nuestros objetivos en el Makefile:

## help: print this help message
.PHONY: help
help:
    @echo &#39;Usage:&#39;
    @sed -n &#39;s/^##//p&#39; ${MAKEFILE_LIST} | column -t -s &#39;:&#39; | sed -e &#39;s/^/  /&#39;

.PHONY: confirm
confirm:
    @echo -n &#39;Are you sure? [y/N] &#39; &amp;&amp; read ans &amp;&amp; [ $${ans:-N} = y ]

## run/api: run the cmd/api application
.PHONY: run/api
run/api:
    go run ./cmd/api

## db/psql: connect to the database using psql
.PHONY: db/psql
db/psql:
    psql ${GREENLIGHT_DB_DSN}

## db/migrations/new name=$1: create a new database migration
.PHONY: db/migrations/new
db/migrations/new:
    @echo &#39;Creating migration files for ${name}...&#39;
    migrate create -seq -ext=.sql -dir=./migrations ${name}

## db/migrations/up: apply all up database migrations
.PHONY: db/migrations/up
db/migrations/up: confirm
    @echo &#39;Running up migrations...&#39;
    migrate -path ./migrations -database ${GREENLIGHT_DB_DSN} up

Si ejecutas make run/api nuevamente ahora, debería reconocer correctamente este objetivo como un objetivo ficticio y ejecutar la regla para nosotros:

$ make run/api
go run ./cmd/api -db-dsn=postgres://greenlight:pa55word@localhost/greenlight
time=2023-09-10T10:59:13.722+02:00 level=INFO msg=&quot;database connection pool established&quot;
time=2023-09-10T10:59:13.722+02:00 level=INFO msg=&quot;starting server&quot; addr=:4000 env=development

Podrías pensar que solo es necesario declarar los objetivos como ficticios si tienes un nombre de archivo en conflicto, pero en la práctica, no declarar un objetivo como ficticio cuando realmente lo es puede conducir a errores o comportamientos confusos. Por ejemplo, imagina si en el futuro alguien crea inconscientemente un archivo llamado confirm en el directorio raíz del proyecto. Esto significaría que nuestra regla confirm nunca se ejecutaría, lo que a su vez llevaría a que se ejecuten reglas peligrosas o destructivas sin confirmación.

Para evitar este tipo de error, si tienes una regla en el makefile que realiza una acción (en lugar de crear un archivo), es mejor adquirir el hábito de declararla como ficticia.

Si estás siguiendo el ejemplo, puedes continuar y eliminar el contenido del directorio run que acabamos de crear. Así:

$ rm -rf run/

En resumen, esto se está desarrollando bien. Nuestro makefile está comenzando a contener algunas funcionalidades útiles, y seguiremos agregando más en los próximos capítulos de este libro. Aunque esta es una de las últimas cosas que estamos haciendo en esta construcción, crear un makefile en el directorio raíz de un proyecto es normalmente una de las primeras cosas que hago al iniciar un proyecto. Encuentro que usar un makefile para tareas comunes ayuda a ahorrar tanto en escritura como en esfuerzo mental durante el desarrollo, y a largo plazo, actúa como un punto de entrada útil y un recordatorio de cómo funcionan las cosas cuando regresas a un proyecto después de un largo período de tiempo.

19.2 Manejando variables de entorno

Utilizando el comando make run/api para ejecutar nuestra aplicación API nos brinda la oportunidad de ajustar nuestras banderas de línea de comandos y eliminar el valor predeterminado para nuestra DSN de base de datos del archivo main.go. De esta manera:

package main

...

func main() {
    var cfg config

    flag.IntVar(&cfg.port, "port", 4000, "API server port")
    flag.StringVar(&cfg.env, "env", "development", "Environment (development|staging|production)")

    // Use the empty string "" as the default value for the db-dsn command-line flag,
    // rather than os.Getenv("GREENLIGHT_DB_DSN") like we were previously.
    flag.StringVar(&cfg.db.dsn, "db-dsn", "", "PostgreSQL DSN")

    ...
}

podemos actualizar nuestro archivo makefile para que el valor DSN de la variable de entorno GREENLIGHT_DB_DSN se pase como parte de la regla. Si estás siguiendo, por favor actualiza la regla run/api de la siguiente manera:

...
## run/api: run the cmd/api application
.PHONY: run/api
run/api:
    go run ./cmd/api -db-dsn=${GREENLIGHT_DB_DSN}
...

Este es un cambio pequeño pero realmente agradable, porque significa que los valores de configuración predeterminados para nuestra aplicación ya no cambian según el entorno operativo. Los valores de las banderas de línea de comandos pasados en tiempo de ejecución son el único mecanismo para configurar los ajustes de nuestra aplicación, y aún no hay secretos codificados en nuestros archivos de proyecto. Durante el desarrollo, ejecutar nuestra aplicación sigue siendo fácil y sencillo; todo lo que necesitamos hacer es escribir make run/api, así:

$ make run/api
go run ./cmd/api -db-dsn=postgres://greenlight:pa55word@localhost/greenlight
time=2023-09-10T10:59:13.722+02:00 level=INFO msg=&quot;database connection pool established&quot;
time=2023-09-10T10:59:13.722+02:00 level=INFO msg=&quot;starting server&quot; addr=:4000 env=development

Consejo: Si no te sientes cómodo con el valor DSN (que contiene una contraseña) siendo mostrado en tu pantalla cuando escribes make run/api, recuerda que puedes usar el carácter @ en tu archivo makefile para suprimir que ese comando sea mostrado.

19.2 Usando .envrc

Si lo prefieres, también podrías eliminar la variable de entorno GREENLIGHT_DB_DSN de tus archivos $HOME/.profile o $HOME/.bashrc, y almacenarla en un archivo .envrc en la raíz de tu directorio de proyecto en su lugar. Si estás siguiendo, adelante y crea un nuevo archivo .envrc así:

$ touch .envrc
export GREENLIGHT_DB_DSN=postgres://greenlight:pa55word@localhost/greenlight

Entonces puedes usar una herramienta como direnv para cargar automáticamente las variables desde el archivo .envrc en tu shell actual, o alternativamente, puedes agregar un comando include en la parte superior de tu Makefile para cargarlas en su lugar. Así:

# Include variables from the .envrc file
include .envrc

## help: print this help message
.PHONY: help
help:
    @echo 'Usage:'
    @sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | \
    sed -e 's/^/ /'

Este enfoque es particularmente conveniente en proyectos donde necesitas hacer cambios frecuentes en tus variables de entorno, porque significa que puedes editar simplemente el archivo .envrc sin necesidad de reiniciar tu computadora o ejecutar source después de cada cambio. Otro beneficio interesante de este enfoque es que proporciona un grado de separación entre variables si estás trabajando en múltiples proyectos en la misma máquina.

Importante: Si utilizas este enfoque y tu archivo .envrc contiene secretos, debes tener cuidado de no poner el archivo en un sistema de control de versiones (como Git o Mercurial).

En unas cuantas capítulos comenzaremos a controlar la versión de nuestra base de código con Git, así que agreguemos preventivamente una regla de ignorar para que el archivo .envrc nunca se cometa.

$ echo &#39;.envrc&#39; &gt;&gt; .gitignore

19.3 Controlando la calidad del codigo

En este capítulo nos centraremos en agregar una regla de auditoría a nuestro Makefile para verificar, probar y limpiar automáticamente nuestra base de código. En particular, la regla realizará lo siguiente:

  • Utilizará el comando go mod tidy para eliminar las dependencias no utilizadas de los archivos go.mod y go.sum, y agregar cualquier dependencia faltante.
  • Utilizará el comando go mod verify para verificar que las dependencias en tu computadora (ubicadas en tu caché de módulos en $GOPATH/pkg/mod) no hayan cambiado desde que se descargaron y que coincidan con los hashes criptográficos en tu archivo go.sum. Ejecutar esto ayuda a garantizar que las dependencias que se están utilizando son exactamente las que esperas.
  • Utilizará el comando go fmt ./... para formatear todos los archivos .go en el directorio del proyecto, de acuerdo con el estándar Go. Esto reformateará los archivos “en su lugar” y mostrará los nombres de cualquier archivo modificado.
  • Utilizará el comando go vet ./... para verificar todos los archivos .go en el directorio del proyecto. La herramienta go vet ejecuta una variedad de analizadores que realizan análisis estático de tu código y te advierten sobre cosas que podrían estar mal pero que no serán detectadas por el compilador, como código inalcanzable, asignaciones innecesarias y etiquetas de compilación mal formadas.
  • Utilizará el comando go test -race -vet=off ./... para ejecutar todas las pruebas en el directorio del proyecto. Por defecto, go test ejecuta automáticamente un pequeño subconjunto de las comprobaciones de go vet antes de ejecutar cualquier prueba, así que para evitar la duplicación usaremos la bandera -vet=off para desactivar esto. La bandera -race habilita el detector de carreras de Go, que puede ayudar a detectar ciertas clases de condiciones de carrera mientras se ejecutan las pruebas.
  • Utilizará la herramienta de análisis estático de terceros staticcheck para llevar a cabo algunas comprobaciones adicionales de análisis estático.

Si estás siguiendo, necesitarás instalar la herramienta staticcheck en tu máquina en este punto. La forma más sencilla de hacerlo es ejecutando el comando go install de la siguiente manera:

$ go install honnef.co/go/tools/cmd/staticcheck@latest
go: downloading honnef.co/go/tools v0.1.3
go: downloading golang.org/x/tools v0.1.0
go: downloading github.com/BurntSushi/toml v0.3.1
go: downloading golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4
go: downloading golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
go: downloading golang.org/x/mod v0.3.0
$ which staticcheck
/home/alex/go/bin/staticcheck

Una vez instalado eso, procedamos a crear una nueva regla de auditoría en nuestro makefile. Mientras lo hacemos, también agreguemos algunos bloques de comentarios para ayudar a organizar y aclarar el propósito de nuestras diferentes reglas en el makefile, de esta manera:

include .envrc

# ==================================================================================== #
# HELPERS
# ==================================================================================== #

## help: print this help message
.PHONY: help
help:
	@echo &#39;Usage:&#39;
	@sed -n &#39;s/^##//p&#39; ${MAKEFILE_LIST} | column -t -s &#39;:&#39; | sed -e &#39;s/^/ /&#39;

.PHONY: confirm
confirm:
	@echo -n &#39;Are you sure? [y/N] &#39; &amp;&amp; read ans &amp;&amp; [ $${ans:-N} = y ]

# ==================================================================================== #
# DEVELOPMENT
# ==================================================================================== #

## run/api: run the cmd/api application
.PHONY: run/api
run/api:
	go run ./cmd/api -db-dsn=${GREENLIGHT_DB_DSN}

## db/psql: connect to the database using psql
.PHONY: db/psql
db/psql:
	psql ${GREENLIGHT_DB_DSN}

## db/migrations/new name=$1: create a new database migration
.PHONY: db/migrations/new
db/migrations/new:
	@echo &#39;Creating migration files for ${name}...&#39;
	migrate create -seq -ext=.sql -dir=./migrations ${name}

## db/migrations/up: apply all up database migrations
.PHONY: db/migrations/up
db/migrations/up: confirm
	@echo &#39;Running up migrations...&#39;
	migrate -path ./migrations -database ${GREENLIGHT_DB_DSN} up

# ==================================================================================== #
# QUALITY CONTROL
# ==================================================================================== #

## audit: tidy dependencies and format, vet and test all code
.PHONY: audit
audit:
	@echo &#39;Tidying and verifying module dependencies...&#39;
	go mod tidy
	go mod verify
	@echo &#39;Formatting code...&#39;
	go fmt ./...
	@echo &#39;Vetting code...&#39;
	go vet ./...
	staticcheck ./...
	@echo &#39;Running tests...&#39;
	go test -race -vet=off ./...

Ahora que eso está hecho, todo lo que necesitas hacer es escribir make audit para ejecutar estas comprobaciones antes de comprometer cualquier cambio de código en tu sistema de control de versiones o construir binarios. Intentémoslo. Si has estado siguiendo de cerca, la salida debería lucir muy similar a esto:

$ make audit
Tidying and verifying module dependencies...
go mod tidy
go: finding module for package gopkg.in/mail.v2
go: downloading gopkg.in/mail.v2 v2.3.1
go: found gopkg.in/mail.v2 in gopkg.in/mail.v2 v2.3.1
go mod verify
all modules verified
Formatting code...
go fmt ./...
Vetting code...
go vet ./...
staticcheck ./...
Running tests...
go test -race -vet=off ./...
?
?greenlight.alexedwards.net/cmd/api
[no test files]
greenlight.alexedwards.net/cmd/examples/cors/preflight [no test files]
?greenlight.alexedwards.net/cmd/examples/cors/simple
?greenlight.alexedwards.net/internal/data
[no test files]
?greenlight.alexedwards.net/internal/jsonlog
[no test files]
?greenlight.alexedwards.net/internal/mailer
[no test files]
?greenlight.alexedwards.net/internal/validator
[no test files]
[no test files]

Eso parece bien. El comando go mod tidy resultó en la necesidad de descargar algunos paquetes adicionales, pero aparte de eso, todas las comprobaciones se completaron correctamente sin problemas.

19.3 Informacion adicional

19.3 testing

Hemos cubierto el tema de las pruebas con mucho detalle en el primer libro “Let’s Go”, y los mismos principios se aplican nuevamente aquí. Si lo deseas, siéntete libre de revisitar la sección de pruebas de “Let’s Go” y volver a implementar algunos de esos mismos patrones en tu API. Por ejemplo, podrías intentar:

  1. Crear una prueba de extremo a extremo para el endpoint GET /v1/healthcheck para verificar que los encabezados y el cuerpo de respuesta sean los que esperas.
  2. Crear una prueba unitaria para el middleware rateLimit() para confirmar que envía una respuesta 429 Too Many Requests después de cierto número de solicitudes.
  3. Crear una prueba de integración de extremo a extremo, utilizando una instancia de base de datos de prueba, que confirme que los middlewares authenticate() y requirePermission() funcionan correctamente juntos para permitir o denegar el acceso a puntos finales específicos.

19.4 Module Proxies and Vendoring

Una de las riesgos de usar paquetes de terceros en tu código de Go es que el repositorio de paquetes puede dejar de estar disponible. Por ejemplo, el paquete httprouter juega un papel central en nuestra aplicación, y si el autor decidiera eliminarlo de GitHub, nos causaría bastante dolor de cabeza tener que apresurarnos a reemplazarlo con una alternativa. (¡No estoy sugiriendo que esto sea probable que suceda con httprouter, solo lo estoy usando como ejemplo!) Afortunadamente, Go proporciona dos maneras en las que podemos mitigar este riesgo: proxies de módulos y vendoring.

19.4 Module Proxies

Go admite proxies de módulos (también conocidos como espejos de módulos) de forma predeterminada. Estos son servicios que reflejan el código fuente desde los repositorios originales y autoritarios (como los alojados en GitHub, GitLab o BitBucket). Adelante y ejecuta el comando go env en tu máquina para imprimir la configuración de tu entorno operativo de Go. La salida debería parecerse a esto:

$ go env
GO111MODULE=&quot;&quot;
GOARCH=&quot;amd64&quot;
GOBIN=&quot;&quot;
GOCACHE=&quot;/home/akerman/.cache/go-build&quot;
GOENV=&quot;/home/akerman/.config/go/env&quot;
GOEXE=&quot;&quot;
GOFLAGS=&quot;&quot;
GOHOSTARCH=&quot;amd64&quot;
GOHOSTOS=&quot;linux&quot;
GOINSECURE=&quot;&quot;
GOMODCACHE=&quot;/home/akerman/go/pkg/mod&quot;
GONOPROXY=&quot;&quot;
GONOSUMDB=&quot;&quot;
GOOS=&quot;linux&quot;
GOPATH=&quot;/home/akerman/go&quot;
GOPRIVATE=&quot;&quot;
GOPROXY=&quot;https://proxy.golang.org,direct&quot;
GOROOT=&quot;/usr/local/go&quot;
GOSUMDB=&quot;sum.golang.org&quot;
GOTMPDIR=&quot;&quot;
GOTOOLDIR=&quot;/usr/local/go/pkg/tool/linux_amd64&quot;
GCCGO=&quot;gccgo&quot;
AR=&quot;ar&quot;
CC=&quot;gcc&quot;
CXX=&quot;g++&quot;
CGO_ENABLED=&quot;1&quot;
GOMOD=&quot;/home/alex/Projects/greenlight/go.mod&quot;
CGO_CFLAGS=&quot;-g -O2&quot;
CGO_CPPFLAGS=&quot;&quot;
CGO_CXXFLAGS=&quot;-g -O2&quot;
CGO_FFLAGS=&quot;-g -O2&quot;
CGO_LDFLAGS=&quot;-g -O2&quot;
PKG_CONFIG=&quot;pkg-config&quot;
GOGCCFLAGS=&quot;...&quot;

Lo importante a tener en cuenta aquí es el ajuste de GOPROXY, que contiene una lista separada por comas de espejos de módulos. De forma predeterminada, tiene el siguiente valor:

GOPROXY=&quot;https://proxy.golang.org,direct&quot;

La URL https://proxy.golang.org que vemos aquí apunta a un espejo de módulos mantenido por el equipo de Go en Google, que contiene copias del código fuente de decenas de miles de paquetes Go de código abierto. Cada vez que descargas un paquete usando el comando go —ya sea con go get o uno de los comandos go mod *—, primero intentará recuperar el código fuente de este espejo. Si el espejo ya tiene una copia almacenada del código fuente para el paquete y la versión requerida, entonces lo devolverá inmediatamente en un archivo zip. De lo contrario, si no está almacenado, entonces el espejo intentará recuperar el código del repositorio autoritario, lo pasará a través del proxy hacia ti y lo almacenará para uso futuro.

Si el espejo no puede recuperar el código en absoluto, entonces devolverá una respuesta de error y la herramienta go volverá a intentar obtener una copia directamente del repositorio autoritario (gracias a la directiva direct en el ajuste de GOPROXY).

Usar un espejo de módulos como la primera ubicación de descarga tiene algunos beneficios:

  • El espejo de módulos https://proxy.golang.org generalmente almacena paquetes a largo plazo, lo que proporciona un grado de protección en caso de que el repositorio original desaparezca de internet.
  • No es posible anular o eliminar un paquete una vez que se almacena en el espejo de módulos https://proxy.golang.org. Esto puede ayudar a prevenir cualquier error o problema que pueda surgir si un autor de paquete (o un atacante) lanza una versión editada del paquete con el mismo número de versión.
  • Obtener módulos del espejo https://proxy.golang.org puede ser mucho más rápido que obtenerlos de los repositorios autoritarios.

En la mayoría de los casos, generalmente sugeriría dejar el ajuste de GOPROXY con sus valores predeterminados. Pero si no deseas utilizar el espejo de módulos proporcionado por Google, o estás detrás de un firewall que lo bloquea, hay otras alternativas como https://goproxy.io y el proporcionado por Microsoft https://athens.azurefd.net que puedes probar en su lugar. O incluso puedes alojar tu propio espejo de módulos utilizando los proyectos de código abierto Athens y goproxy.

Por ejemplo, si quisieras cambiar para usar https://goproxy.io como el espejo principal, luego volver a usar https://proxy.golang.org como espejo secundario, y luego hacer una descarga directa, podrías actualizar tu ajuste de GOPROXY de la siguiente manera:

$ export GOPROXY=https://goproxy.io,https://proxy.golang.org,direct

O si deseas desactivar por completo los espejos de módulos, simplemente puedes establecer el valor en direct de la siguiente manera:

$ export GOPROXY=direct

19.4 vendoring

La funcionalidad de espejo de módulos de Go es excelente y recomiendo utilizarla. Sin embargo, no es una solución milagrosa para todos los desarrolladores y proyectos.

Por ejemplo, tal vez no quieras usar un espejo de módulos proporcionado por Google u otro tercero, pero tampoco quieras la sobrecarga de alojar tu propio espejo. O quizás necesites trabajar rutinariamente en un entorno sin acceso a la red. En esos escenarios, probablemente aún desees mitigar el riesgo de una dependencia que desaparece, pero usar un espejo de módulos no es posible o atractivo.

También debes tener en cuenta que el espejo de módulos por defecto proxy.golang.org no garantiza absolutamente que almacenará una copia del módulo para siempre. Según las preguntas frecuentes:

proxy.golang.org no guarda todos los módulos para siempre. Hay varias razones para esto, pero una de ellas es si proxy.golang.org no puede detectar una licencia adecuada. En este caso, solo se hará disponible una copia temporalmente almacenada del módulo, y podría volverse no disponible si se elimina de la fuente original y queda obsoleta.

Además, si necesitas regresar a una base de código “fría” en 5 o 10 años, ¿el espejo de módulos proxy.golang.org seguirá estando disponible? Con suerte sí, pero es difícil decirlo con certeza.

Por estas razones, aún puede tener sentido incluir las dependencias de tu proyecto utilizando el comando go mod vendor. Al incluir las dependencias de esta manera, básicamente se almacena una copia completa del código fuente de los paquetes de terceros en una carpeta vendor en tu proyecto.

Permíteme mostrarte cómo hacer esto. Comenzaremos adaptando nuestro Makefile para incluir una nueva regla de vendor que llame a los comandos go mod tidy, go mod verify y go mod vendor, de la siguiente manera:

# ==================================================================================== #
# CONTROL DE CALIDAD
# ==================================================================================== #
## audit: limpiar y agregar dependencias y formatear, analizar y probar todo el código
.PHONY: audit
audit: vendor
	@echo &#39;Formateando código...&#39;
	go fmt ./...
	@echo &#39;Analizando código...&#39;
	go vet ./...
	staticcheck ./...
	@echo &#39;Ejecutando pruebas...&#39;
	go test -race -vet=off ./...

## vendor: limpiar y agregar dependencias
.PHONY: vendor
vendor:
	@echo &#39;Limpiando y verificando dependencias del módulo...&#39;
	go mod tidy
	go mod verify
	@echo &#39;Agregando dependencias...&#39;
	go mod vendor

Además de agregar la regla vendor, hay un par de cambios adicionales que hemos realizado aquí:

  • Hemos eliminado los comandos go mod tidy y go mod verify de la regla audit.
  • Hemos agregado la regla vendor como un requisito previo para audit, lo que significa que se ejecutará automáticamente cada vez que ejecutemos la regla audit.

Solo para estar claro sobre lo que está sucediendo detrás de escena aquí, repasemos rápidamente qué sucederá cuando ejecutemos make vendor:

  • El comando go mod tidy asegurará que los archivos go.mod y go.sum enumeren todas las dependencias necesarias para nuestro proyecto (y ninguna innecesaria).
  • El comando go mod verify verificará que las dependencias almacenadas en la caché de su módulo (ubicada en su máquina en $GOPATH/pkg/mod) coincidan con los hashes criptográficos en el archivo go.sum.
  • El comando go mod vendor luego copiará el código fuente necesario de su caché de módulos en un nuevo directorio vendor en la raíz de su proyecto.

Intentemos esto y ejecutemos la nueva regla vendor de la siguiente manera:

$ make vendor
Tidying and verifying module dependencies...
go mod tidy
go mod verify
all modules verified
Vendoring dependencies...
go mod vendor

Una vez completado, verás que se ha creado un nuevo directorio vendor que contiene copias de todo el código fuente junto con un archivo modules.txt. La estructura de directorios en tu carpeta vendor debería parecerse a esto:

$ tree -L 3 ./vendor/
./vendor/
├── github.com
│├── go-mail
││
│├── julienschmidt
││
│└── lib
│
└── mail
└── httprouter
└── pq
├── golang.org
│
└── x
│├── crypto
│└── time
├── gopkg.in
│
│
└── alexcesaro
└── quotedprintable.v3
└── modules.txt

Ahora, cuando ejecutes un comando como go run, go test o go build, la herramienta go reconocerá la presencia de una carpeta vendor y el código de dependencia en la carpeta vendor se usará, en lugar del código en la caché de módulos en tu máquina local.

Si quieres, adelante y prueba ejecutar la aplicación de la API. Deberías encontrar que todo se compila y sigue funcionando como antes.

$ make run/api
go run ./cmd/api -db-dsn=postgres://greenlight:pa55word@localhost/greenlight
time=2023-09-10T10:59:13.722+02:00 level=INFO msg=&quot;database connection pool established&quot;
time=2023-09-10T10:59:13.722+02:00 level=INFO msg=&quot;starting server&quot; addr=:4000 env=development

Nota: Si deseas confirmar que realmente se están utilizando las dependencias vendidas, puedes ejecutar go clean -modcache para eliminar todo de la caché de módulos local. Cuando ejecutes la API nuevamente, deberías encontrar que aún se inicia correctamente sin necesidad de volver a descargar las dependencias desde el espejo de módulos de Go.

Debido a que todo el código fuente de las dependencias ahora está almacenado en el repositorio de tu proyecto, es fácil incluirlo en Git (o en un sistema de control de versiones alternativo) junto con el resto de tu código. Esto es tranquilizador porque te otorga la propiedad completa de todo el código utilizado para construir y ejecutar tus aplicaciones, mantenido bajo control de versiones.

La desventaja de esto, por supuesto, es que agrega tamaño e hinchazón a tu repositorio de proyecto. Esto es de particular preocupación en proyectos que tienen muchas dependencias y el repositorio será clonado muchas veces, como en proyectos donde un sistema CI/CD clona el repositorio con cada nuevo commit.

También echemos un vistazo rápido al archivo vendor/modules.txt que se creó. Si has estado siguiendo, debería verse similar a esto:

# github.com/go-mail/mail/v2 v2.3.0
## explicit
github.com/go-mail/mail/v2
# github.com/julienschmidt/httprouter v1.3.0
## explicit; go 1.7
github.com/julienschmidt/httprouter
# github.com/lib/pq v1.10.9
## explicit; go 1.13
github.com/lib/pq
github.com/lib/pq/oid
github.com/lib/pq/scram
# github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce
## explicit
github.com/tomasen/realip
# golang.org/x/crypto v0.13.0
## explicit; go 1.17
golang.org/x/crypto/bcrypt
golang.org/x/crypto/blowfish
# golang.org/x/time v0.3.0
## explicit
golang.org/x/time/rate
# gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc
## explicit
gopkg.in/alexcesaro/quotedprintable.v3
# gopkg.in/mail.v2 v2.3.1
## explicit

Este archivo vendor/modules.txt es esencialmente un manifiesto de los paquetes vendidos y sus números de versión. Cuando se está utilizando el vendoring, la herramienta go verificará que los números de versión del módulo en modules.txt sean consistentes con los números de versión en el archivo go.mod. Si hay alguna inconsistencia, entonces la herramienta go informará un error.

Nota: Es importante señalar que no hay una manera fácil de verificar que los checksums de las dependencias vendidas coincidan con los checksums en el archivo go.sum. O, en otras palabras, no hay un equivalente a go mod verify que funcione directamente en el contenido de la carpeta vendor. Para mitigar esto, es una buena idea ejecutar regularmente tanto go mod verify como go mod vendor. Usar go mod verify verificará que las dependencias en la caché de módulos coincidan con el archivo go.sum, y go mod vendor copiará esas mismas dependencias desde la caché de módulos hacia tu carpeta vendor. Esta es una de las razones por las cuales nuestra regla make vendor está configurada para ejecutar ambos comandos, y por qué también lo hemos incluido como un requisito previo para la regla make audit.

Por último, debes evitar hacer cambios en el código dentro del directorio vendor. Hacerlo puede potencialmente causar confusión (porque el código ya no sería consistente con la versión original del código fuente) y, además, ejecutar go mod vendor sobrescribirá cualquier cambio que hagas cada vez que lo ejecutes. Si necesitas cambiar el código de una dependencia, es mucho mejor bifurcarlo y luego importar la versión bifurcada en su lugar.

19.4 Vendoring new dependencies

En la próxima sección del libro, vamos a implementar nuestra aplicación de API en internet con Caddy como un proxy inverso delante de ella. Esto significa que, según nuestra API, todas las solicitudes que reciba vendrán de una única dirección IP (la que ejecuta la instancia de Caddy). A su vez, esto causará problemas para nuestro middleware de limitación de velocidad de acceso que limita el acceso basado en la dirección IP.

Afortunadamente, como la mayoría de los otros proxies inversos, Caddy agrega un encabezado X-Forwarded-For a cada solicitud. Este encabezado contendrá la dirección IP real del cliente.

Aunque podríamos escribir la lógica para verificar la presencia de un encabezado X-Forwarded-For y manejarlo nosotros mismos, recomiendo usar el paquete realip para ayudar con esto. Este paquete recupera la dirección IP del cliente de cualquier encabezado X-Forwarded-For o X-Real-IP, y si ninguno de ellos está presente, utiliza r.RemoteAddr.

Si estás siguiendo, adelante e instala la última versión de realip usando el comando go get:

$ go get github.com/tomasen/realip@latest
go: downloading github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce
go get: added github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce

Luego, abre el archivo cmd/api/middleware.go y actualiza el middleware rateLimit() para usar este paquete de la siguiente manera:

package main

import (
    "errors"
    "expvar"
    "fmt"
    "net/http"
    "strconv"
    "strings"
    "sync"
    "time"

    "greenlight.alexedwards.net/internal/data"
    "greenlight.alexedwards.net/internal/validator"

    "github.com/tomasen/realip" // New import
    "golang.org/x/time/rate"
)

func (app *application) rateLimit(next http.Handler) http.Handler {
    // ...
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if app.config.limiter.enabled {
            // Use the realip.FromRequest() function to get the client's real IP address.
            ip := realip.FromRequest(r)
            mu.Lock()
            if _, found := clients[ip]; !found {
                clients[ip] = &client{
                    limiter: rate.NewLimiter(rate.Limit(app.config.limiter.rps), app.config.limiter.burst),
                }
            }
            clients[ip].lastSeen = time.Now()
            if !clients[ip].limiter.Allow() {
                mu.Unlock()
                app.rateLimitExceededResponse(w, r)
                return
            }
            mu.Unlock()
        }
        next.ServeHTTP(w, r)
    })
}

Si intentas ejecutar la aplicación de la API nuevamente ahora, deberías recibir un mensaje de error similar a este:

$ make run/api
go: inconsistent vendoring in /home/alex/Projects/greenlight:
github.com/tomasen/realip@v0.0.0-20180522021738-f0c99a92ddce: is explicitly
required in go.mod, but not marked as explicit in vendor/modules.txt
To ignore the vendor directory, use -mod=readonly or -mod=mod.
To sync the vendor directory, run:
go mod vendor
make: *** [Makefile:24: run/api] Error 11

Básicamente, lo que está sucediendo aquí es que Go está buscando el paquete github.com/tomasen/realip en nuestro directorio vendor, pero en este momento ese paquete no existe allí.

Para resolver esto, deberás ejecutar manualmente alguno de los comandos make vendor o make audit, así:

$ make run/api
go run ./cmd/api -db-dsn=postgres://greenlight:pa55word@localhost/greenlight
time=2023-09-10T10:59:13.722+02:00 level=INFO msg="database connection pool established"
time=2023-09-10T10:59:13.722+02:00 level=INFO msg="starting server" addr=:4000 env=development

19.4 Informacion adicional

19.4 El patron ./…

La mayoría de las herramientas de Go admiten el patrón de comodín ./..., como go fmt ./..., go vet ./... y go test ./.... Este patrón coincide con el directorio actual y todos los subdirectorios, excluyendo el directorio vendor.

En general, esto es útil porque significa que no estamos formateando, analizando o probando el código en nuestro directorio vendor innecesariamente, y nuestra regla make audit no fallará debido a problemas que puedan existir dentro de esos paquetes vendor.

19.5 Construyendo binarios

Hasta ahora hemos estado ejecutando nuestra API usando el comando go run (o más recientemente, make run/api). Pero en este capítulo vamos a centrarnos en explicar cómo construir un ejecutable binario que puedas distribuir y ejecutar en otras máquinas sin necesidad de tener instalada la cadena de herramientas de Go.

Para construir un binario necesitamos usar el comando go build. Como ejemplo simple, el uso se ve así:

$ go build -o=./bin/api ./cmd/api

Cuando ejecutamos este comando, go build compilará el paquete cmd/api (y cualquier otro paquete dependiente) en archivos que contienen código máquina, y luego los enlazará para formar un ejecutable binario. En el comando anterior, el binario ejecutable se generará en ./bin/api.

Para mayor comodidad, agreguemos una nueva regla build/api a nuestro archivo makefile que ejecute este comando, de la siguiente manera:

...
# ==================================================================================== #
# BUILD
# ==================================================================================== #
## build/api: build the cmd/api application
.PHONY: build/api
build/api:
@echo &#39;Building cmd/api...&#39;
go build -o=./bin/api ./cmd/api

Una vez hecho eso, procede a ejecutar la regla make build/api. Deberías ver que se crea un archivo binario ejecutable en ./bin/api.

$ make build/api
Building cmd/api...
go build -o=./bin/api ./cmd/api
$ ls -l ./bin/
total 10228
-rwxrwxr-x 1 alex alex 10470419 Apr 18 16:05 api

Y deberías poder ejecutar este ejecutable para iniciar tu aplicación de API, pasando los valores de las banderas de línea de comandos que sean necesarios. Por ejemplo:

$ ./bin/api -port=4040 -db-dsn=postgres://greenlight:pa55word@localhost/greenlight
time=2023-09-10T10:59:13.722+02:00 level=INFO msg=&quot;database connection pool established&quot;
time=2023-09-10T10:59:13.722+02:00 level=INFO msg=&quot;starting server&quot; addr=:4040 env=development

19.5 Reduciendo el tamanio del binario.

Si observas más de cerca el binario ejecutable, verás que tiene un peso de 10470419 bytes (aproximadamente 10.5 MB).

$ ls -l ./bin/api
-rwxrwxr-x 1 alex alex 10470419 Apr 18 16:05 ./bin/api

Nota: Si estás siguiendo los pasos, el tamaño de tu binario puede ser ligeramente diferente. Depende de tu sistema operativo y de la versión exacta de Go y de las dependencias de terceros que estés utilizando.

Es posible reducir el tamaño del binario en aproximadamente un 25% al instruir al enlazador de Go para que elimine la información de depuración DWARF y la tabla de símbolos del binario. Podemos hacer esto como parte del comando go build utilizando la bandera del enlazador -ldflags="-s" de la siguiente manera:

Si ejecutas nuevamente make build/api, ahora deberías encontrar que el tamaño del binario se reduce a menos de 8 MB.

$ make build/api
Building cmd/api...
go build -ldflags=&#39;-s&#39; -o=./bin/api ./cmd/api
$ ls -l ./bin/api
-rwxrwxr-x 1 alex alex 7618560 Apr 18 16:08 ./bin/api

Es importante tener en cuenta que eliminar la información DWARF y la tabla de símbolos hará que sea más difícil depurar un ejecutable utilizando una herramienta como Delve o gdb. Sin embargo, en general, no es frecuente que necesites hacer esto, e incluso hay una propuesta abierta de Rob Pike para hacer que omitir la información DWARF sea el comportamiento predeterminado del enlazador en el futuro.

19.5 Cross-compilation

Por defecto, el comando go build generará un binario adecuado para usar en el sistema operativo y la arquitectura de tu máquina local. Pero también admite la compilación cruzada, lo que te permite generar un binario adecuado para usar en una máquina diferente. Esto es particularmente útil si estás desarrollando en un sistema operativo y desplegando en otro.

Para ver una lista de todas las combinaciones de sistemas operativos y arquitecturas que Go admite, puedes ejecutar el comando go tool dist list de la siguiente manera:

$ go tool dist list
aix/ppc64
android/386
android/amd64
android/arm
android/arm64
darwin/amd64
...

Y puedes especificar el sistema operativo y la arquitectura para los cuales deseas crear el binario configurando las variables de entorno GOOS y GOARCH al ejecutar go build. Por ejemplo:

$ GOOS=linux GOARCH=amd64 go build {args}

En la próxima sección del libro, vamos a explicar cómo desplegar un binario ejecutable en un servidor Ubuntu Linux alojado por Digital Ocean. Para esto, necesitaremos un binario diseñado para ejecutarse en una máquina con una combinación de sistema operativo y arquitectura linux/amd64.

Entonces, actualicemos nuestra regla make build/api para que cree dos binarios: uno para usar en tu máquina local y otro para desplegar en el servidor Ubuntu Linux.

...
# ==================================================================================== #
# BUILD
# ==================================================================================== #
## build/api: build the cmd/api application
.PHONY: build/api
build/api:
@echo &#39;Building cmd/api...&#39;
go build -ldflags=&#39;-s&#39; -o=./bin/api ./cmd/api
GOOS=linux GOARCH=amd64 go build -ldflags=&#39;-s&#39; -o=./bin/linux_amd64/api ./cmd/api

Si estás siguiendo, procede a ejecutar make build/api nuevamente. Deberías ver que ahora se crean dos binarios, con el binario compilado cruzado ubicado en el directorio ./bin/linux_amd64, de la siguiente manera:

$ make build/api
Building cmd/api...
go build -ldflags=&#39;-s&#39; -o=./bin/api ./cmd/api
GOOS=linux GOARCH=amd64 go build -ldflags=&#39;-s&#39; -o=./bin/linux_amd64/api ./cmd/api
$ tree ./bin
./bin
├── api
└── linux_amd64
└── api

Como regla general, probablemente no quieras incluir tus binarios de Go en el control de versiones junto con tu código fuente, ya que aumentarán significativamente el tamaño de tu repositorio. Por lo tanto, si estás siguiendo estos pasos, agreguemos rápidamente una regla adicional al archivo .gitignore que instruya a Git a ignorar el contenido del directorio bin.

$ echo &#39;bin/&#39; &gt;&gt; .gitignore
$ cat .gitignore
.envrc
bin/

19.5 Informacion adicional

19.5 Building caching

Es importante tener en cuenta que el comando go build almacena en caché la salida de la compilación en la caché de compilación de Go. Esta salida en caché se reutilizará en futuras compilaciones cuando sea apropiado, lo que puede acelerar significativamente el tiempo de compilación total de tu aplicación.

Si no estás seguro de dónde está tu caché de compilación, puedes verificarlo ejecutando el comando go env GOCACHE:

$ go env GOCACHE
/home/akerman/.cache/go-build

También debes tener en cuenta que la caché de compilación no detecta automáticamente ningún cambio en las bibliotecas C que tu código importa con cgo. Por lo tanto, si has cambiado una biblioteca C desde la última compilación, necesitarás usar la bandera -a para forzar la reconstrucción de todos los paquetes al ejecutar go build. Alternativamente, podrías usar go clean para purgar la caché:

$ go build -a -o=/bin/foo ./cmd/foo# Force all packages to be rebuilt
$ go clean -cache# Remove everything from the build cache

Nota: Si alguna vez ejecutas go build en un paquete que no sea main, la salida de compilación se almacenará en la caché de compilación para que pueda ser reutilizada, pero no se producirá ningún ejecutable.

19.6 Manejando y automatizando numero de versiones

Justo al inicio de este libro, codificamos el número de versión de nuestra aplicación como la constante “1.0.0” en el archivo cmd/api/main.go.

En este capítulo, vamos a tomar medidas para facilitar la visualización y gestión de este número de versión, y también explicaremos cómo puedes generar números de versión automáticamente basados en los commits de Git e integrarlos en tu aplicación.

19.6 Mostrando el numero de version

Comencemos actualizando nuestra aplicación para que podamos verificar fácilmente el número de versión ejecutando el binario con una bandera de línea de comandos -version, similar a esto:

$ ./bin/api -version
Version:
1.0.0

Conceptualmente, esto es bastante sencillo de implementar. Necesitamos definir una bandera de línea de comandos booleana version, verificar esta bandera al iniciar la aplicación, y luego imprimir el número de versión y salir de la aplicación si es necesario.

Si estás siguiendo estos pasos, procede a actualizar tu archivo cmd/api/main.go de la siguiente manera:

package main

import (
    "context"
    "database/sql"
    "expvar"
    "flag"
    "fmt" // New import
    "log/slog"
    "os"
    "runtime"
    "strings"
    "sync"
    "time"

    "greenlight.alexedwards.net/internal/data"
    "greenlight.alexedwards.net/internal/mailer"
    _ "github.com/lib/pq"
)

const version = "1.0.0"

// Rest of the code here...

func main() {
    var cfg config
    flag.IntVar(&cfg.port, "port", 4000, "API server port")
    flag.StringVar(&cfg.env, "env", "development", "Environment (development|staging|production)")
    flag.StringVar(&cfg.db.dsn, "db-dsn", "", "PostgreSQL DSN")
    flag.IntVar(&cfg.db.maxOpenConns, "db-max-open-conns", 25, "PostgreSQL max open connections")
    flag.IntVar(&cfg.db.maxIdleConns, "db-max-idle-conns", 25, "PostgreSQL max idle connections")
    flag.DurationVar(&cfg.db.maxIdleTime, "db-max-idle-time", 15*time.Minute, "PostgreSQL max connection idle time")
    flag.BoolVar(&cfg.limiter.enabled, "limiter-enabled", true, "Enable rate limiter")
    flag.Float64Var(&cfg.limiter.rps, "limiter-rps", 2, "Rate limiter maximum requests per second")
    flag.IntVar(&cfg.limiter.burst, "limiter-burst", 4, "Rate limiter maximum burst")
    flag.StringVar(&cfg.smtp.host, "smtp-host", "sandbox.smtp.mailtrap.io", "SMTP host")
    flag.IntVar(&cfg.smtp.port, "smtp-port", 25, "SMTP port")
    flag.StringVar(&cfg.smtp.username, "smtp-username", "a7420fc0883489", "SMTP username")
    flag.StringVar(&cfg.smtp.password, "smtp-password", "e75ffd0a3aa5ec", "SMTP password")
    flag.StringVar(&cfg.smtp.sender, "smtp-sender", "Greenlight <no-reply@greenlight.alexedwards.net>", "SMTP sender")
    flag.Func("cors-trusted-origins", "Trusted CORS origins (space separated)", func(val string) error {
        cfg.cors.trustedOrigins = strings.Fields(val)
        return nil
    })

    // Create a new version boolean flag with the default value of false.
    displayVersion := flag.Bool("version", false, "Display version and exit")
    flag.Parse()

    // If the version flag value is true, then print out the version number and
    // immediately exit.
    if *displayVersion {
        fmt.Printf("Version:\t%s\n", version)
        os.Exit(0)
    }

    // Rest of the code here...
}


Vale, probemos esto. Adelante y reconstruye los binarios ejecutables usando make build/api, luego ejecuta el binario ./bin/api con la bandera -version. Deberías encontrar que imprime el número de versión y luego sale, similar a esto:

$ make build/api
Building cmd/api...
go build -ldflags="-s" -o="./bin/api" ./cmd/api
GOOS=linux GOARCH=amd64 go build -ldflags="-s" -o="./bin/linux_amd64/api" ./cmd/api
$ ./bin/api -version
Version:
1.0.0

Recuerda: Las banderas de línea de comandos booleanas sin un valor se interpretan como teniendo el valor verdadero (true). Entonces, ejecutar nuestra aplicación con -version es lo mismo que ejecutarla con -version=true.

19.6 Automatizando numero de version con Git

Importante: Esta parte del libro solo es realmente relevante si usas Git o un sistema de control de versiones similar. Si no lo haces, eso está perfectamente bien y puedes avanzar al siguiente capítulo sin problemas.

Desde la versión 1.18, Go ahora incrusta información de control de versiones en tus binarios ejecutables cuando ejecutas go build en un paquete principal que está rastreado con Git, Mercurial, Fossil o Bazaar.

Hay dos formas de acceder a esta información de control de versiones: ya sea utilizando el comando go version -m en tu binario, o desde dentro del código de tu aplicación llamando a debug.ReadBuildInfo().

Veamos ambos enfoques.

Si estás siguiendo (y aún no lo has hecho), por favor procede a inicializar un nuevo repositorio Git en la raíz de tu directorio de proyecto:

$ git init
Initialized empty Git repository in /home/alex/Projects/greenlight/.git/

Luego, realiza un nuevo commit que contenga todos los archivos en tu directorio de proyecto, de la siguiente manera:

$ git add .
$ git commit -m "Initial commit"

Si observas el historial de tus commits usando el comando git log, verás el hash para este commit.

$ git log
commit 59bdb76fda0c15194ce18afae5d4875237f05ea9 (HEAD -> master)
Author: akerman <nahuel@nahueldev23.net>
Date:
Wed Feb 22 18:14:42 2023 +0100
Initial commit

En mi caso, el hash del commit es 59bdb76fda0c15194ce18afae5d4875237f05ea9, pero es muy probable que el tuyo sea un valor diferente.

A continuación, vuelve a ejecutar make build para generar un nuevo binario y luego usa el comando go version -m en él. Así:

$ make build/api
Building cmd/api...
go build -ldflags="-s" -o=./bin/api ./cmd/api
GOOS=linux GOARCH=amd64 go build -ldflags="-s" -o=./bin/linux_amd64/api ./cmd/api

$ go version -m ./bin/api
./bin/api: go1.21.0
    path    greenlight.alexedwards.net/cmd/api
    mod     greenlight.alexedwards.net (devel)
    dep     github.com/go-mail/mail/v2        v2.3.0
    dep     github.com/julienschmidt/httprouter
    dep     github.com/lib/pq
    dep     github.com/tomasen/realip
    dep     golang.org/x/crypto                v0.13.0         h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
    dep     golang.org/x/time                  v0.3.0
    build   buildmode=exe compiler=gc -ldflags=-s CGO_ENABLED=1 CGO_CFLAGS= CGO_CPPFLAGS= CGO_CXXFLAGS= CGO_LDFLAGS= GOARCH=amd64 GOOS=linux GOAMD64=v1 vcs=git vcs.revision=3f5ab2cbaaf4bf7c936d03a1984d4abc08e8c6d3 vcs.time=2023-09-10T06:37:03Z vcs.modified=true
    h1      wha99yf2v3cpUzD1V9ujP404Jbw2uEvs+rBJybkdYcw=   v1.3.0
    h1      U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
    h1      YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=   v1.10.9
    h1      fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc=
    h1      rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=

La salida de go version -m nos muestra información interesante sobre el binario. Podemos ver la versión de Go con la que se construyó (go1.21.0 en mi caso), las dependencias del módulo y la información sobre la configuración de la compilación, incluidas las banderas del enlazador utilizadas y el sistema operativo y la arquitectura para los que se construyó.

Sin embargo, lo que nos interesa más en este momento son los ajustes de compilación vcs en la parte inferior:

  • vcs=git nos indica que el sistema de control de versiones utilizado es Git.
  • vcs.revision es el hash para el último commit de Git.
  • vcs.time es el momento en que se realizó este commit.
  • vcs.modified nos indica si el código rastreado por el repositorio de Git ha sido modificado desde que se realizó el commit. Un valor de false indica que el código no ha sido modificado, lo que significa que el binario se construyó utilizando el código exacto del commit vcs.revision. Un valor de true indica que el repositorio de control de versiones estaba “sucio” cuando se construyó el binario, y el código utilizado para construir el binario puede no ser el código exacto del commit vcs.revision.

Como mencioné brevemente antes, toda la información que ves en la salida de go version -m también está disponible para ti en tiempo de ejecución.

Aprovechemos esto y adaptemos nuestro archivo main.go para que el valor de la versión se establezca en el hash del commit de Git, en lugar de la constante codificada “1.0.0”.

Para ayudar con esto, crearemos un pequeño paquete internal/vcs que genere un número de versión para nuestra aplicación basado en el hash del commit de vcs.revision más un sufijo opcional -dirty si vcs.modified=true. Para hacer eso, necesitaremos:

  • Llamar a la función debug.ReadBuildInfo(). Esto devolverá una estructura debug.BuildInfo que contiene esencialmente la misma información que vimos al ejecutar el comando go version -m.
  • Recorrer el campo debug.BuildInfo.Settings para extraer los valores de vcs.revision y vcs.modified.

asi :

$ mkdir internal/vcs
$ touch internal/vcs/vcs.go
package vcs

import (
    "fmt"
    "runtime/debug"
)

func Version() string {
    var revision string
    var modified bool

    bi, ok := debug.ReadBuildInfo()
    if ok {
        for _, s := range bi.Settings {
            switch s.Key {
            case "vcs.revision":
                revision = s.Value
            case "vcs.modified":
                if s.Value == "true" {
                    modified = true
                }
            }
        }
    }

    if modified {
        return fmt.Sprintf("%s-dirty", revision)
    }
    return revision
}

Ahora que eso está en su lugar, volvamos a nuestro archivo main.go y actualicémoslo para establecer el número de versión usando esta nueva función vcs.Version():

package main

import (
    "context"
    "database/sql"
    "expvar"
    "flag"
    "fmt"
    "log/slog"
    "os"
    "runtime"
    "strings"
    "sync"
    "time"

    "greenlight.alexedwards.net/internal/data"
    "greenlight.alexedwards.net/internal/mailer"
    "greenlight.alexedwards.net/internal/vcs" // New import

    _ "github.com/lib/pq"
)

// Make version a variable (rather than a constant) and set its value to vcs.Version().
var (
    version = vcs.Version()
)

...

De acuerdo, vamos a probar esto. Adelante y reconstruye el binario nuevamente…

$ make build/api
Building cmd/api...
go build -ldflags=&quot;-s&quot; -o=./bin/api ./cmd/api
GOOS=linux GOARCH=amd64 go build -ldflags=&quot;-s&quot; -o=./bin/linux_amd64/api ./cmd/api

Y luego ejecútalo con la bandera -version:

$ ./bin/api -version
Version:
59bdb76fda0c15194ce18afae5d4875237f05ea9-dirty

En mi caso, podemos ver que el número de versión reportado es 59bdb76fda0c15194ce18afae5d4875237f05ea9-dirty. Eso tiene sentido: el último hash del commit fue 59bdb76fda0c15194ce18afae5d4875237f05ea9 y hemos cambiado la base de código desde ese commit, por lo que incluye el sufijo -dirty.

Vamos a solucionar eso haciendo commit de nuestros cambios recientes…

$ git add .
$ git commit -m &quot;Generate version number automatically&quot;

Y cuando reconstruyas el binario y verifiques el número de versión nuevamente, deberías ver un nuevo número de versión sin el sufijo -dirty, similar a esto:

$ make build/api
Building cmd/api...
go build -ldflags=&quot;-s&quot; -o=./bin/api ./cmd/api
GOOS=linux GOARCH=amd64 go build -ldflags=&quot;-s&quot; -o=./bin/linux_amd64/api ./cmd/api
$ ./bin/api -version
Version:
1c9b6ff48ea800acdf4f5c6f5c3b62b98baf2bd7

Importante: La información de control de versiones solo se incrusta de forma predeterminada cuando ejecutas go build. Nunca se incrusta cuando usas go run, y solo se incrusta al usar go test si usas la bandera -buildvcs=true. Puedes ver este comportamiento en acción si usas go run junto con nuestra bandera -version; no se incrusta información de control de versiones y la cadena de versión permanece en blanco.

$ go run ./cmd/api/ -version

Pero en general, esto es realmente bueno. El número de versión de nuestra aplicación ahora se alinea con el historial de commits en nuestro repositorio de Git, lo que significa que es fácil identificar exactamente qué código contiene un binario específico o qué está utilizando una aplicación en ejecución. Todo lo que necesitamos hacer es ejecutar el binario con la bandera -version, o llamar al punto final de comprobación de salud, y luego cruzar la referencia del número de versión con el historial del repositorio de Git.

19.6 Informacion adicional

19.6 Including commit time in the version number

Si quieres, podrías extender la función vcs.Version() para incluir también la hora del commit en el número de versión.

func Version() string {
    var (
        time     string
        revision string
        modified bool
    )

    bi, ok := debug.ReadBuildInfo()
    if ok {
        for _, s := range bi.Settings {
            switch s.Key {
            case "vcs.time":
                time = s.Value
            case "vcs.revision":
                revision = s.Value
            case "vcs.modified":
                if s.Value == "true" {
                    modified = true
                }
            }
        }
    }

    if modified {
        return fmt.Sprintf("%s-%s-dirty", time, revision)
    }
    return fmt.Sprintf("%s-%s", time, revision)
}

Realizar ese cambio resultaría en números de versión que se ven similares a esto:

2022-04-30T10:16:24Z-1c9b6ff48ea800acdf4f5c6f5c3b62b98baf2bd7-dirty
19.6 Using linker flags

Antes de Go 1.18, la forma idiomática de administrar los números de versión automáticamente era “incrustar” el número de versión al compilar el binario usando la bandera del enlazador -X. El uso de debug.ReadBuildInfo() es ahora el método preferido, pero el enfoque antiguo aún puede ser útil si necesita establecer el número de versión en algo que no está disponible a través de debug.ReadBuildInfo().

Por ejemplo, si desea establecer el número de versión en el valor de una variable de entorno VERSION en la máquina que construye el binario, podría usar la bandera del enlazador -X para “incrustar” este valor en la variable main.version. Así:

## build/api: build the cmd/api application
.PHONY: build/api
build/api:
  @echo &#39;Building cmd/api...&#39;
  go build -ldflags=&#39;-s -X main.version=${VERSION}&#39; -o=./bin/api ./cmd/api
  GOOS=linux GOARCH=amd64 go build -ldflags=&#39;-s -X main.version=${VERSION}&#39; -o=./bin/linux_amd64/api ./cmd/api

Post Relacionados