Files
printer-driver/docs/04-go-with-c-mindset.md
T
2026-09-30 10:22:42 +02:00

96 lines
4.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.