Cuando diseñas un sistema distribuido, la primera decisión crítica no es qué lenguaje usar, sino cómo vas a empaquetar los datos para que viajen por la red. El formato de serialización que elijas determinará el consumo de CPU, el ancho de banda y, lo más importante, la capacidad de tus servicios para hablar entre ellos.
El estándar por excelencia para APIs públicas es encoding/json. Es un formato de texto basado en claves-valor que cualquier cliente (un navegador en JavaScript, un script en Python o una app móvil) puede entender sin configuraciones previas. Sin embargo, su flexibilidad tiene un coste: el parsing de texto es lento y el uso de reflection en Go para mapear campos de JSON a structs consume ciclos de CPU considerables. Si tu servicio es un componente interno de un sistema de microservicios con altísimo tráfico, el overhead de JSON puede convertirse en un cuello de botella.
Si te encuentras en un entorno donde todo es Go, la opción lógica es encoding/gob. Es un formato binario nativo que aprovecha la estructura de tipos de Go. A diferencia de JSON, gob es capaz de transmitir la información del tipo de dato de forma eficiente, lo que lo hace ideal para flujos de datos continuos (streams) o comunicación entre procesos Go (como en net/rpc). No obstante, tiene una limitación crítica: es un protocolo cerrado al ecosistema Go; si intentas enviar un gob a un cliente en Rust o Java, te encontrarás con un problema de interoperabilidad inmediato.
Para escenarios de alto rendimiento en arquitectos de microservicios, el estándar de la industria es google.golang.org/protobuf (Protocol Buffers). A diferencia de JSON o gob, Protobuf es schema-first: primero defines tu estructura en un archivo .proto y luego generas el código. Esto permite que el payload sea extremadamente pequeño, ya que no envía los nombres de los campos, sino identificadores numéricos, y el parsing es increíblemente rápido. Es la opción predilecta para gRPC. Si buscas un punto medio —la velocidad y el tamaño de un binario, pero con la flexibilidad de no tener que definir un esquema rígido como en Protobuf—, entonces mira hacia msgpack. Es esencialmente “JSON en binario”, ideal para bases de datos NoSQL o sistemas de mensajería donde la estructura puede evolucionar sin la rigidez de un contrato estricto.
Para completar el panorama, encoding/xml sigue siendo necesario cuando interactúas con APIs legacy o formatos de configuración complejos que requieren una estructura jerárquica estricta basada en etiquetas.
package main
import (
"bytes"
"encoding/gob"
"encoding/json"
"fmt"
"log"
)
// Session representa una sesión de usuario con metadatos dinámicos.
type Session struct {
ID int
User string
Metadata map[string]string
}
func main() {
session := Session{
ID: 1024,
User: "dev_senior",
Metadata: map[string]string{
"region": "us-east-1",
"tier": "premium",
},
}
// --- JSON ---
// Ideal para APIs REST públicas. Es legible y universal, pero pesado.
var jsonBuf bytes.Buffer
if err := json.NewEncoder(&jsonBuf).Encode(session); err != nil {
log.Fatalf("Error en JSON: %v", err)
}
// --- Gob ---
// Formato binario nativo. Muy eficiente para comunicación Go-to-Go.
var gobBuf bytes.Buffer
if err := gob.NewEncoder(&gobBuf).Encode(session); err != nil {
log.Fatalf("Error en Gob: %v", err)
}
fmt.Printf("JSON: %s | Tamaño: %d bytes\n", jsonBuf.String(), jsonBuf.Len())
fmt.Printf("Gob: (binario) | Tamaño: %d bytes\n", gobBuf.Len())
}
Desglose del ejemplo
En el código anterior, hemos comparado la representación de una misma estructura Session.
Al usar json.NewEncoder(&jsonBuf).Encode(session), el encoder recorre la estructura mediante reflexión y genera una cadena de texto. Fíjate en el fmt.Printf: el JSON es legible ({"ID":1024,...}), lo cual es su mayor ventaja para debugging, pero cada clave ("ID", "User", etc.) ocupa espacio innecesario en el payload.
Con gob.NewEncoder(&gobBuf).Encode(session), el proceso es diferente. gob no escribe nombres de campos como “User” en cada mensaje, sino que optimiza la transmisión de la información de tipo. Para una única estructura pequeña, el tamaño puede parecer similar, pero en un flujo constante de datos, gob reduce drásticamente la redundancia. Es importante notar que gobBuf.Bytes() no es legible para humanos; es un flujo de bytes diseñado para ser procesado por otra instancia de Go que conozca la definición de Session.
Si estuviéramos usando Protobuf, no veríamos json.NewEncoder, sino un método generado por el compilador protoc que asigna un número a ID (ej. 1) y a User (ej. 2), eliminando por completo la necesidad de enviar las palabras “ID” o “User” a través de la red.
El error frecuente
Un error clásico al trabajar con serialización en Go es asumir que todos los campos de un struct serán serializados. Tanto encoding/json como encoding/gob respetan la visibilidad de los paquetes.
// Este struct causará problemas de datos perdidos
type User struct {
PublicName string // Se serializa correctamente
secretToken string // Error: es privada (minúscula) y será ignorada
}
Si intentas enviar un struct con campos en minúscula (unexported), el encoder simplemente los saltará sin lanzar un error, lo que resultará en una pérdida de datos silenciosa muy difícil de depurar en producción. Asegúrate siempre de que los campos que necesitas transmitir comiencen con mayúscula.
Para decidir correctamente: usa JSON si el consumidor es un cliente externo; usa Protobuf si estás construyendo el núcleo de tu arquitectura de microservicios; y reserva Gob para procesos internos donde solo hablarás Go.
N° 122