En Cloudflare nos gusta Go. Lo utilizamos en muchos proyectos de software propios y también como parte de sistemas de canalización más grandes. Pero, ¿podemos llevar a Go al siguiente nivel y usarlo como lenguaje de scripting para nuestro sistema operativo favorito, Linux?

gopher-tux-1
Imagen de una taltuza CC BY 3.0 Renee French Imagen de Tux CC0 BY OpenClipart-Vectores

Por qué considerar a Go como un lenguaje de scripting

Respuesta corta: ¿por qué no? Go es relativamente fácil de usar, no demasiado ampuloso y hay un enorme ecosistema de bibliotecas que se pueden reutilizar para evitar desarrollar todo el código desde cero. Algunas otras ventajas potenciales que podría traer aparejadas:

  • Sistema de compilación en base a Go para su proyecto Go: el comandogo build  es principalmente adecuado para proyectos pequeños y autónomos. Los proyectos más complejos suelen adoptar algún sistema/conjunto de scripts de compilación. ¿Por qué no tener entonces estos scripts también en lenguaje Go?
  • Fácil administración de paquetes, sin privilegios, listos para usar: si desea utilizar una biblioteca de terceros en su script, simplemente puede obtenerla. Y como el código se instalará en su GOPATH, acceder a la librería de un tercero no requiere de privilegios administrativos en el sistema (a diferencia de otros lenguajes de scripting). Esto resulta especialmente útil en grandes entornos corporativos.
  • Prototipado de código rápido en las primeras etapas del proyecto: cuando se desarrolla la primera iteración del código, por lo general se necesita una gran cantidad de ediciones incluso para que se compile, y debe hacer una gran cantidad de pulsaciones de teclas en el ciclo "edit->build->check". En su lugar, puede omitir la parte de “build” (“crear”) y ejecutar inmediatamente el archivo de origen.
  • Lenguaje de scripting con estrictas restricciones respecto a la combinación de valores: si uno comete un pequeño error tipográfico en algún lugar del script, la mayoría de los scripts ejecutarán todo hasta ese punto y fallarán en el error tipográfico. Esto podría dejar el sistema en un estado de inconsistencia. Con este lenguaje de scripting con estrictas restricciones respeto a la combinación de valores, muchos errores tipográficos se pueden detectar en el momento de la compilación, por lo tanto, el script con el error no se ejecutará en primer lugar.

Estado actual del scripting Go

A primera vista, los scripts Go parecen fáciles de implementar con el soporte de las líneas shebang de Unix para scripts. Una línea de shebang es la primera línea del script, que comienza con #! y especifica el intérprete de script que se utilizará para ejecutar el script (por ejemplo,#!/bin/bash o #!/usr/bin/env python), de manera que el sistema sabe exactamente cómo ejecutar el script independientemente del lenguaje de programación que se utiliza. Y Go ya es compatible con la invocación similar a la de un intérprete para archivos.go con comando go run, de manera que solo sería cuestión de agregar una línea de shebang adecuada, como #!/usr/bin/env go run, a cualquier archivo .go, estableciendo el bit ejecutable y ya está listo para usar.

Sin embargo, surgen problemas al usar go run directamente. Esta excelente publicacióndescribe en detalle todos los problemas relacionados congo run y las posibles soluciones, pero el punto esencial es:

  • go run no arroja correctamente el código de error de script al sistema operativo y esto es importante para los scripts, ya que los códigos de error son una de las formas más comunes de interacción entre varios scripts entre sí y con el entorno del sistema operativo.
  • no puede haber una línea de shebang en un archivo válido .go, ya que Go no sabe cómo procesar las líneas que comienzan con #. Otros lenguajes de scripting no tienen este problema, porque para la mayoría de ellos #es una manera de especificar comentarios, de modo que el intérprete final simplemente ignora la línea de shebang, pero los comentarios de Go que comienzan con // y go run en la invocación solo generarán un error como:
package main:
helloscript.go:1:1: illegal character U+0023 '#'

La publicación describe varias posibles soluciones para los problemas planteados, incluso la utilización de un gorun de un programa wrapper personalizado como intérprete, pero ninguno de ellos no proporciona una solución ideal. Usted tienen que hacer alguna de estas dos cosas:

  • utilizar la línea de shebang no estándar, que comienza con //. Esto técnicamente ni siquiera es una línea de shebang, sino la forma en que una shell de bash procesa los archivos de texto ejecutables, de manera que esta solución es específica de bash. Además, debido al comportamiento específico de go run, esta línea es bastante compleja y no es obvia (ver la publicación original para obtener ejemplos).
  • utilizar un gorun de un programa wrapper personalizado en la línea de shebang, que funcione bien, sin embargo, termina con archivos .go, que no se compilan con el comando estándar go build por el carácter ilegal #.

Cómo Linux ejecuta archivos

Bien, parece que el enfoque shebang no nos brinda una solución integral. ¿Hay algo más que podamos usar? Analicemos en más detalle cómo el núcleo de Linux ejecuta archivos binarios en primer lugar. Cuando usted intenta ejecutar un archivo binario/script (o cualquier archivo para el caso que tiene un bit ejecutable establecido), su shell finalmente usará execve system callde Linux transmitiendo la ruta del sistema de archivos del archivo binario en cuestión, los parámetros de línea de comandos y las variables de entorno definidas actualmente. A continuación, el núcleo se encarga del análisis correcto del archivo y de la creación de un nuevo proceso con el código del archivo. La mayoría de nosotros sabemos que Linux (y muchos otros sistemas operativos tipo Unix) utilizan el formato binario ELF para sus ejecutables.

Sin embargo, uno de los principios básicos del desarrollo del núcleo de Linux es evitar el “bloqueo de proveedor/formato” para cualquier subsistema que forma parte del núcleo. Por lo tanto, Linux implementa un sistema “conectable”, que permite que cualquier formato binario sea compatible con el núcleo. Todo lo que tiene que hacer es desarrollar un módulo correcto, que puede analizar el formato de su elección. Y si estudia más detenidamente el código de origen del núcleo, verá que Linux admite más formatos binarios estándar. Por ejemplo, para el reciente núcleo de Linux4.14 podemos observar que admite, por lo menos, 7 formatos binarios (los módulos in-tree (formato de árbol) para varios formatos binarios, por lo general, tienen el prefijo binfmt_ en sus nombres). Vale la pena tener en cuenta el módulo binfmt_script, que se encarga de analizar las líneas de shebang mencionadas anteriormente y ejecutar scripts en el sistema de destino (no todo el mundo sabe que la compatibilidad de shebang es realmente implementada en el núcleo en sí y no en la shell u otro daemon/proceso).

Ampliación de los formatos binarios compatibles desde el espacio del usuario

Pero como llegamos a la conclusión de que shebang no es la mejor opción para nuestro scripting Go, parece que es necesario algo más. Sorprendentemente, el núcleo de Linux ya tiene ¨algo más¨que un módulo de soporte binario, que tiene un nombre apropiadobinfmt_misc. El módulo permite que un administrador agregue soporte de manera dinámica para varios formatos ejecutables directamente desde el espacio del usuario a través de una interfaz procfs bien definida y está bien documentado.

Sigamos la documentación e intentemos configurar una descripción de formato binario para archivos .go. En primer lugar, la guía le indica montar el sistema de archivos especialbinfmt_misc en /proc/sys/fs/binfmt_misc. Si utiliza una distribución de Linux basada en systemd relativamente reciente, es muy probable que el sistema de archivos ya esté montado, porque el systemd predeterminado instala unidades especiales montadas y montadas automáticamente con este propósito. Para hacer un comprobación minuciosa, simplemente ejecute:

$ mount | grep binfmt_misc
systemd-1 on /proc/sys/fs/binfmt_misc type autofs (rw,relatime,fd=27,pgrp=1,timeout=0,minproto=5,maxproto=5,direct)

Otra forma es comprobar si usted tiene algún archivo en  /proc/sys/fs/binfmt_misc: el sistema de archivosbinfmt_misccorrectamente montado creará al menos dos archivos especiales conregistro de nombres  y estadoen ese directorio.

A continuación, como queremos que nuestros scripts .go puedan pasar de manera adecuada el código de salida al sistema operativo, necesitamos el wrapper personalizado gorun como nuestro “intérprete”.

$ go get github.com/erning/gorun
$ sudo mv ~/go/bin/gorun /usr/local/bin/

Técnicamente, no necesitamos mover gorun a /usr/local/bin o a cualquier otra ruta del sistema como binfmt_misc que en todos los casos necesita una ruta completa al intérprete, pero el sistema puede hacer correr este ejecutable con privilegios arbitrarios, por lo que se sugiere limitar el acceso al archivo desde el punto de vista de la seguridad.

En este punto, crearemos un simple toy script de Gohelloscript.go y verificaremos si podemos “interpretarlo” correctamente. El script:

package main

import (
	"fmt"
	"os"
)

func main() {
	s := "world"

	if len(os.Args) > 1 {
		s = os.Args[1]
	}

	fmt.Printf("Hello, %v!", s)
	fmt.Println("")

	if s == "fail" {
		os.Exit(30)
	}
}

Verificar si el paso de parámetros y el control de errores funcionan según lo previsto:

$ gorun helloscript.go
Hello, world!
$ echo $?
0
$ gorun helloscript.go gopher
Hello, gopher!
$ echo $?
0
$ gorun helloscript.go fail
Hello, fail!
$ echo $?
30

Ahora tenemos que indicarle al módulobinfmt_misccómo ejecutar nuestros archivos .gocon gorun. Luego de la documentación necesitamos esta cadena de configuración: :golang:E::go::/usr/local/bin/gorun:OC, que básicamente le indica al sistema: “si se encuentra un archivo ejecutable con extensión .go, se debe ejecutar con el intérprete /usr/local/bin/gorun”. Las marcasOCal final de la cadena se aseguran de que el script se ejecutará de acuerdo con la información del propietario y los bits de permiso establecidos en el propio script, y no los establecidos en el binario del intérprete. Esto hace que el comportamiento de ejecución de los scripts Go sea el mismo que el del resto de los archivos ejecutables y scripts de Linux.

Registremos nuestro nuevo formato binario de script Go:

$ echo ':golang:E::go::/usr/local/bin/gorun:OC' | sudo tee /proc/sys/fs/binfmt_misc/register
:golang:E::go::/usr/local/bin/gorun:OC

Si el sistema ha registrado correctamente el formato, debe aparecer un nuevo archivo golang en el directorio /proc/sys/fs/binfmt_misc. Por último, podemos ejecutar nuestros archivos .go:

$ chmod u+x helloscript.go
$ ./helloscript.go
Hello, world!
$ ./helloscript.go gopher
Hello, gopher!
$ ./helloscript.go fail
Hello, fail!
$ echo $?
30

¡Eso es todo! Ahora podemos editar helloscript.go según más nos agrade y ver que los cambios se harán visibles de inmediato la próxima vez que se ejecute el archivo. Además, a diferencia del enfoque shebang anterior, podemos compilar este archivo en cualquier momento, en un archivo ejecutable real con go build.


Independientemente de si le gusta Go o desea investigar en Linux, tenemos puestos para cualquiera de los dos o incluso para ambos a la vez. Consulte nuestra página de empleos.