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:
- Cadena básica.
- Representación interna de los carácteres.
- UTF-8 y Runas.
- Inmutabilidad.
- Longitud.
- Comparación.
- Concatenación.
Si eres nuevo en el mundo de Go te recomiendo los siguientes artÃculos anteriores:
- Go basics
- Go functions
- Go unit tests
- Go pointers
- Go interfaces
- Go structs
- Go generics
- Go errors
- Go routines
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.
package main
import (
"fmt"
)
func main() {
name := "Hello World"
fmt.Println(name)
}
Si ejecutamos el código veremos la siguiente salida:
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.
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:
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.
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:
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.
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:
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.
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:
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.
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:
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.
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:
String len: 12
String utf8.RuneCountInString: 11
Comparación:
La comparación de cadenas es muy sencilla, tan solo debemos utilizar el operador ==
.
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 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 +
.
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 is awesome