Mapas, strings e manipulação de texto em Go

Capítulo 5

Tempo estimado de leitura: 8 minutos

+ Exercício

Mapas (map): dicionários em Go

Em Go, map é uma estrutura de dados que associa uma chave a um valor (como um dicionário). Você escolhe o tipo da chave e do valor: map[K]V. As chaves precisam ser de um tipo comparável (por exemplo: string, int, ponteiros, structs comparáveis). Slices, maps e funções não podem ser chaves.

Criando mapas

Existem duas formas comuns: usando make (map vazio pronto para uso) ou um literal (já com valores).

package main

import "fmt"

func main() {
	// 1) make: cria um map vazio (não-nil)
	idades := make(map[string]int)
	idades["Ana"] = 28
	idades["Bruno"] = 31

	// 2) literal: cria e inicializa
	capitais := map[string]string{
		"BR": "Brasília",
		"PT": "Lisboa",
	}

	fmt.Println(idades, capitais)
}

Um detalhe importante: o valor zero de um map é nil. Um map nil não pode receber atribuições (vai causar panic), mas pode ser lido (retorna valor zero do tipo do valor).

var m map[string]int
fmt.Println(m["x"]) // 0 (leitura ok)
// m["x"] = 1      // panic: assignment to entry in nil map

Leitura, escrita, remoção e tamanho

idades := map[string]int{"Ana": 28}
idades["Ana"] = 29          // atualiza
idades["Bruno"] = 31        // insere
fmt.Println(idades["Ana"])  // lê
fmt.Println(len(idades))     // quantidade de pares

delete(idades, "Bruno")     // remove (se não existir, não dá erro)

Verificando existência: padrão value, ok

Ao ler uma chave que não existe, você recebe o valor zero do tipo do valor. Isso pode ser ambíguo quando o valor zero é um valor válido (ex.: 0 em contagens). Para resolver, use o padrão value, ok.

idades := map[string]int{"Ana": 0}

v1 := idades["Ana"]
v2 := idades["Carla"]
fmt.Println(v1, v2) // 0 0 (ambíguo)

if v, ok := idades["Ana"]; ok {
	fmt.Println("Ana existe, valor:", v)
}

if v, ok := idades["Carla"]; !ok {
	fmt.Println("Carla não existe, valor lido (zero):", v)
}

Iteração e implicações de ordem

Você pode iterar com range. Porém, a ordem de iteração em mapas não é garantida e pode variar entre execuções. Isso é intencional: não escreva código que dependa da ordem de um map.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

Baixar o aplicativo

capitais := map[string]string{"BR": "Brasília", "PT": "Lisboa", "AR": "Buenos Aires"}

for k, v := range capitais {
	fmt.Println(k, v)
}

Quando você precisa de ordem determinística (por exemplo, para exibir ou testar), extraia as chaves, ordene e depois acesse o map.

package main

import (
	"fmt"
	"sort"
)

func main() {
	capitais := map[string]string{"BR": "Brasília", "PT": "Lisboa", "AR": "Buenos Aires"}

	keys := make([]string, 0, len(capitais))
	for k := range capitais {
		keys = append(keys, k)
	}
	sort.Strings(keys)

	for _, k := range keys {
		fmt.Println(k, capitais[k])
	}
}

Strings, bytes e runes

Em Go, string é uma sequência imutável de bytes. Normalmente esses bytes representam texto em UTF-8. Isso traz duas consequências práticas: (1) índices em string acessam bytes, não “caracteres”; (2) alguns caracteres (como “ç”, “á”, “你”, “🙂”) ocupam mais de 1 byte.

Bytes vs runes

byte é um alias de uint8 e representa um byte. rune é um alias de int32 e costuma representar um ponto de código Unicode. Ao iterar uma string com range, Go decodifica UTF-8 e entrega runes.

s := "café"

fmt.Println(len(s)) // tamanho em bytes
fmt.Println(s[0])   // byte (uint8) do primeiro byte

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

for idx, r := range s {
	fmt.Printf("rune em byteIndex=%d: %c (U+%04X)\n", idx, r, r)
}

Note que len(s) conta bytes, não runes. Para contar runes, você pode converter para []rune (custo extra) ou usar utf8.RuneCountInString.

package main

import (
	"fmt"
	"unicode/utf8"
)

func main() {
	s := "café"
	fmt.Println("bytes:", len(s))
	fmt.Println("runes:", utf8.RuneCountInString(s))
	fmt.Println("runes via []rune:", len([]rune(s)))
}

Iteração correta para texto

Para processar texto “por caractere”, use for _, r := range s. Se você usar indexação direta (s[i]), estará processando bytes, o que pode quebrar caracteres multibyte.

// Exemplo: transformar letras em maiúsculas (Unicode-aware)
package main

import (
	"fmt"
	"strings"
)

func main() {
	s := "Olá, mundo"
	fmt.Println(strings.ToUpper(s))
}

Quando você precisa manipular “caracteres” manualmente (por exemplo, remover acentos, filtrar letras), normalmente você trabalha com runes e pacotes como unicode.

package main

import (
	"fmt"
	"unicode"
)

func main() {
	s := "Go 1.22!"
	out := make([]rune, 0, len(s))
	for _, r := range s {
		if unicode.IsLetter(r) {
			out = append(out, r)
		}
	}
	fmt.Println(string(out)) // "Go"
}

Conversões comuns

  • string[]byte: útil para I/O e manipulação em nível de bytes.
  • string[]rune: útil para indexar por “caractere” (rune), com custo de alocação.
  • []byte/[]runestring: cria uma nova string.
s := "café"
bs := []byte(s)
rs := []rune(s)

fmt.Println(bs)        // bytes UTF-8
fmt.Println(rs)        // pontos de código
fmt.Println(string(bs))
fmt.Println(string(rs))

Construção eficiente de texto com strings.Builder

Concatenar strings repetidamente com + em loops pode gerar muitas alocações, porque strings são imutáveis: cada concatenação cria uma nova string. Para montar texto incrementalmente, prefira strings.Builder.

Passo a passo: montando um relatório

package main

import (
	"fmt"
	"strings"
)

func main() {
	linhas := []string{"item=maçã", "item=banana", "item=café"}

	var b strings.Builder
	b.Grow(64) // opcional: reserva capacidade para reduzir realocações

	b.WriteString("Relatório\n")	
	for i, ln := range linhas {
		b.WriteString(fmt.Sprintf("%d) %s\n", i+1, ln))
	}

	texto := b.String()
	fmt.Println(texto)
}

Observação: fmt.Sprintf é conveniente, mas pode ser mais custoso. Quando possível, escreva partes diretamente no builder (por exemplo, usando strconv.AppendInt em um buffer de bytes, ou fmt.Fprintf(&b, ...) quando a clareza for prioridade).

package main

import (
	"fmt"
	"strings"
)

func main() {
	var b strings.Builder
	fmt.Fprintf(&b, "Olá %s, você tem %d mensagens.\n", "Ana", 3)
	fmt.Print(b.String())
}

Tarefas práticas: parsing e contagem de frequência

Tarefa 1: parsing simples de pares chave=valor

Objetivo: receber uma lista de linhas no formato chave=valor, ignorar linhas inválidas e construir um map[string]string. Você vai praticar: strings.SplitN, strings.TrimSpace, map e o padrão value, ok.

Passo a passo

  • Crie um map com make.
  • Para cada linha: remova espaços com TrimSpace.
  • Ignore vazias e comentários (por exemplo, começando com #).
  • Use SplitN(linha, "=", 2) para separar em no máximo 2 partes.
  • Se não houver 2 partes, a linha é inválida.
  • Faça TrimSpace em chave e valor.
  • Se a chave já existir, decida se sobrescreve ou mantém (mostrado abaixo: sobrescreve).
package main

import (
	"fmt"
	"strings"
)

func parseKV(lines []string) map[string]string {
	out := make(map[string]string)
	for _, line := range lines {
		line = strings.TrimSpace(line)
		if line == "" || strings.HasPrefix(line, "#") {
			continue
		}

		parts := strings.SplitN(line, "=", 2)
		if len(parts) != 2 {
			continue
		}

		k := strings.TrimSpace(parts[0])
		v := strings.TrimSpace(parts[1])
		if k == "" {
			continue
		}

		out[k] = v
	}
	return out
}

func main() {
	lines := []string{
		"host = localhost",
		"port= 5432",
		"# comentário",
		"invalida",
		"user=ana",
		"user=bruno", // sobrescreve
	}

	cfg := parseKV(lines)
	if v, ok := cfg["user"]; ok {
		fmt.Println("user:", v)
	}
	fmt.Println(cfg)
}

Tarefa 2: contagem de frequência de palavras

Objetivo: dado um texto, contar quantas vezes cada palavra aparece. Você vai praticar: normalização de texto, iteração correta, map com contadores e ordenação para exibir resultados.

Decisões importantes

  • Normalização: converter para minúsculas para não separar “Go” de “go”.
  • Separação: você pode usar strings.Fields (separação por espaços) ou um scanner mais robusto. Aqui vamos fazer uma tokenização simples por runes: manter letras e números e trocar o resto por espaço.
  • Contagem: use map[string]int e incremente: freq[w]++.
  • Ordem: para imprimir em ordem, ordene as chaves.

Passo a passo com tokenização por runes

package main

import (
	"fmt"
	"sort"
	"strings"
	"unicode"
)

func normalizeToWords(s string) []string {
	// 1) minúsculas
	s = strings.ToLower(s)

	// 2) substituir pontuação por espaços, preservando letras e números
	var b strings.Builder
	b.Grow(len(s))
	for _, r := range s {
		if unicode.IsLetter(r) || unicode.IsNumber(r) {
			b.WriteRune(r)
		} else {
			b.WriteByte(' ')
		}
	}

	// 3) separar por espaços (colapsa múltiplos espaços)
	return strings.Fields(b.String())
}

func wordFrequency(text string) map[string]int {
	freq := make(map[string]int)
	for _, w := range normalizeToWords(text) {
		freq[w]++
	}
	return freq
}

func main() {
	text := "Go é Go. Café, café! GO? 2024 é 2024."
	freq := wordFrequency(text)

	keys := make([]string, 0, len(freq))
	for k := range freq {
		keys = append(keys, k)
	}
	sort.Strings(keys)

	for _, k := range keys {
		fmt.Printf("%s: %d\n", k, freq[k])
	}
}

Extensões sugeridas (para praticar mais)

  • Mostrar as N palavras mais frequentes (exige ordenar por valor; você pode criar uma slice de pares e ordenar).
  • Manter também o total de palavras e o total de palavras únicas.
  • Tratar hífen como parte da palavra (por exemplo, “auto-escola”).
  • Contar caracteres (runes) e bytes do texto e comparar.
  • Gerar uma saída formatada usando strings.Builder (por exemplo, uma tabela em texto).

Agora responda o exercício sobre o conteúdo:

Ao contar a frequência de palavras em um texto em Go, qual abordagem ajuda a evitar diferenças por maiúsculas/minúsculas e também impede que pontuação quebre a contagem?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Normalizar para minúsculas evita separar "Go" de "go". Iterar por range processa runes (UTF-8), permitindo substituir pontuação por espaços e então usar strings.Fields para obter palavras consistentes.

Próximo capitúlo

Tipos em Go: aliases, conversões, ponteiros e composição

Arrow Right Icon
Capa do Ebook gratuito Go (Golang) para Iniciantes: Fundamentos, Concorrência e Estrutura de Projetos
28%

Go (Golang) para Iniciantes: Fundamentos, Concorrência e Estrutura de Projetos

Novo curso

18 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.