Esta pagina se ve mejor con JavaScript habilitado

Go strings, chars y runas

 ·  🎃 kr0m

En este artículo veremos como manipular cadenas de texto en Go, la representación interna de carácteres, la problemática que implica trabajar con carácteres UTF-8 y como Go lo resuelve mediante el concepto de runas.

El artículo está dividido en las siguientes secciones:


Si eres nuevo en el mundo de Go te recomiendo los siguientes artículos anteriores:


Cadena básica:

Un string no es mas que un slice de bytes en Go, estas pueden ser creadas escribiendo un conjunto de carácteres entre comillas dobles: " ". Veamos un sencillo ejemplo.

vi strings00.go
package main

import (
    "fmt"
)

func main() {
    name := "Hello World"
    fmt.Println(name)
}

Si ejecutamos el código veremos la siguiente salida:

go run strings00.go

Hello World

Representación interna de los carácteres:

Los datos de los carácteres de las cadenas son almacenados byte a byte, por lo tanto podemos iterar sobre la cadena obteniendo cada uno de estos. En el siguiente ejemplo mostramos la representación como chars y la representación hexadecimal de cada uno de ellos.

vi strings01.go
package main

import (
    "fmt"
)

func printChars(s string) {
    fmt.Printf("Characters: ")
    for i := 0; i < len(s); i++ {
        fmt.Printf("%c ", s[i])
    }
    fmt.Printf("\n")
}

func printBytes(s string) {
    fmt.Printf("Bytes: ")
    for i := 0; i < len(s); i++ {
        fmt.Printf("%x ", s[i])
    }
    fmt.Printf("\n")
}

func main() {
    name := "Hello World"
    fmt.Printf("String: %s\n", name)
    printChars(name)
    printBytes(name)
}

Si ejecutamos el código veremos la siguiente salida:

go run strings01.go

String: Hello World
Characters: H e l l o   W o r l d
Bytes: 48 65 6c 6c 6f 20 57 6f 72 6c 64

UTF-8 y Runas:

Los carácteres ocupan 1 byte, pero si se trata de UTF-8(cualquier carácter no inglés) pueden ocupar 1,2,3 o 4 bytes. Si tratamos de mostrar estas cadenas char a char, no funcionará correctamente.

vi strings02.go
package main

import (
    "fmt"
)

func printChars(s string) {
    fmt.Printf("Characters: ")
    for i := 0; i < len(s); i++ {
        fmt.Printf("%c ", s[i])
    }
    fmt.Printf("\n")
}

func printBytes(s string) {
    fmt.Printf("Bytes: ")
    for i := 0; i < len(s); i++ {
        fmt.Printf("%x ", s[i])
    }
    fmt.Printf("\n")
}

func main() {
    name := "Hello señor"
    fmt.Printf("String: %s\n", name)
    printChars(name)
    printBytes(name)
}

Si ejecutamos el código veremos la siguiente salida:

go run strings02.go

String: Hello señor
Characters: H e l l o   s e à ± o r
Bytes: 48 65 6c 6c 6f 20 73 65 c3 b1 6f 72

Podemos resolver el problema utilizando runas. En el siguiente código generamos un slice de runas a partir de una cadena, cuando iteremos cada uno de los carácteres estaremos iterando realmente cada una de las runas.

vi strings03.go
package main

import (
    "fmt"
)

func printChars(s string) {
    fmt.Printf("Characters: ")
    runes := []rune(s)
    for i := 0; i < len(runes); i++ {
        fmt.Printf("%c ", runes[i])
    }
    fmt.Printf("\n")
}

func printBytes(s string) {
    fmt.Printf("Bytes: ")
    for i := 0; i < len(s); i++ {
        fmt.Printf("%x ", s[i])
    }
    fmt.Printf("\n")
}

func main() {
    name := "Hello World"
    fmt.Printf("String: %s\n", name)
    printChars(name)
    printBytes(name)

    fmt.Printf("\n\n")

    name = "Señor"
    fmt.Printf("String: %s\n", name)
    printChars(name)
    printBytes(name)
}

Si ejecutamos el código veremos la siguiente salida:

go run strings03.go

String: Hello World
Characters: H e l l o   W o r l d
Bytes: 48 65 6c 6c 6f 20 57 6f 72 6c 64


String: Señor
Characters: S e ñ o r
Bytes: 53 65 c3 b1 6f 72

Si utilizamos un for range de Go no será necesario utilizar un slice de runas ya que el propio for hará la conversión por nosotros.

vi strings04.go
package main

import (
    "fmt"
)

func printChars(s string) {
    fmt.Printf("Characters: ")
    for _, char := range s {
        fmt.Printf("%c", char)
    }
    fmt.Printf("\n")
}

func printBytes(s string) {
    fmt.Printf("Bytes: ")
    for i := 0; i < len(s); i++ {
        fmt.Printf("%x ", s[i])
    }
    fmt.Printf("\n")
}

func main() {
    name := "Hello señor"
    fmt.Printf("String: %s\n", name)
    printChars(name)
    printBytes(name)
}

Si ejecutamos el código veremos la siguiente salida:

go run strings04.go

String: Hello señor
Characters: Hello señor
Bytes: 48 65 6c 6c 6f 20 73 65 c3 b1 6f 72

Como norma general es siempre recomendable utilizar runas para evitar sorpresas desagradables.


Inmutabilidad:

Los strings en Go son inmutables, es decir, no se puede alterar su contenido, por ejemplo esta función produciría un error:

func mutate(s string) string {
    s[0] = 'a'
    return s
}
./strings05.go:8:5: cannot assign to s[0] (neither addressable nor a map index expression)

Si queremos modificar el contenido, debemos trabajar con runas.

vi strings05.go
package main

import (  
    "fmt"
)

func mutate(s string) string {
    runes := []rune(s)
    runes[0] = 'a' 
    return string(runes)
}

func main() {  
    s := "hello"
    fmt.Println(mutate(s))
}

Si ejecutamos el código veremos la siguiente salida:

go run strings05.go

aello

Longitud:

Otro problema que podemos tener es al contar la longitud de las cadenas, mediante la visualización de los carácteres hexadecimales podemos ver que “señor” ocupa 6 chars cuando debería de ocupar 5.

name := "Hello World"
Bytes: 48 65 6c 6c 6f 20 57 6f 72 6c 64
"Hello " -> 48 65 6c 6c 6f 20
"World"  -> 57 6f 72 6c 64

name := "Hello señor"
Bytes: 48 65 6c 6c 6f 20 73 65 c3 b1 6f 72
"Hello " -> 48 65 6c 6c 6f 20
"señor"  -> 73 65 c3 b1 6f 72

La función len() siempre mostará el número de bytes de una cadena, en el caso que nos ocupa len(48 65 6c 6c 6f 20 73 65 c3 b1 6f 72). Si queremos obtener el número de runas, debemos utilizar utf8.RuneCountInString.

vi strings06.go

package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {
    name := "Hello señor"
    fmt.Printf("String len: %v\n", len(name))
    fmt.Printf("String utf8.RuneCountInString: %v\n", utf8.RuneCountInString(name))
}

Si ejecutamos el código veremos la siguiente salida:

go run strings06.go

String len: 12
String utf8.RuneCountInString: 11

Comparación:

La comparación de cadenas es muy sencilla, tan solo debemos utilizar el operador ==.

vi strings07.go
package main

import (
    "fmt"
)

func compareStrings(str1 string, str2 string) {
    if str1 == str2 {
        fmt.Printf("%s and %s are equal\n", str1, str2)
        return
    }
    fmt.Printf("%s and %s are not equal\n", str1, str2)
}

func main() {
    string1 := "Go"
    string2 := "Go"
    compareStrings(string1, string2)
	
    string3 := "hello"
    string4 := "world"
    compareStrings(string3, string4)
}

Si ejecutamos el código veremos la siguiente salida:

go run strings07.go

Go and Go are equal
hello and world are not equal

Concatenación:

La concatenación también resulta muy sencilla, tan solo debemos unir las dos cadenas utilizando el operador +.

vi strings08.go
package main

import (
    "fmt"
)

func main() {
    string1 := "Go"
    string2 := "is awesome"
    result := string1 + " " + string2
    fmt.Println(result)
}

Si ejecutamos el código veremos la siguiente salida:

go run strings08.go

Go is awesome
Si te ha gustado el artículo puedes invitarme a un RedBull aquí