4.0 KiB
Go escrito con mentalidad de C
Experimento: el mismo generador de tickets ESC/POS escrito en Go de dos formas, midiendo el resultado (i9-9900K, Go 1.26, 30 líneas por ticket). Las dos versiones producen exactamente los mismos bytes.
| ns/ticket | bytes reservados/ticket | reservas/ticket | |
|---|---|---|---|
Hábitos de JS (+= en strings, fmt.Sprintf, []*Item) |
~9.600 | 14.378 | 126 |
Hábitos de C (un buffer reutilizado, strconv.Append*) |
~550 | 0 | 0 |
Por qué
- Los strings son inmutables.
s += xcrea un string nuevo y copia todo lo anterior en él. En un bucle, cada iteración copia más bytes que la anterior: el coste crece con el cuadrado del tamaño. fmt.Sprintfreserva memoria para el resultado y usa reflexión.strconv.AppendIntescribe directamente en tu buffer.- Reutilizar el buffer:
buf = buf[:0]pone la longitud a 0 pero conserva la capacidad, es decir, la memoria ya reservada. Tras el primer ticket no se vuelve a reservar nada. Es lo que hará el buffer de la fase 2.
Padding: el orden de los campos cambia el tamaño
El procesador exige que un int64 empiece en una dirección múltiplo de 8. El
compilador rellena con huecos (padding) para cumplirlo:
{bool, int64, bool, int64, bool}ocupa 40 bytes.{int64, int64, bool, bool, bool}ocupa 24 bytes.
Con un millón de elementos son 38 MB frente a 22 MB. En C pasa exactamente lo mismo.
Un slice es {puntero, longitud, capacidad}
header := make([]byte, 0, 16)
header = append(header, 0x1b, '@')
a := append(header, 'A')
b := append(header, 'B') // a también termina en 'B'
Hay capacidad libre, así que append no copia: escribe en el mismo array.
a y b comparten memoria. Quien piensa en punteros lo ve venir; quien
piensa en arrays de JS, no.
Dinero en float64
10 × 0.10 da 0.9999999999999999. El number de JS es un float64. Para
dinero se usan enteros en céntimos.
El precio de la versión rápida
BuildTicket devuelve una vista del buffer interno, que deja de ser
válida en la siguiente llamada. Hay que documentar quién es el dueño de esa
memoria. Es exactamente la regla de la API de la librería: documentar en cada
función quién es dueño de cada puntero.
Tercera versión: una IA a partir de una spec escueta
Spec: "cabecera en negrita, una línea por producto, total, corte, precios con
dos decimales, tests". El código resultante es idiomático (bytes.Buffer,
fmt.Fprintf, dinero en céntimos) y pasa todos los criterios de
aceptación.
| ns/ticket | bytes/ticket | reservas/ticket | |
|---|---|---|---|
| IA con spec escueta | ~8.300 | 3.186 | 99 |
Casos que la spec no menciona:
| Caso | IA | Estilo C | Estilo JS |
|---|---|---|---|
| Descuento de −1,50 | -1.-50 ❌ |
-1.+0 ❌ |
-1.50 ✅ (de casualidad) |
é, ñ, € |
UTF-8 ❌ | UTF-8 ❌ | UTF-8 ❌ |
| Nombre más largo que el papel (48/32 columnas) | no lo trata | no lo trata | no lo trata |
- Negativos: en Go (y en C) el resto de un número negativo es negativo:
-150 % 100 == -50. Las versiones con enteros lo formatean mal. - Codificación: la impresora no entiende UTF-8.
ése envía comoC3 A9y en papel sale como dos símbolos extraños. Hay que convertir a la página de códigos de la impresora (fase 6).
Conclusión: el resultado nunca es mejor que la spec más la capacidad de quien lo revisa. Las decisiones que no se piden las toma el modelo "por defecto", y solo las detecta quien sabe que existen. Una spec de experto incluiría: presupuesto de reservas de memoria, quién es dueño del buffer, importes negativos, página de códigos y ancho de línea según el modelo de impresora.
Cuándo no merece la pena
Si el ticket se genera una vez por venta, 9 µs frente a 0,5 µs da igual. La
versión rápida compensa en rutas calientes: miles de peticiones por segundo o
bucles sobre millones de elementos. Primero se mide (go test -bench -benchmem, pprof) y después se optimiza.