Cómo importar un paquete de Go usando un dominio propio

La idea final es explicar cómo se puede hacer para importar un paquete de Go usando una construcción como import "example.com/package/foobar/lib" y que funcione bien, en el sentido de que la ruta que se le pone en el import es una ruta que resuelve y desde la que se puede descargar el paquete correspondiente, pero sin tener que poner explícitamente github.com o gitlab.com en la ruta del import.

En Go, salvo los paquetes de la biblioteca estandar, que son los que suelen tener nombres como "log" o "net/http", lo normal es que el resto de bibliotecas e imports se resuelvan en base a paquetes que tienen nombres de URL.

Esto es bueno porque ahora "golang.org/x/crypto" es una ruta completa que permite meter en un namespace la biblioteca crypto, sin perjuicio de que ahora pueda existir otro example.com/pkg/crypto. Aunque al importarlos habrá que hacer un alias, ambos paquetes pueden coexistir.

Existe toda una especificación sorbe cómo funciona el sistema de imports remotos. Está documentado en su web. La idea es que cuando se hace la instalación de un módulo, como golang.org/x/crypto, lo primero que hay que hacer es una petición HTTP mediante protocolo HTTPS a esa misma URL, por ejemplo, https://golang.org/x/crypto. En el cuerpo de esa respuesta tiene que venir una etiqueta <meta> de nombre go-import, que será la que describa las instrucciones sobre cómo obtener el módulo.

Si se visita https://golang.org/x/crypto, la página en este momento hace una redirección a la documentación de ese paquete. Pero viendo el código de https://golang.org/x/crypto, por ejemplo mediante cURL o wget, hay dos etiquetas meta:

<meta name="go-import" content="golang.org/x/crypto git https://go.googlesource.com/crypto">
<meta http-equiv="refresh" content="0; url=https://pkg.go.dev/golang.org/x/crypto">

La primera de estas etiquetas es la go-import, que describe cómo se importa el módulo. La segunda etiqueta es la que hace la redirección que ocurre cuando se accede a https://golang.org/x/crypto desde el navegador web.

Una de las formas más típicas de alojar paquetes de Go es en GitHub y similares. Y de hecho si repetimos el procedimiento, por ejemplo, si vemos el código fuente de https://github.com/spf13/cobra, vemos que GitHub también tiene su propia etiqueta <meta name="go-import"> en el código fuente HTML:

<meta name="go-import" content="github.com/spf13/cobra git https://github.com/spf13/cobra.git">

Sin embargo, importar una URL de GitHub es menos canónico que importar un paquete desde un dominio propio. Además, quién sabe cuándo las cosas pueden cambiar y cuando puede ser necesario migrar a GitLab o a Codeberg o similares. La ventaja de importar las cosas desde un dominio propio es poder ser independiente a ese tipo de cosas.

Pongamos que tengo un paquete en https://github.com/danirod/foo, pero quiero importarlo como danirod.es/pkg/foo en vez de como github.com/danirod/foo. De acuerdo con la especificación, mientras hacer una petición web a https://danirod.es/pkg/foo devuelva un documento HTML que lleve su etiqueta meta, Go sabrá resolver el paquete.

Entrando en detalle, Go resuelve un paquete haciendo una petición HTTP usando protocolo HTTPS, usando como base de la URL el nombre del paquete, pero también añadiendo el queryparam ?go-get=1 al final. O sea, que hacer un import de danirod.es/pkg/foo hace que Go pida esos metadatos desde https://danirod.es/pkg/foo?go-get=1.

El formato de la etiqueta go-import, según el spec es:

<meta name="go-import" content="import-prefix vcs repo-root">

Donde import-prefix es la raíz del paquete (ahora explico), vcs es un string que indica el tipo de control de versiones que tiene que usar Go para descargar el paquete (por ejemplo, "git" o "svn"), y repo-root es la URL al repositorio desde el que clonar el paquete.

Por ejemplo, ante el siguiente ejemplo:

<meta name="go-import" content="danirod.es/pkg/foo git https://github.com/danirod/foo">

La etiqueta le permite a Go saber que para obtener el paquete danirod.es/pkg/foo tiene que hacer git clone https://github.com/danirod/foo. Otros sistemas de control de versiones como Fossil o Bazaar también están soportados.

Sobre la cuestión del import-prefix, su principal propósito es indicar cuál es la raíz real del proyecto. Esto es importante si se trata de resolver un import de un subpaquete concreto, como pueda ser golang.org/x/crypto/argon2. En ese caso, Go tratará de descargar el paquete inicialmente desde https://golang.org/x/crypto/argon2?go-get=1, por lo que especificar como import-root el nombre golang.org/x/cyrpto le señaliza a Go que en realidad lo que está intentando importar es un subpaquete de otro.