96 lines
4.0 KiB
Markdown
96 lines
4.0 KiB
Markdown
# 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.
|