initial commit
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user