Em Go, strings são sequências imutáveis de bytes, mas a representação de texto Unicode é feita com runes (códigos de ponto Unicode). Esta aula aborda os fundamentos de UTF-8, iteração sobre strings, e os pacotes da biblioteca padrão para manipulação e conversão.

Entender a diferença entre bytes e runes é crucial para processar texto corretamente em Go, especialmente com caracteres multi-byte. Vamos ver como Go lida com codificação UTF-8 de forma nativa e eficiente.

UTF-8 em Go

Go adota UTF-8 como codificação padrão para strings. Uma string em Go é uma fatia de bytes ([]byte) que pode conter qualquer sequência de bytes, mas quando representa texto, espera-se que seja UTF-8 válido. Uma rune é um valor inteiro de 32 bits que representa um ponto de código Unicode.

Para acessar caracteres individuais, é necessário converter a string para uma fatia de runes ou usar o loop range. O tipo rune é um alias para int32 e é usado para armazenar valores Unicode.

package main

import (
	"fmt"
	"unicode/utf8"
)

func main() {
	s := "Olá, 世界"
	fmt.Println(len(s))                    // bytes: 13
	fmt.Println(utf8.RuneCountInString(s)) // runes: 8

	for i, r := range s {
		fmt.Printf("%d: %c (rune %U)\n", i, r, r)
	}
}

No exemplo, len(s) retorna o número de bytes (13), enquanto utf8.RuneCountInString retorna 8 runes. O loop range itera sobre runes, não bytes, e retorna o índice inicial de cada rune.

range em strings

O loop for range em strings itera sobre runes, decodificando UTF-8 automaticamente. A cada iteração, ele retorna o índice de byte inicial e a rune correspondente. Se a string contiver bytes inválidos, a rune é \uFFFD (caractere de substituição).

Para iterar sobre bytes, use um loop tradicional com índice. Para obter uma fatia de runes, use []rune(s).

package main

import "fmt"

func main() {
	s := "Ação"
	for i, r := range s {
		fmt.Printf("byte %d: rune %c (U+%04X)\n", i, r, r)
	}
	// byte 0: A (U+0041)
	// byte 1: ç (U+00E7)
	// byte 3: ã (U+00E3)
	// byte 5: o (U+006F)

	// Iterando sobre bytes:
	for i := 0; i < len(s); i++ {
		fmt.Printf("byte %d: %02x\n", i, s[i])
	}
}

Note que a rune 'ç' ocupa dois bytes (c3 a7) e 'ã' ocupa dois bytes (c3 a3). O range pula para o próximo índice após cada rune.

Pacote strings

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

  • Contains, Count, HasPrefix, HasSuffix
  • Index, LastIndex (buscam substrings)
  • Join, Split, Replace, ToUpper, ToLower
  • Trim, TrimSpace

Essas funções operam em runes, não bytes, respeitando caracteres multi-byte.

package main

import (
	"fmt"
	"strings"
)

func main() {
	s := "  Olá, Mundo!  "
	fmt.Println(strings.TrimSpace(s))          // "Olá, Mundo!"
	fmt.Println(strings.ToUpper(s))            // "  OLÁ, MUNDO!  "
	fmt.Println(strings.Replace(s, "Mundo", "Go", 1)) // "  Olá, Go!  "
	fmt.Println(strings.Split("a,b,c", ","))  // ["a" "b" "c"]
}

Pacote strconv

O pacote strconv converte strings para tipos básicos e vice-versa. Funções importantes:

  • Atoi e Itoa para inteiros
  • ParseInt, ParseFloat, ParseBool
  • FormatInt, FormatFloat, FormatBool
  • Quote e Unquote para strings com escape

Essas funções lidam com representações textuais de dados.

package main

import (
	"fmt"
	"strconv"
)

func main() {
	i, err := strconv.Atoi("42")
	if err != nil {
		panic(err)
	}
	fmt.Println(i + 1) // 43

	s := strconv.Itoa(123)
	fmt.Println(s) // "123"

	f, _ := strconv.ParseFloat("3.14", 64)
	fmt.Println(f) // 3.14

	q := strconv.Quote("Hello, \"世界\"")
	fmt.Println(q) // "\"Hello, \\"世界\\"\""
}

Referências

Exercícios

  1. Crie uma função que receba uma string e retorne o número de runes (caracteres Unicode) nela.

    ✓ Resposta:
    func countRunes(s string) int {
        return utf8.RuneCountInString(s)
    }
  2. Escreva um programa que use range para imprimir cada rune de uma string com seu índice de byte e valor Unicode.

    ✓ Resposta:
    package main
    
    import "fmt"
    
    func main() {
        s := "Go é show!"
        for i, r := range s {
            fmt.Printf("byte %d: %c (U+%04X)\n", i, r, r)
        }
    }
  3. Use o pacote strings para verificar se uma string contém a substring "Go" e, se sim, substitua por "Python".

    ✓ Resposta:
    package main
    
    import (
        "fmt"
        "strings"
    )
    
    func main() {
        s := "I love Go!"
        if strings.Contains(s, "Go") {
            s = strings.Replace(s, "Go", "Python", 1)
        }
        fmt.Println(s)
    }
  4. Converta a string "123" para inteiro usando strconv, adicione 10 e imprima o resultado.

    ✓ Resposta:
    package main
    
    import (
        "fmt"
        "strconv"
    )
    
    func main() {
        n, _ := strconv.Atoi("123")
        fmt.Println(n + 10)
    }
  5. Escreva uma função que receba uma string com espaços extras e retorne uma versão com espaços simples e trimada.

    ✓ Resposta:
    func normalizeSpaces(s string) string {
        fields := strings.Fields(s)
        return strings.Join(fields, " ")
    }