Em Go, strings são sequências imutáveis de bytes, não de caracteres. Isso significa que uma string pode conter qualquer dado binário, mas quando lidamos com texto, geralmente usamos a codificação UTF-8, que é o padrão em Go. Nesta aula, vamos entender como Go lida com UTF-8, como percorrer strings corretamente usando range, e explorar os pacotes strings e strconv para manipulação e conversão.

Dominar esses conceitos é essencial para evitar bugs comuns, como contar caracteres incorretamente ou corromper dados ao processar texto internacionalizado. Vamos começar!

UTF-8 em Go

Go adota UTF-8 como codificação padrão para strings de texto. Isso significa que cada caractere (rune) pode ocupar de 1 a 4 bytes. Um rune é um tipo inteiro que representa um ponto de código Unicode (int32). Por exemplo, a letra 'A' (U+0041) ocupa 1 byte, enquanto o emoji '😀' (U+1F600) ocupa 4 bytes.

Para acessar os bytes individuais de uma string, podemos indexá-la como um slice de bytes. No entanto, para obter caracteres reais, devemos usar a iteração com range ou funções do pacote unicode/utf8. Exemplo:

s := "Olá, 🌍"
fmt.Println(len(s)) // 10 (bytes)
fmt.Println(len([]rune(s))) // 5 (runes)

// Iterando sobre bytes
for i := 0; i < len(s); i++ {
    fmt.Printf("%x ", s[i])
}
// Output: 4f 6c c3 a1 2c 20 f0 9f 8c 8d

Perceba que 'á' é codificado como dois bytes (c3 a1) e o globo como quatro (f0 9f 8c 8d). O pacote unicode/utf8 oferece funções como DecodeRuneInString para decodificar runes manualmente.

range em strings

Quando usamos range em uma string, ele itera sobre runes (caracteres Unicode), não sobre bytes. A cada iteração, obtemos o índice inicial do rune (em bytes) e o próprio rune. Isso é seguro e eficiente para processar texto.

s := "Olá, 🌍"
for i, r := range s {
    fmt.Printf("índice %d: %c (U+%04X)\n", i, r, r)
}
// Output:
// índice 0: O (U+004F)
// índice 1: l (U+006C)
// índice 2: á (U+00E1)
// índice 4: , (U+002C)
// índice 5:   (U+0020)
// índice 6: 🌍 (U+1F30D)

Note que o índice pula de 2 para 4 porque 'á' ocupa 2 bytes, e de 5 para 6 porque o espaço é 1 byte. Se precisar apenas dos runes, pode ignorar o índice com for _, r := range s.

Pacote strings

O pacote strings oferece funções para manipular strings UTF-8 de forma eficiente. As principais incluem:

  • Contains(s, substr) - verifica se s contém substr.
  • Count(s, substr) - conta ocorrências não sobrepostas.
  • HasPrefix(s, prefix) / HasSuffix(s, suffix).
  • Index(s, substr) - retorna o índice do primeiro byte da primeira ocorrência.
  • Join(elems, sep) - concatena um slice de strings com separador.
  • Split(s, sep) - divide uma string em slice.
  • ToLower(s) / ToUpper(s) - conversão para minúsculas/maiúsculas (respeita Unicode).
  • Trim(s, cutset) - remove caracteres do conjunto.

Exemplo prático:

import "strings"

s := "  Olá, Mundo!  "
trimmed := strings.TrimSpace(s) // "Olá, Mundo!"
parts := strings.Split(trimmed, ", ") // ["Olá", "Mundo!"]
joined := strings.Join(parts, "...") // "Olá...Mundo!"
fmt.Println(strings.Contains(joined, "Mundo")) // true

Essas funções são otimizadas e lidam corretamente com UTF-8, sem quebrar caracteres multibyte.

Pacote strconv

O pacote strconv converte entre strings e tipos numéricos (int, float, bool) e também oferece funções para escapar/unescape strings. As funções mais usadas são:

  • Atoi(s) - converte string para int.
  • Itoa(i) - converte int para string.
  • ParseFloat(s, bitSize) - converte string para float64/float32.
  • FormatFloat(f, fmt, prec, bitSize) - formata float para string.
  • Quote(s) - retorna uma string com aspas duplas e escapes.
  • Unquote(s) - interpreta escapes em uma string entre aspas.

Exemplo:

import "strconv"

numStr := "42"
num, err := strconv.Atoi(numStr)
if err != nil {
    fmt.Println("erro:", err)
} else {
    fmt.Println(num + 1) // 43
}

f := 3.14159
fStr := strconv.FormatFloat(f, 'f', 2, 64)
fmt.Println(fStr) // "3.14"

quoted := strconv.Quote("Hello\nWorld")
fmt.Println(quoted) // "\"Hello\\nWorld\""

Cuidado: Atoi retorna erro se a string não for um número inteiro válido. Use ParseInt para bases diferentes.

Boas práticas

  • Sempre use range para iterar sobre caracteres de uma string, não indexação por byte.
  • Para contar caracteres, use utf8.RuneCountInString(s) ou len([]rune(s)).
  • Ao construir strings, prefira strings.Builder para eficiência.
  • Valide conversões com strconv sempre verificando erros.

Referências

Exercícios

  1. Escreva uma função que receba uma string e retorne a quantidade de runes (caracteres Unicode) nela contidos. Teste com a string "Hello, 世界".

    ✓ Resposta:
    func countRunes(s string) int {
        return len([]rune(s))
    }
    // Teste:
    fmt.Println(countRunes("Hello, 世界")) // 9
    
  2. Usando range, escreva um programa que imprima cada rune de uma string junto com seu índice (em bytes). Teste com "Go é show!".

    ✓ Resposta:
    s := "Go é show!"
    for i, r := range s {
        fmt.Printf("índice %d: %c\n", i, r)
    }
    // Output:
    // índice 0: G
    // índice 1: o
    // índice 2:   (espaço)
    // índice 3: é
    // índice 5:   (espaço)
    // índice 6: s
    // índice 7: h
    // índice 8: o
    // índice 9: w
    // índice 10: !
    
  3. Use o pacote strings para verificar se a string "Go é demais!" contém a palavra "demais" e substitua por "top". Imprima o resultado.

    ✓ Resposta:
    s := "Go é demais!"
    if strings.Contains(s, "demais") {
        s = strings.Replace(s, "demais", "top", 1)
    }
    fmt.Println(s) // "Go é top!"
    
  4. Converta a string "123" para inteiro usando strconv.Atoi. Em seguida, converta o inteiro 456 para string usando strconv.Itoa. Imprima a soma dos dois números (como inteiro).

    ✓ Resposta:
    num1, _ := strconv.Atoi("123")
    num2 := 456
    soma := num1 + num2
    fmt.Println(soma) // 579
    
  5. Escreva uma função que receba uma string e retorne uma nova string com todas as letras minúsculas e sem espaços no início e fim. Use funções dos pacotes strings.

    ✓ Resposta:
    func cleanString(s string) string {
        return strings.ToLower(strings.TrimSpace(s))
    }
    // Teste:
    fmt.Println(cleanString("  Olá Mundo!  ")) // "olá mundo!"