En Go, el comentario también es código

2026-04-08

post-thumb

Índice

El comentario es esa cosa que todo el mundo aprende el primer día de cualquier lenguaje y ya asume saber todo al respecto. En Go, esa suposición cuesta caro. El lenguaje hizo del comentario una herramienta de primera clase: un mismo // puede ser apenas un recordatorio para vos, convertirse en documentación pública, decidir qué va a incluir el compilador en el binario, generar código nuevo en tiempo de build o incluso convertirse en código C de verdad.

Este post es sobre los comentarios en Go que hacen más que comentar.


La sintaxis básica

Para sacárnoslo de encima: Go soporta dos tipos de comentarios, exactamente como C.

// comentario de una línea
// que puede ocupar varias líneas pegadas

/* comentario de bloque
   que funciona bien para bloques largos
   o cuando necesitás un comentario en medio de una expresión */

x := /* inline en medio de la línea */ 42

En la práctica, // es el estándar en casi todo el código Go idiomático, incluyendo la biblioteca estándar. /* */ queda reservado para casos específicos, tipo documentación de paquete muy larga o un comentario inline puntual.

Hasta acá nada mágico. La magia empieza cuando entendés que el compilador y las herramientas de Go leen algunos de esos comentarios.


1. Documentación que se convierte en sitio público: godoc

La primera regla no escrita de Go es: el comentario que precede una declaración se convierte en la documentación de esa declaración. Sin anotación, sin tag, sin nada. Basta con poner el comentario inmediatamente arriba, sin línea en blanco en el medio.

// Foo aplica la transformación foo en la string recibida.
// Retorna error si la string no puede ser foo'eada.
func Foo(s string) error {
    // ...
}

Este comentario no es solo para quien lee el código en el editor. Alimenta pkg.go.dev, go doc en la terminal y cualquier herramienta que genere documentación de paquetes Go. ¿Exportaste un símbolo (nombre que empieza con mayúscula)? La convención es que venga con un comentario descriptivo, empezando con el propio nombre del símbolo.

Funciona para todo lo que sea exportable:

package objetos

// Objeto es un contenedor genérico de algo.
type Objeto struct{}

// Procesar aplica la lógica de procesamiento al Objeto.
// Retorna error si el objeto está en un estado inválido.
func (o Objeto) Procesar() error {
    return nil
}

// Lista contiene todos los objetos registrados actualmente.
var Lista []Objeto

// MaxObjetos define el límite máximo de objetos permitidos.
const MaxObjetos = 50

También existe la documentación de paquete: un comentario colocado inmediatamente antes de la declaración package, al inicio de cualquier archivo del paquete. La convención es ponerlo en un archivo llamado doc.go cuando la descripción sea larga.

// Package objetos provee un contenedor genérico y seguro para uso concurrente,
// con registro global y límites configurables.
package objetos

Una sutileza importante: la primera frase del comentario es lo que aparece en el índice de pkg.go.dev. Escribí la primera frase pensando en ella como un resumen de una línea. El resto del párrafo solo aparece cuando alguien abre la página completa.


2. Comentarios que controlan qué compila: build constraints

Acá la cosa se empieza a poner divertida. Go tiene un sistema de build constraints (también conocidos como build tags) que permite decirle al compilador: “este archivo solo tiene sentido en Linux”, o “solo compila cuando el flag integration esté activo”. Y todo eso vive dentro de un comentario.

La sintaxis clásica (pre Go 1.17):

// +build linux

package main

// todo acá solo compila en Linux

La regla es bien específica: el comentario // +build ... tiene que estar en las primeras líneas del archivo, antes de la declaración package, seguido por una línea en blanco antes del package. Si te olvidás de la línea en blanco, se convierte en comentario normal y el compilador lo ignora.

A partir de Go 1.17 existe una sintaxis nueva, más limpia:

//go:build linux

package main

Ambas formas funcionan, y los proyectos modernos suelen incluir las dos por compatibilidad.

Operadores

Los build constraints aceptan lógica booleana:

OperadorSintaxis clásicaSintaxis nuevaSignificado
OR// +build linux darwin//go:build linux || darwincompila en Linux o macOS
AND// +build windows,386//go:build windows && 386compila en Windows y 386
NOT// +build !windows//go:build !windowscompila en cualquier cosa menos Windows

En la sintaxis clásica, múltiples líneas suman como AND:

// +build windows
// +build 386

package main

Esto es equivalente a windows AND 386.

Tags customizadas

No te tenés que limitar a sistemas operativos y arquitecturas. Cualquier palabra puede ser un tag, y la activás con go build -tags="mitag".

//go:build integration

package repositorio

// tests que solo corren con: go test -tags=integration ./...

Así es como las bibliotecas separan tests unitarios de tests que necesitan base de datos, por ejemplo.

El tag ignore

Una convención útil: usar ignore como tag para archivos que querés mantener en el repositorio pero que el compilador no debe incluir en ninguna build normal.

//go:build ignore

package main

// este archivo es un utilitario de generación manual
// corré con: go run archivo.go

Nombre de archivo como build constraint implícito

Bonus: si un archivo se llama foo_linux.go, foo_windows.go o foo_amd64.go, Go aplica la constraint automáticamente, sin necesidad de comentario. El sufijo es el propio build tag. Las combinaciones funcionan: foo_linux_amd64.go solo compila en Linux amd64.


3. Comentarios que generan código: go generate

Este es el que más sorprende a la gente que viene de otros lenguajes. Go tiene un subcomando llamado go generate que recorre tu código buscando comentarios especiales y ejecuta los comandos que están dentro de ellos. La sintaxis es:

//go:generate <comando> <argumentos>

Atención al detalle: no puede haber espacio entre las dos barras y go:generate. // go:generate no funciona. Tiene que ser //go:generate, todo pegado.

El ejemplo canónico es la herramienta stringer, que genera automáticamente un método String() para tipos enumerados. Partiendo de este código:

package pedido

//go:generate stringer -type=Status

type Status int

const (
    StatusPendiente Status = iota
    StatusPagado
    StatusCancelado
    StatusEntregado
)

Cuando corrés go generate ./..., Go encuentra ese comentario, ejecuta stringer -type=Status en el directorio y genera un archivo status_string.go con algo así:

// Code generated by "stringer -type=Status"; DO NOT EDIT.

package pedido

func (i Status) String() string {
    switch i {
    case StatusPendiente:
        return "Pendiente"
    case StatusPagado:
        return "Pagado"
    case StatusCancelado:
        return "Cancelado"
    case StatusEntregado:
        return "Entregado"
    }
    return "Status desconocido"
}

A partir de ahí fmt.Println(StatusPagado) imprime Pagado, no el entero subyacente. Todo sin que vos escribas una sola línea del switch.

go generate no corre automáticamente durante go build. Es un comando explícito que tenés que correr (idealmente automatizado vía Makefile o CI) antes de compilar. Es intencional: Go prefiere que el código generado sea commiteado al repositorio, no producido en tiempo de build.

Podés usar go:generate con cualquier comando, no solo stringer. Herramientas comunes incluyen mockgen (generación de mocks), protoc-gen-go (Protobuf), sqlc (queries SQL tipadas) y similares.


4. Comentarios que se convierten en código C: Cgo

Acá la magia sale de Go y entra en territorio extranjero. Cgo es el mecanismo oficial para llamar código C desde Go, y la forma de escribir ese código C es… adivinaste, dentro de comentarios.

El patrón es colocar un bloque de comentario inmediatamente antes del import "C" (sin línea en blanco entre los dos). Todo lo que esté en ese comentario se pasa literalmente al compilador C.

package main

// #include <stdio.h>
// #include <errno.h>
import "C"

También podés escribir funciones C enteras ahí dentro:

package main

// #include <stdio.h>
//
// static void imprimir(char *s) {
//     printf("%s\n", s);
// }
import "C"

func main() {
    C.imprimir(C.CString("hola, mundo C"))
}

Ese imprimir es una función C de verdad, compilada por el compilador C del sistema, e invocada desde Go como si fuera una función Go cualquiera.

Directivas #cgo

Además del código C puro, hay unas directivas especiales que empiezan con #cgo y configuran flags del compilador:

package imagenes

// #cgo CFLAGS: -DPNG_DEBUG=1
// #cgo amd64 386 CFLAGS: -DX86=1
// #cgo LDFLAGS: -lpng
// #cgo pkg-config: png cairo
// #include <png.h>
import "C"

Traduciendo cada línea:

  • CFLAGS: -DPNG_DEBUG=1 pasa este flag al compilador C en todas las arquitecturas
  • amd64 386 CFLAGS: -DX86=1 pasa este flag solo en amd64 y 386
  • LDFLAGS: -lpng le dice al linker que tiene que linkear con libpng
  • pkg-config: png cairo usa pkg-config para descubrir automáticamente los flags correctos para libpng y libcairo

Cgo es poderoso, pero tiene un costo real de performance (cada llamada Go a C cruza una frontera) y complica bastante el cross-compile. Yo lo uso como último recurso, cuando no existe una biblioteca Go nativa para lo que necesito.


Por qué importa

Podés escribir Go ignorando todo esto. Podés vivir años usando // solo como comentario normal y nunca cruzarte con un //go:build. Pero entender este mecanismo cambia dos cosas en tu relación con el lenguaje.

Primero, empezás a leer la biblioteca estándar de una forma distinta. De repente ese archivo con //go:build darwin al tope tiene sentido, y entendés por qué el paquete net tiene comportamientos sutilmente distintos en cada sistema operativo. La biblioteca estándar es un curso práctico de cómo usar build constraints bien.

Segundo, y más importante: te das cuenta de que Go tomó una decisión de diseño deliberada e inusual. Mientras otros lenguajes inventan anotaciones (@override, @Component), atributos ([Serializable]), macros (#[derive]) o decorators (@classmethod), Go reutilizó algo que todos los lenguajes ya tienen: el comentario. La sintaxis sigue simple. Quien no conoce el mecanismo lo ignora sin perder nada. Quien lo conoce, gana superpoderes sin ensuciar la gramática del lenguaje.

Es el tipo de decisión que parece demasiado simple para funcionar, y después de un tiempo usándola entendés por qué funciona.


Para llevar en el bolsillo

Un resumen para dejar abierto mientras escribís:

ComentarioPara qué sirveQuién lo procesa
// Foo hace tal cosa. arriba de una declaraciónSe convierte en documentación públicago doc, pkg.go.dev
// Package x hace Y. antes de packageDocumentación del paquete enterogo doc, pkg.go.dev
//go:build linux (o // +build linux)Restringe la compilación a Linuxcompilador
//go:build integrationCompila solo con -tags=integrationcompilador
//go:build ignoreEl archivo queda en el repo pero no compilacompilador
//go:generate cmd argsCorre cmd args con go generatego generate
Comentario antes de import "C"Se convierte en código C literalcompilador C vía cgo
// #cgo LDFLAGS: -lpngConfigura flags de cgocgo

El próximo paso es mirar el código fuente de un paquete grande de la biblioteca estándar (recomiendo runtime o net) y buscar estos comentarios. Vas a encontrar usos creativos que no caben en un solo post.

Y la próxima vez que alguien te diga que un comentario no es código, sonreí y decile que abra un archivo .go.