2026-04-08

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.
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.
godocLa 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.
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.
Los build constraints aceptan lógica booleana:
| Operador | Sintaxis clásica | Sintaxis nueva | Significado |
|---|---|---|---|
| OR | // +build linux darwin | //go:build linux || darwin | compila en Linux o macOS |
| AND | // +build windows,386 | //go:build windows && 386 | compila en Windows y 386 |
| NOT | // +build !windows | //go:build !windows | compila 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.
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.
ignoreUna 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
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.
go generateEste 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.
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.
#cgoAdemá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 arquitecturasamd64 386 CFLAGS: -DX86=1 pasa este flag solo en amd64 y 386LDFLAGS: -lpng le dice al linker que tiene que linkear con libpngpkg-config: png cairo usa pkg-config para descubrir automáticamente los flags correctos para libpng y libcairoCgo 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.
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.
Un resumen para dejar abierto mientras escribís:
| Comentario | Para qué sirve | Quién lo procesa |
|---|---|---|
// Foo hace tal cosa. arriba de una declaración | Se convierte en documentación pública | go doc, pkg.go.dev |
// Package x hace Y. antes de package | Documentación del paquete entero | go doc, pkg.go.dev |
//go:build linux (o // +build linux) | Restringe la compilación a Linux | compilador |
//go:build integration | Compila solo con -tags=integration | compilador |
//go:build ignore | El archivo queda en el repo pero no compila | compilador |
//go:generate cmd args | Corre cmd args con go generate | go generate |
Comentario antes de import "C" | Se convierte en código C literal | compilador C vía cgo |
// #cgo LDFLAGS: -lpng | Configura flags de cgo | cgo |
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.