UTF-8 y el tipo rune en Go

En Go, un string no es una lista de caracteres, sino una secuencia de bytes leída como UTF-8. Aunque solemos pensar en texto como una sucesión de letras, para la máquina un string es solo memoria cruda. Un rune es la abstracción que usamos para manejar el texto real: es un alias de int32 que representa un Unicode code point (el número único asignado a cada carácter en el estándar Unicode).

Para entender esto, considera que UTF-8 es un encoding de longitud variable. Un carácter simple como A ocupa 1 byte, pero una ñ ocupa 2 y un emoji como 🚀 ocupa 4. Por eso, si intentas contar caracteres usando la función len(), lo que realmente estás obteniendo es el número de bytes, no el número de letras. Si necesitas contar caracteres reales, debes usar el paquete unicode/utf8. Si necesitas procesar el texto carácter por carácter de forma segura, debes iterar sobre runes. Si tratas de segmentar un string usando índices de bytes directamente (como s[i]), corres el riesgo de cortar un carácter a la mitad, dejando una secuencia de bytes inválida que el sistema no podrá interpretar.

package main

import (
	"fmt"
	"unicode/utf8"
)

func main() {
	// Un string con caracteres ASCII (1 byte), latinos (2 bytes) y emojis (4 bytes)
	texto := "Holañ🚀"

	// len() devuelve la cantidad de bytes, no de caracteres.
	fmt.Printf("Texto: %s\n", texto)
	fmt.Printf("Bytes en el string (len): %d\n", len(texto))

	// RuneCountInString cuenta cuántos caracteres (runes) hay realmente.
	fmt.Printf("Caracteres reales (RuneCount): %d\n", utf8.RuneCountInString(texto))

	fmt.Println("\n--- Iteración con 'for range' ---")
	// El loop 'range' sobre un string es especial: decodifica UTF-8 
	// automáticamente y nos entrega el índice del byte donde empieza 
	// cada carácter y el valor del rune.
	for i, r := range texto {
		fmt.Printf("Inicio byte: %d | Rune: %c | Valor int32: %d\n", i, r, r)
	}

	fmt.Println("\n--- Decodificación manual ---")
	// Si sabemos que el emoji está después de 'Holañ', podemos saltar a esa posición.
	// 'Hola' (4) + 'ñ' (2) = offset 6.
	offset := 6
	if offset < len(texto) {
		// DecodeRuneInString nos dice qué carácter hay en un offset y cuánto mide.
		r, width := utf8.DecodeRuneInString(texto[offset:])
		fmt.Printf("En el offset %d, encontramos: %c (ocupa %d bytes)\n", offset, r, width)
	}
}

En el ejemplo anterior, observa cómo len(texto) devuelve 10. Esto ocurre porque H, o, l, a ocupan 1 byte cada uno (4), la ñ ocupa 2 bytes y el emoji 🚀 ocupa 4 bytes ($4 + 2 + 4 = 10$). Sin embargo, utf8.RuneCountInString nos da el número lógico de caracteres: 6.

Fíjate bien en el for range. El índice i no aumenta de uno en uno de forma lineal. Cuando el bucle llega a la ñ, el primer índice es 4, pero el siguiente índice que entrega el loop es 6. Esto sucede porque el range es inteligente: detecta que la ñ ocupa dos bytes y salta directamente al inicio del siguiente carácter. Si hubiéramos usado un for i := 0; i < len(texto); i++ tradicional, habríamos intentado leer la mitad de la ñ y la mitad del emoji, obteniendo datos corruptos.

Finalmente, utf8.DecodeRuneInString es la forma segura de inspeccionar el contenido de un string desde una posición específica, devolviéndote el rune y cuántos bytes consume, lo cual es vital para navegar strings sin romper la codificación.

El error frecuente

Un error clásico es intentar acceder a un carácter de un string usando un índice directo de byte cuando el string contiene caracteres multibyte.

s := "café"
// La 'é' ocupa 2 bytes en UTF-8.
// Si intentas acceder al segundo byte de 'é':
fmt.Printf("Byte problemático: %c\n", s[4]) 

En este caso, s[4] no te devolverá la é ni un espacio en blanco; te devolverá uno de los dos bytes que componen la é. Como ese byte por sí solo no es un carácter válido en UTF-8, la consola imprimirá un símbolo de reemplazo o basura (como ). Para trabajar con posiciones, siempre usa el range o el paquete utf8.

30

Dejar un comentario

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

Scroll al inicio