# 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 += x` crea 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.Sprintf`** reserva memoria para el resultado y usa reflexión. `strconv.AppendInt` escribe 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}` ```go 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 como `C3 A9` y 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.