Strings en Go: bytes vs caracteres

En Go, un string es una secuencia inmutable de bytes. Aunque lo visualices como una cadena de letras, para la máquina es simplemente una porción de memoria donde se guardan valores de tipo byte (un uint8). Esto es una decisión de diseño para optimizar el uso de memoria: en lugar de asignar un tamaño fijo a cada carácter (lo cual desperdiciaría mucho espacio), Go utiliza UTF-8, un estándar donde los caracteres más comunes ocupan 1 byte, pero otros, como los acentos o emojis, pueden ocupar 2, 3 o 4 bytes.

Por esto, cuando ejecutas len(s), el resultado es la cantidad de bytes que ocupa la cadena, no la cantidad de letras que ves. Si tu texto es puramente ASCII (sin acentos ni caracteres especiales), len(s) y el número de caracteres coincidirán, pero en cuanto introduces un carácter Unicode, la lógica cambia. Intentar acceder a una posición con s[i] te devolverá un byte individual, lo que puede darte un valor incompleto de un carácter si este es multi-byte. Por eso, para trabajar con “letras” reales (llamadas runas en Go), debes usar el bucle range, que es capaz de identificar dónde empieza y termina cada carácter complejo, o convertir el string a un slice de runas []rune si necesitas manipularlo. Si intentas modificar un string directamente, como s[0] = 'A', el compilador te dará un error porque los strings son inmutables; para cambiar una letra, debes transformar el string en algo mutable primero.

package main

import (
	"fmt"
	"unicode/utf8"
)

func main() {
	// "Héctor" tiene 6 caracteres, pero la 'é' ocupa 2 bytes en UTF-8.
	texto := "Héctor"

	fmt.Printf("Texto: %s\n", texto)
	fmt.Printf("Bytes (len): %d\n", len(texto))
	fmt.Printf("Runas (caracteres reales): %d\n", utf8.RuneCountInString(texto))

	fmt.Println("\n--- Acceso por índice (trata al string como bytes) ---")
	for i := 0; i < 3; i++ {
		// Esto no te dará la letra, te dará el valor numérico del byte
		fmt.Printf("s[%d] = %d (byte)\n", i, texto[i])
	}

	fmt.Println("\n--- Iteración con range (desempaqueta runas) ---")
	// range es inteligente: sabe saltar los bytes de un carácter multi-byte
	for i, r := range texto {
		fmt.Printf("Posición byte %d: carácter %c (rune)\n", i, r)
	}

	fmt.Println("\n--- Modificación (Immutability workaround) ---")
	// No podemos hacer: texto[0] = 'B' (error de compilación)
	// Convertimos a []rune para poder editar caracteres de forma segura
	runas := []rune(texto)
	runas[0] = 'B'
	textoModificado := string(runas)

	fmt.Printf("Original: %s\n", texto)
	fmt.Printf("Modificado: %s\n", textoModificado)
}

En el código anterior, observa cómo len(texto) devuelve 7 a pesar de que visualmente vemos 6 letras; esto es porque la é consume 2 bytes. Cuando iteramos con el bucle for i := 0; i < 3; i++ usando el índice directo, texto[1] no te devuelve la é, sino el primer byte que compone ese carácter, lo cual es un valor numérico que no tiene sentido como letra por sí solo.

Sin embargo, al usar for i, r := range texto, el runtime de Go hace el trabajo sucio por ti. El índice i no aumenta de uno en uno de forma lineal si encuentra un carácter multi-byte; fíjate que en la consola verás que después de la posición 1 (la é), el siguiente índice es el 3. Esto ocurre porque range detecta que el carácter en la posición 1 ocupa dos bytes y salta directamente al siguiente. Finalmente, para resolver el problema de la inmutabilidad, convertimos el string a []rune. Al hacerlo, Go crea una nueva estructura en memoria donde cada elemento es un int32 (un rune), permitiéndonos cambiar la H por una B sin romper la estructura del carácter.

El error frecuente

s := "Héctor"
// ERROR: cannot assign to s[0] (cannot assign to s[0] in struct literal)
s[0] = 'B' 

// ERROR LÓGICO: intentas contar letras con len()
if len(s) > 5 {
    // Esto será true para "Héctor" (7 bytes), aunque solo tenga 6 letras.
}

29

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio