← Volver al listado de tecnologías

Strings, Runes y Bytes

Por: Artiko
gostringsrunesbytesunicode

Strings, Runes y Bytes

En Go, un string es una secuencia inmutable de bytes. Para trabajar correctamente con texto Unicode, es fundamental entender la relacion entre string, []byte y []rune.

Strings son Inmutables

Un string no puede modificarse despues de crearse. Cualquier operacion que “modifica” un string en realidad crea uno nuevo:

package main

import "fmt"

func main() {
    s := "Hola"
    // s[0] = 'h' // ERROR: no compila, strings son inmutables

    // Para modificar, convertir a []byte
    b := []byte(s)
    b[0] = 'h'
    s2 := string(b)
    fmt.Println(s2) // "hola"

    // Concatenacion crea un nuevo string
    s3 := s + " mundo"
    fmt.Println(s3) // "Hola mundo"
}

String vs []byte vs []rune

TipoRepresentaEjemplo
stringSecuencia inmutable de bytes"Hola"
[]byteSlice mutable de bytes[]byte{72, 111, 108, 97}
[]runeSlice de code points Unicode[]rune{'H', 'o', 'l', 'a'}
package main

import "fmt"

func main() {
    s := "cafe\u0301" // "cafe" + acento combinante = "café"

    fmt.Println("string:", s)
    fmt.Println("len (bytes):", len(s))         // 6
    fmt.Println("[]byte:", []byte(s))            // [99 97 102 101 204 129]
    fmt.Println("[]rune:", []rune(s))            // [99 97 102 101 769]
    fmt.Printf("tipo rune: %T\n", []rune(s)[0]) // int32
}

Un rune es un alias de int32 y representa un code point Unicode.

Raw Strings con Backticks

Los raw strings usan backticks y no procesan secuencias de escape:

package main

import "fmt"

func main() {
    // String normal: procesa escapes
    normal := "Linea 1\nLinea 2\tTabulado"
    fmt.Println(normal)

    // Raw string: literal, sin escapes
    raw := `Linea 1\nLinea 2\tTabulado`
    fmt.Println(raw) // imprime \n y \t literalmente

    // Util para regex, SQL, HTML
    query := `
        SELECT nombre, edad
        FROM usuarios
        WHERE edad > 18
        ORDER BY nombre
    `
    fmt.Println(query)

    ruta := `C:\Users\artiko\documentos`
    fmt.Println(ruta)
}

len() vs RuneCountInString()

len() cuenta bytes, no caracteres. Para contar caracteres Unicode usa utf8.RuneCountInString():

package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {
    ascii := "Hello"
    utf := "Hola mundo"
    emoji := "Go es 🔥"
    japones := "日本語"

    fmt.Printf("%-15s len=%d runes=%d\n", ascii,
        len(ascii), utf8.RuneCountInString(ascii))

    fmt.Printf("%-15s len=%d runes=%d\n", utf,
        len(utf), utf8.RuneCountInString(utf))

    fmt.Printf("%-15s len=%d runes=%d\n", emoji,
        len(emoji), utf8.RuneCountInString(emoji))

    fmt.Printf("%-15s len=%d runes=%d\n", japones,
        len(japones), utf8.RuneCountInString(japones))
}

Salida:

Hello           len=5  runes=5
Hola mundo      len=10 runes=10
Go es 🔥        len=10 runes=7
日本語           len=9  runes=3

Iterar: range (Runes) vs Indice (Bytes)

package main

import "fmt"

func main() {
    s := "café"

    // Por indice: itera bytes (puede romper caracteres multi-byte)
    fmt.Println("=== Por bytes ===")
    for i := 0; i < len(s); i++ {
        fmt.Printf("byte[%d] = %x\n", i, s[i])
    }

    // Con range: itera runes (correcto para Unicode)
    fmt.Println("\n=== Por runes ===")
    for i, r := range s {
        fmt.Printf("rune[%d] = %c (U+%04X)\n", i, r, r)
    }
}

El range sobre un string decodifica automaticamente UTF-8 y entrega cada rune con su posicion en bytes.

Paquete strings

El paquete strings provee funciones para manipular strings:

package main

import (
    "fmt"
    "strings"
)

func main() {
    s := "  Go es simple y poderoso  "

    // Busqueda
    fmt.Println(strings.Contains(s, "simple"))    // true
    fmt.Println(strings.HasPrefix(s, "  Go"))     // true
    fmt.Println(strings.HasSuffix(s, "poderoso  ")) // true
    fmt.Println(strings.Count(s, "o"))            // 3
    fmt.Println(strings.Index(s, "simple"))       // 8

    // Transformacion
    fmt.Println(strings.ToUpper("hola"))      // "HOLA"
    fmt.Println(strings.ToLower("MUNDO"))     // "mundo"
    fmt.Println(strings.TrimSpace(s))         // "Go es simple y poderoso"
    fmt.Println(strings.Trim("***hola***", "*")) // "hola"

    // Split y Join
    partes := strings.Split("a,b,c,d", ",")
    fmt.Println(partes) // [a b c d]

    unido := strings.Join(partes, " - ")
    fmt.Println(unido) // "a - b - c - d"

    // Reemplazo
    fmt.Println(strings.Replace("foo bar foo", "foo", "baz", 1))  // "baz bar foo"
    fmt.Println(strings.ReplaceAll("foo bar foo", "foo", "baz"))  // "baz bar baz"

    // Repeticion
    fmt.Println(strings.Repeat("Go! ", 3)) // "Go! Go! Go! "
}

Resumen de funciones strings

FuncionDescripcion
Contains(s, sub)Contiene substring
HasPrefix(s, pre)Empieza con prefijo
HasSuffix(s, suf)Termina con sufijo
Split(s, sep)Divide en slice
Join(sl, sep)Une slice en string
Replace(s, old, new, n)Reemplaza n ocurrencias
ReplaceAll(s, old, new)Reemplaza todas
TrimSpace(s)Elimina espacios extremos
ToUpper(s) / ToLower(s)Cambia mayusculas/minusculas
Count(s, sub)Cuenta ocurrencias

Paquete strconv

Convierte entre strings y tipos numericos:

package main

import (
    "fmt"
    "strconv"
)

func main() {
    // Int a String
    s := strconv.Itoa(42)
    fmt.Println(s) // "42"

    // String a Int
    n, err := strconv.Atoi("123")
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println(n) // 123

    // String a Int con error
    _, err = strconv.Atoi("abc")
    fmt.Println(err) // strconv.Atoi: parsing "abc": invalid syntax

    // Float a String
    sf := strconv.FormatFloat(3.14159, 'f', 2, 64)
    fmt.Println(sf) // "3.14"

    // String a Float
    f, err := strconv.ParseFloat("2.718", 64)
    if err == nil {
        fmt.Println(f) // 2.718
    }

    // Bool
    fmt.Println(strconv.FormatBool(true))  // "true"
    b, _ := strconv.ParseBool("true")
    fmt.Println(b) // true
}

strings.Builder

Para concatenar muchos strings eficientemente, usa strings.Builder en lugar del operador +:

package main

import (
    "fmt"
    "strings"
)

func main() {
    // Ineficiente: cada + crea un nuevo string
    resultado := ""
    for i := 0; i < 5; i++ {
        resultado += fmt.Sprintf("item-%d ", i)
    }
    fmt.Println(resultado)

    // Eficiente: Builder usa un buffer interno
    var b strings.Builder
    for i := 0; i < 5; i++ {
        fmt.Fprintf(&b, "item-%d ", i)
    }
    fmt.Println(b.String())
}

La diferencia de rendimiento es significativa con muchas concatenaciones: + es O(n^2) mientras que Builder es O(n).

Ejemplo Completo: Procesador de Texto

package main

import (
    "fmt"
    "strings"
    "unicode"
    "unicode/utf8"
)

func contarPalabras(texto string) int {
    return len(strings.Fields(texto))
}

func capitalizarPalabras(texto string) string {
    palabras := strings.Fields(texto)
    var b strings.Builder
    for i, p := range palabras {
        if i > 0 {
            b.WriteByte(' ')
        }
        runes := []rune(p)
        runes[0] = unicode.ToUpper(runes[0])
        b.WriteString(string(runes))
    }
    return b.String()
}

func main() {
    texto := "  go es un lenguaje  simple y eficiente  "

    fmt.Println("Original:", texto)
    fmt.Println("Limpio:", strings.TrimSpace(texto))
    fmt.Println("Palabras:", contarPalabras(texto))
    fmt.Println("Capitalizado:", capitalizarPalabras(texto))
    fmt.Println("Runes:", utf8.RuneCountInString(texto))
}

Ejercicios

  1. Escribe una funcion que invierta un string respetando caracteres Unicode multi-byte
  2. Crea una funcion esPalindromo(s string) bool que ignore mayusculas y espacios
  3. Implementa un contador de frecuencia de caracteres usando map[rune]int

Resumen