Pular para o conteúdo

Hardware e sistemas operacionais · parte 4 de 27

Texto, ASCII, Unicode e UTF-8: como uma letra vira número

Como uma letra vira número, por que o mesmo texto ocupa bytes diferentes e por que medir tamanho de strings pode enganar no dia a dia.

iniciantePublicada em · 30 min de leitura

Tamanho da fonte:
Largura da coluna:
Tom:
Espaço entre linhas:
Fonte:

Texto, ASCII, Unicode e UTF-8: como uma letra vira número

Você digita “João” no celular, o texto some para dentro de um servidor, volta do banco de dados e aparece na tela de outra pessoa, num navegador de outro país, sem nenhum erro de acento. Parece trivial, e é um dos milagres mais silenciosos da computação. Por trás disso existe uma convenção de que cada letra é um número, que esse número é gravado em bytes, e que todo mundo concorda com a mesma tabela. Quando alguém esquece uma dessas três coisas, a palavra “João” vira “João” na tela e ninguém entende por quê.

A gente vai começar pelo problema real, que é anterior aos computadores, e depois descer até o byte. Nesta aula você vai ver como o telégrafo resolveu o texto com um código de cinco pulsos, por que o ASCII de sete bits não bastou para o português, como o Unicode separou o “que letra é” do “como guardar”, e por que o UTF-8 virou o padrão da web. Os exemplos rodam, e a saída de cada um está colada no texto. Pré-requisito: saber o que é um bit e converter binário e hexadecimal, o que está em Bits e binário.

Mapa da aula

A aula começa pela ideia, passa pela história que a motivou, pelos exemplos e pelo mecanismo do UTF-8 e do Unicode. A seção 5 cuida das bordas: UTF-16, BOM, normalização, emoji, a ordenação de palavras e o caminho de um texto digitado até a tela. Os erros comuns e os exercícios vêm depois, seguidos do glossário e das fontes.

1. Uma letra é um número

Pense em uma sala de aula em que cada aluno tem um número na chamada. A professora não grava o nome de ninguém no caderno: ela escreve o número, e quem sabe a chamada consegue voltar do número para o nome. Um computador faz o mesmo com o texto. A letra “A” é guardada como o número 65, a letra “a” como 97, o espaço como 32. Quem lê o arquivo precisa conhecer a chamada, ou seja, a tabela que diz qual número é qual letra. A chamada, porém, só diz qual número é qual letra. Não diz como esse número é escrito no caderno, e essa parte é a codificação, que a aula explica mais adiante.

Tecnicamente, o conjunto de caracteres que uma convenção reconhece se chama repertório, e cada caractere recebe um número inteiro chamado ponto de código. A forma de gravar esses números em bytes se chama codificação. Essa separação é a chave da aula. O ponto de código responde “que caractere é este”. A codificação responde “como eu guardo esse número em bytes”. Quando as duas coisas se misturam na cabeça, aparecem quase todos os bugs de texto.

Um byte tem oito bits e cobre 256 valores, de 0 a 255. Se cada caractere coubesse em um byte, o texto seria simples, e o problema seria só de tamanho da tabela. O que torna a história longa é que o mundo tem muito mais que 256 caracteres, e que os primeiros sistemas precisavam caber em muito menos.

2. Para que serve: o telégrafo e o código de cinco pulsos

Antes do computador, a transmissão de texto à distância era telégrafo. Na década de 1870, Émile Baudot criou um código em que cada caractere era transmitido com cinco pulsos elétricos. A máquina dele foi patenteada em 1874, mas o código em si não foi patenteado. Cinco bits dão 32 combinações (2 elevado a 5). Não é o bastante para 26 letras, dez dígitos e a pontuação ao mesmo tempo, então o equipamento alterna entre um modo de letras e um modo de figuras, e a mesma combinação de pulsos significa coisas diferentes em cada modo. Essa troca de modo é a primeira vez que aparece nesta aula a ideia de que o significado de um código depende do contexto. A história também tem uma disputa de prioridade: Louis Victor Mimault levou o caso à justiça em 1877, e a corte chegou a dar razão a ele antes de a decisão ser revista (fonte: verbete Baudot code da Wikipedia, consultado em 1 de novembro de 2026, ver Fontes).

A figura abaixo resume a sequência de codificações que vieram depois. Cada uma surgiu porque a anterior não cabia no que as pessoas precisavam escrever.

Cinco eras de codificação de texto, do Baudot dos anos 1870 ao UTF-8 de 1992: cada passo surgiu porque o anterior não cabia nos caracteres necessários
figura 1 · Eras de codificação de texto: do telégrafo ao UTF-8

A figura lê da esquerda para a direita. O ASCII, de 1963, usa sete bits e 128 posições, mas a edição de 1963 não tinha letras minúsculas: elas só entraram na revisão de 1967, e por isso o inglês normal só fica completo a partir dela. Quando computadores de outros países passaram a usar o mesmo sistema, cada um precisou de letras com acento, e apareceram as páginas de código, tabelas diferentes que reaproveitavam as mesmas posições para letras distintas. O Unicode surgiu para acabar com essa confusão, e o UTF-8 é a forma de gravar o Unicode que a web e a maioria dos sistemas usam hoje.

O ASCII merece uma olhada de perto porque ainda está em todo lugar. A edição de 1963, publicada como ASA X3.4-1963, deixou 28 posições sem significado atribuído, e as letras minúsculas foram definidas na revisão de 1967, publicada como USAS X3.4-1967 (fontes: verbete ASCII da Wikipedia e ascii-code.com, ambos consultados em 1 de novembro de 2026; o texto original da norma não foi aberto). O comitê escolheu sete bits para reduzir o custo da transmissão. O oitavo bit ficou livre, e podia servir para verificação de erro por paridade. Quando as páginas de código passaram a usar esse bit, elas ocuparam as posições de 128 a 255 para as letras acentuadas. A figura seguinte mostra a letra A em sete bits, e também a letra é, que tem o número 233 e portanto precisa do oitavo bit.

A letra A em ASCII ocupa sete bits com o oitavo zerado, e a letra é em Latin-1 usa o oitavo bit aceso para chegar ao número 233
figura 2 · ASCII e Latin-1: a letra A em sete bits e a letra é com o oitavo bit aceso

O oitavo bit sozinho já divide o mundo. Com ele zerado, o byte é ASCII e cabe nas 128 posições do padrão. Com ele aceso, o byte passa a representar uma tabela diferente, e a mesma sequência de bits pode significar um acento em uma página de código e outro símbolo em outra. Essa ambiguidade é o motivo de quase toda a confusão que vem a seguir.

3. Exemplos em contextos diferentes

O primeiro contexto é o da língua. Para escrever português, você precisa de ç, ã, é e de dezenas de outros caracteres. Quando um sistema grava “João” em uma codificação e lê em outra, cada byte acentuado vira outro caractere. A figura 3 mostra o caso clássico, o mojibake (nome que se dá ao texto que aparece com caracteres trocados), em que os bytes de “João” em UTF-8 são lidos como se fossem Latin-1. Latin-1 (ISO 8859-1) é uma tabela de um byte por caractere, com as 128 primeiras posições iguais ao ASCII e as demais ocupadas por letras acentuadas de línguas da Europa ocidental. A seção 4 mostra a tabela em detalhe.

Linha do tempo de um erro: o editor grava João em UTF-8 como cinco bytes, e o leitor, usando Latin-1, mostra João na tela
figura 3 · Mojibake: João gravado em UTF-8 e lido como Latin-1

O segundo contexto é o do banco de dados. Se a conexão com o banco usa uma codificação e a aplicação espera outra, o texto que entra e o que sai não batem. Um exemplo: a aplicação grava “João” em UTF-8, o banco guarda os cinco bytes, e a aplicação lê esses bytes como Latin-1 e mostra “João” para o usuário, numa tela que parece correta por fora. O terceiro contexto é o da rede: um navegador decide como ler a página pelo cabeçalho de resposta ou pela marcação do próprio documento. Se nenhum dos dois informa a codificação, o navegador usa uma heurística, uma regra de palpite que pode errar.

O quarto contexto é o emoji. Um emoji de rosto ocupa quatro bytes em UTF-8, e algumas figuras, como a família, são compostas de vários pontos de código. A seção 5 volta a esse caso com números medidos. Por enquanto, guarde que o que você digita nem sempre corresponde a um só número.

4. Como funciona por dentro: Unicode e UTF-8

O Unicode é o repertório. Ele atribui a cada caractere um ponto de código escrito no formato U+ seguido de hexadecimal. A letra é tem o ponto U+00E9, o euro tem U+20AC, e o emoji de rosto sorrindo tem U+1F600. O espaço de pontos de código vai de U+0000 até U+10FFFF, o que dá 1.114.112 posições, organizadas em 17 planos de 65.536 cada. Plano é um bloco de 65.536 pontos de código, numerado de 0 a 16. Nem todas as posições têm um caractere.

A versão 17.0 do Unicode foi anunciada em 9 de setembro de 2025 e acrescenta 4.803 caracteres gráficos (fonte: blog.unicode.org, ver Fontes). A página de estatísticas da versão traz números que se confundem com facilidade. O número de caracteres gráficos (letras, símbolos e marcas de acento) mais os de formato, que são invisíveis mas têm função, dá 159.801. Esse é o número de caracteres. Logo abaixo aparece “Total Assigned”, com 297.334. Esse total não é só de caracteres: ele soma os 159.801 acima, 137.468 pontos de uso privado (posições que cada programa pode usar como quiser, sem que o padrão dê nome a elas) e 65 controles, como a quebra de linha. A conta fecha: 159.629 + 172 + 65 + 137.468 = 297.334. Os 2.048 substitutos (de D800 a DFFF, usados só para montar pares no UTF-16) e os 66 não-caracteres (posições reservadas para uso interno de programas) ficam de fora desse total. Sobram 814.664 pontos reservados, que ainda não têm uso.

O UTF-8 é a codificação desse repertório em bytes. Ele tem uma regra simples que cabe em uma tabela. Um ponto de código entre U+0000 e U+007F ocupa um byte, com o bit alto zerado. Entre U+0080 e U+07FF ocupa dois bytes. Entre U+0800 e U+FFFF ocupa três, e entre U+10000 e U+10FFFF ocupa quatro. O primeiro byte de cada sequência anuncia quantos bytes vêm, por meio dos seus primeiros bits: 0 para um byte, 110 para dois, 1110 para três e 11110 para quatro. Os bytes seguintes sempre começam com 10, e são chamados de bytes de continuação.

A figura abaixo desmonta os três exemplos de bits. A letra é ocupa dois bytes, o euro três e o emoji quatro. Os campos cinza são os prefixos, que dizem o tamanho, e os verdes são a carga, que guarda os bits do ponto de código. Repare que o total de bits de carga é o que importa: 11 para é, 16 para o euro e 21 para o emoji.

Bits de é, € e 😀 em UTF-8: os prefixos 110, 1110 e 11110 marcam o tamanho, e cada byte de continuação começa com 10 antes da carga
figura 4 · Bits de UTF-8 para é, € e 😀: prefixo e carga em cada byte

Leia a figura pela primeira linha. A letra é é U+00E9, que tem o valor decimal 233 e em binário é 11101001. Com zeros à esquerda para completar 11 bits, o número fica 00011101001. Esses 11 bits se encaixam em dois bytes: o primeiro leva 110 de prefixo e cinco bits de carga, e o segundo leva 10 de prefixo e seis bits de carga. O resultado, 110 00011 e 10 101001, dá os bytes C3 A9. O mesmo método funciona para o euro. U+20AC é 0010 000010 101100 em 16 bits, que se dividem em 4, 6 e 6 bits, e o resultado é E2 82 AC. O emoji tem 21 bits, 0 0001 1111 0110 0000 0000, que se dividem em 3, 6, 6 e 6, e o resultado é F0 9F 98 80. Os três resultados são os que o Python grava: 'é'.encode('utf-8') dá c3 a9, '€'.encode('utf-8') dá e2 82 ac e o emoji dá f0 9f 98 80.

A regra do UTF-8 tem uma propriedade que vale a pena guardar: a autossincronização. Se você cair no meio de um caractere, basta recuar até achar um byte que não comece com 10. Esse byte é o início do caractere onde você caiu, e a partir dele a decodificação recomeça sem ler o arquivo inteiro. Por exemplo, se você cair no a9 de c3 a9, recua um byte, encontra o c3, que começa com 11, e sabe que ali começa o é. Essa propriedade é o que permite a um programa pular para uma posição do texto e continuar decodificando sem se perder.

Também vale a compatibilidade com ASCII. Qualquer byte abaixo de 128 é um caractere ASCII com o mesmo significado, e por isso um arquivo só de inglês em UTF-8 é byte a byte idêntico ao mesmo arquivo em ASCII. Essa foi uma das decisões que tornou o UTF-8 aceitável para o mundo inteiro. Ken Thompson projetou o UTF-8 em 1992, com Rob Pike acompanhando o trabalho. Pike conta, em seu relato, que a ideia foi desenhada num jogo americano de papel de uma lanchonete em New Jersey, numa noite de setembro de 1992 ou por aí (tradução livre; fonte: transcrição do relato feita por Markus Kuhn, ver Fontes). Uma busca no arquivo de mensagens, feita por Russ Cox, encontra o primeiro rascunho datado de 2 de setembro de 1992. A aula não depende do dia exato. A especificação oficial é a RFC 3629, de novembro de 2003, que restringiu o UTF-8 ao intervalo do Unicode e proibiu os substitutos (fonte: rfc-editor.org, ver Fontes).

A figura seguinte mostra as três codificações da mesma palavra “João”, lado a lado. O ã ocupa um byte em Latin-1 (E3) e dois em UTF-8 (C3 A3). Em UTF-16 big endian, cada letra ocupa uma unidade de 16 bits, ou seja, 2 bytes. Por isso o texto passa de 4 bytes em Latin-1 ou 5 em UTF-8 para 8 bytes em UTF-16.

João em Latin-1 com quatro bytes, em UTF-8 com cinco e em UTF-16 big endian com oito, com uma unidade de dois bytes por letra
figura 5 · João em Latin-1, UTF-8 e UTF-16 big endian: quatro, cinco e oito bytes

Latin-1 e cp1252 merecem uma definição à parte, porque a mesma sequência de bytes pode significar coisas diferentes nas duas. Latin-1 (ISO 8859-1) tem 256 posições de um byte cada, e não tem o euro. A página de código cp1252, do Windows, usa posições de 128 a 159, que em Latin-1 são controles invisíveis, para o euro e outros sinais. O programa a seguir, rodado com Python 3.12.8, mostra a diferença. “é” vira o byte E9 em Latin-1, e ”€” vira 80 em cp1252, enquanto ”€” em Latin-1 dá erro. A última linha mostra o mojibake do euro: os três bytes UTF-8 do € lidos como cp1252 viram três caracteres.

print("'é' em latin-1 :", "é".encode("latin-1"))
print("'€' em cp1252  :", "€".encode("cp1252"))
try:
    "€".encode("latin-1")
except UnicodeEncodeError as e:
    print("'€' em latin-1 : erro ->", e.reason)
print("João em UTF-8 lido como latin-1:", "João".encode("utf-8").decode("latin-1"))
print("euro UTF-8 lido como cp1252    :", "€".encode("utf-8").decode("cp1252"))
'é' em latin-1 : b'\xe9'
'€' em cp1252  : b'\x80'
'€' em latin-1 : erro -> ordinal not in range(256)
João em UTF-8 lido como latin-1: João
euro UTF-8 lido como cp1252    : €

Sem saber qual das duas foi usada, o mesmo byte pode virar um caractere ou outro, e isso é mais uma fonte de mojibake.

O decodificador é o programa que faz o caminho inverso: recebe bytes e devolve pontos de código. A figura seguinte mostra a decisão dele. Antes de contar os bytes que faltam, o decodificador recusa os bytes que nunca podem começar um caractere: 10xxxxxx, que só aparece como continuação, C0 e C1, que codificariam em dois bytes um valor que cabe em um, e F5 a FF, que passariam de U+10FFFF. Depois, o primeiro byte diz quantas continuações esperar (110, 1110 ou 11110). Cada continuação precisa começar com 10, e o valor montado precisa estar no intervalo válido, sem sobras e sem substitutos. Se algo falha, o decodificador devolve U+FFFD em vez de quebrar. A figura mostra essa decisão e essa validação, não tudo o que um decodificador de produção faz: por exemplo, ela não cobre o fim do fluxo no meio de um caractere.

Fluxo do decodificador UTF-8: o primeiro byte diz quantas continuações esperar, e o caractere só sai se elas começam com 10 e o valor é válido
figura 6 · Decodificador UTF-8: o primeiro byte diz quantas continuações esperar, e o carac

Para ver o fluxo em um caso válido, siga o é, que é c3 a9. O byte c3 começa com 110, então faltam 1 byte de continuação. O a9 começa com 10, então entra com seus seis bits, 101001. Juntando 00011 com 101001, o valor é 00011101001, que é U+00E9. Esse valor é válido, e o caractere é emitido. O programa Go, mais adiante nesta seção, mostra o caso de um byte inválido, o FF, que vira U+FFFD.

5. Detalhes e bordas

Com o UTF-8 entendido, as bordas ficam mais fáceis. A primeira é o UTF-16, usado pelo Java, pelo JavaScript e internamente pelo Windows. Ele grava cada unidade em 16 bits. Os pontos do plano básico (até U+FFFF) ocupam uma unidade, e os do resto do mundo ocupam duas, com os chamados substitutos: um par de unidades entre D800 e DFFF que, juntas, representam um ponto acima de U+FFFF. O intervalo D800 a DFFF é reservado para isso e não representa caractere nenhum sozinho. A conta do emoji de rosto mostra como o par é montado. U+1F600 menos U+10000 dá 0xF600. Os 10 bits altos desse número (0x3D) somados a D800 dão D83D, e os 10 bits baixos (0x200) somados a DC00 dão DE00. Por isso o emoji vira o par D83D DE00.

A figura abaixo mostra o mesmo emoji em UTF-16 com a marca de ordem de bytes (BOM, U+FEFF) no início, em big endian e em little endian. Big endian grava o byte mais significativo primeiro, e little endian grava o menos significativo primeiro. A diferença aparece só na ordem dos bytes dentro de cada unidade, e o BOM é o que permite ao leitor descobrir qual das duas ordens foi usada.

Emoji de rosto em UTF-16 com BOM: em big endian os bytes são FE FF D8 3D DE 00, e em little endian, FF FE 3D D8 00 DE, com os mesmos substitutos
figura 7 · UTF-16 do emoji 😀: dois substitutos com BOM em big endian e em little endian

O BOM serve para o leitor descobrir a ordem dos bytes. Ele é útil em UTF-16, onde big e little endian são ambíguos, e é desnecessário em UTF-8, que não tem ordem de bytes. Um BOM no início de um arquivo UTF-8 é um sinal de herança de programas antigos, e alguns leitores o mostram como um caractere invisível que estraga o primeiro campo de um CSV. Se você recebe um arquivo assim, o sintoma é um cabeçalho que não bate com o nome da coluna.

A segunda borda é a normalização. A letra é pode ser escrita de duas formas: como o ponto único U+00E9, ou como a letra e seguida do acento combinante U+0301. As duas são o mesmo caractere para o olho, e são sequências diferentes para o computador. Por isso a comparação de strings falha e a busca não encontra o nome. A figura mostra as duas formas, chamadas NFC (composta) e NFD (decomposta), com os números de cada uma.

A letra é em duas formas: NFC como um ponto único de 2 bytes em UTF-8 e NFD como a letra e mais o acento combinante, com 3 bytes
figura 8 · A letra é em duas formas: NFC com um ponto de código e NFD com dois

Os números abaixo, rodados em Python, mostram o problema com clareza. Os dois literais usam escapes (é e é) para que a diferença fique visível no código. A função unicodedata.normalize converte uma forma na outra, mas nenhuma das duas é mais correta que a outra. A escolha precisa ser feita de propósito, e a maioria dos sistemas usa NFC.

import unicodedata
pre = "é"   # é como um ponto de código só (NFC)
dec = "é"  # e + acento combinante U+0301 (NFD)
print("pre == dec ?", pre == dec, "| len pre =", len(pre), "| len dec =", len(dec))
print("NFD(pre) == dec ?", unicodedata.normalize("NFD", pre) == dec)
print("len NFC(dec) =", len(unicodedata.normalize("NFC", dec)))
pre == dec ? False | len pre = 1 | len dec = 2
NFD(pre) == dec ? True
len NFC(dec) = 1

A terceira borda é o grafema, o que o olho vê como um caractere. Um grafema pode ser formado por vários pontos de código. A família de emoji é um exemplo: três pessoas unidas por dois caracteres invisíveis chamados ZWJ (joiner de largura zero, U+200D). O terminal que sabe juntar esses pontos mostra uma figura. O que não sabe mostra as três pessoas separadas, ou um quadrado. A figura abaixo mostra a conta.

Família de emoji em cinco pontos de código, unidos por dois ZWJ: um só grafema, que ocupa 18 bytes em UTF-8
figura 9 · Família de emoji: três pessoas de 4 bytes unidas por dois ZWJ de 3 bytes, 18 byt

Conte as figuras para entender o padrão. Três pessoas são três pontos, e os dois ZWJ são dois pontos, então o resultado tem cinco pontos de código. Em UTF-8, cada pessoa ocupa 4 bytes e cada ZWJ ocupa 3, e a conta dá 3 × 4 + 2 × 3 = 18 bytes. Em UTF-16, as pessoas ocupam 2 unidades cada e o ZWJ ocupa 1, o que dá 3 × 2 + 2 × 1 = 8 unidades. Um texto como ”👍🏽” mostra o mesmo fenômeno com menos pontos: o polegar mais o modificador de pele, que é um ponto separado, compõem um grafema só. A figura abaixo mostra esse caso com as três camadas lado a lado.

Um emoji com tom de pele visto em três camadas: um grafema que o olho lê como um caractere, dois pontos de código e oito bytes em UTF-8
figura 10 · Um mesmo texto em três camadas: grafema, pontos de código e bytes

Quem precisa contar caracteres para cortar um texto em uma interface deve contar grafemas, e não pontos. A biblioteca padrão do JavaScript faz isso com Intl.Segmenter.

O quarto ponto é a comparação e a ordenação. Ordenar por ponto de código coloca “Z” antes de “a” e “é” depois de “zebra”, porque o número de “é” é maior que o de “z”. A ordenação que o português espera vem de regras de collation (regras de ordenação de um idioma, que dizem como comparar letras acentuadas e maiúsculas). O programa de JavaScript a seguir mostra a diferença entre as duas ordens. O Array.sort() do JavaScript compara por unidade UTF-16, e para letras do plano básico isso coincide com o ponto de código.

const palavras = ["zebra", "é", "e", "ê", "Z", "a"];
console.log("codigo:", [...palavras].sort());
console.log("pt-BR :", [...palavras].sort(new Intl.Collator("pt-BR").compare));
codigo: [ 'Z', 'a', 'e', 'zebra', 'é', 'ê' ]
pt-BR : [ 'a', 'e', 'é', 'ê', 'Z', 'zebra' ]

Os dois resultados são corretos, cada um para o seu critério. O erro está em usar o primeiro quando o usuário espera o segundo, o que acontece em qualquer lista ordenada que aparece na tela.

Por fim, a comparação por código parece simples, e é a causa de um bug silencioso. O Go compara strings byte a byte, e o byte de “z” (0x7A) é menor que o primeiro byte de “é” (0xC3), por isso “z” < “é” é verdadeiro, e “é” < “z” é falso. A linha final da saída do programa Go, mais abaixo, mostra isso.

A figura seguinte mostra a conta de tamanhos em quatro linguagens, com o mesmo texto. As barras contam unidades de cada linguagem, e não medem desempenho.

Barras de Go, Python, JavaScript e Java para o emoji de rosto e o polegar com tom de pele: cada linguagem conta outra unidade, e o mesmo texto muda de tamanho
figura 11 · Tamanho de um emoji e de um emoji com tom de pele em Go, Python, JavaScript e Ja

Esse gráfico tem uma lição prática. Quando você escreve len(s) em Python, s.length em JavaScript ou len(s) em Go, cada um responde uma pergunta diferente: pontos de código, unidades UTF-16 e bytes. O número é correto para cada linguagem e errado se você esperava outra coisa. Os valores do gráfico foram medidos com Go 1.27.2, OpenJDK 21.0.8, Python 3.12.8 e Node v24.21.0. Em Python, len("👍🏽") dá 2; em JavaScript, "👍🏽".length dá 4. O programa em Go a seguir mede os tamanhos e também mostra o byte inválido e as comparações de strings.

package main

import (
	"fmt"
	"unicode/utf8"
)

func main() {
	casos := []string{"a", "é", "€", "😀", "João", "👍🏽", "👨‍👩‍👧"}
	for _, s := range casos {
		fmt.Printf("%-18q bytes=%2d  runas=%d\n", s, len(s), utf8.RuneCountInString(s))
	}
	r, tam := utf8.DecodeRuneInString(string([]byte{0xFF}))
	fmt.Printf("byte FF: runa=%q U+%04X tamanho=%d\n", r, r, tam)
	fmt.Println(`"z" < "é" ?`, "z" < "é")
	fmt.Println(`"é" < "z" ?`, "é" < "z")
}
"a"                bytes= 1  runas=1
"é"                bytes= 2  runas=1
"€"                bytes= 3  runas=1
"😀"                bytes= 4  runas=1
"João"             bytes= 5  runas=4
"👍🏽"               bytes= 8  runas=2
"👨‍👩‍👧"  bytes=18  runas=5
byte FF: runa='�' U+FFFD tamanho=1
"z" < "é" ? true
"é" < "z" ? false

Repare que o Go é o único dos quatro em que len responde bytes. Isso é consistente com a especificação da linguagem, que define o tamanho de uma string em bytes e a iteração com range em runas (runa é o nome que o Go dá a um ponto de código). O byte FF não começa nenhum caractere válido, e o Go o devolve como U+FFFD, com tamanho 1. A função utf8.RuneCountInString conta as runas.

Em Java, a mesma conta sai assim, com String.length contando unidades UTF-16 e codePointCount contando pontos de código. O programa fica como segue. A linha do família mostra o ZWJ escapado, porque um caractere invisível não se lê na tela.

import java.nio.charset.StandardCharsets;

public class Texto {
    public static void main(String[] args) {
        String[] casos = {"a", "é", "€", "😀", "João", "👍🏽", "👨‍👩‍👧"};
        for (String s : casos) {
            String mostra = s.replace("‍", "\\u200d");
            System.out.printf("%-20s length=%d  pontos=%d  bytes UTF-8=%d%n",
                mostra, s.length(), s.codePointCount(0, s.length()),
                s.getBytes(StandardCharsets.UTF_8).length);
        }
    }
}
a                    length=1  pontos=1  bytes UTF-8=1
é                    length=1  pontos=1  bytes UTF-8=2
€                    length=1  pontos=1  bytes UTF-8=3
😀                   length=2  pontos=1  bytes UTF-8=4
João                 length=4  pontos=4  bytes UTF-8=5
👍🏽                 length=4  pontos=2  bytes UTF-8=8
👨‍👩‍👧   length=8  pontos=5  bytes UTF-8=18

Por último, o caminho do texto. Um texto digitado passa por várias camadas, e cada uma pode reinterpretá-lo. A figura mostra a sequência do teclado ao navegador, com a marca de cada etapa.

Uma letra é digitada, vai ao servidor e ao banco, e volta: a conexão grava UTF-8 e a resposta traz charset=utf-8 no Content-Type, senão o navegador adivinha
figura 12 · Caminho de uma letra digitada no navegador até o banco e de volta com charset na

Veja a última mensagem da figura, que é o ponto em que a maioria dos erros começa. O navegador precisa saber que a resposta está em UTF-8. Esse aviso vem do cabeçalho Content-Type, que o servidor web envia, ou da marcação do próprio documento. Sem ele, o navegador tenta adivinhar, e a adivinhação falha com textos curtos e com acentos de idiomas diferentes.

6. Erros comuns e boas práticas

O primeiro erro é tratar len como número de caracteres. Ele conta bytes em Go, unidades UTF-16 em Java e JavaScript, e pontos de código em Python. Para cortar um texto na tela, conte grafemas. Para o limite de tamanho de um campo no banco, a pergunta é em que unidade o banco mede. MySQL e PostgreSQL contam caracteres em VARCHAR(n), enquanto o Oracle aceita VARCHAR2(n BYTE), que conta bytes. Com essa definição, “João” tem 5 bytes em UTF-8 e não cabe numa coluna VARCHAR2(4 BYTE), mesmo tendo quatro letras. Quem copia um tamanho de um banco para outro sem conferir a unidade costuma descobrir o erro só com um sobrenome acentuado.

O segundo erro é misturar codificações sem avisar. Um arquivo salvo em Latin-1 e lido como UTF-8 dá um erro de decodificação, e um arquivo salvo em UTF-8 e lido como Latin-1 dá o mojibake da figura 3. O conserto é declarar a codificação em cada fronteira: na leitura do arquivo, na conexão com o banco, no cabeçalho HTTP e na marcação HTML. A regra vale em cada fronteira, porque cada uma pode reinterpretar os bytes.

O terceiro erro é comparar strings sem normalizar. Dois usuários que digitam o mesmo nome, um com a letra composta e outro com a decomposta, não encontram um ao outro na busca. A boa prática é normalizar para NFC na entrada, antes de gravar ou comparar. O quarto erro, que quase todo mundo comete uma vez, é achar que UTF-16 é “o Unicode de 16 bits”. Ele é uma codificação do Unicode que usa unidades de 16 bits e substitutos para os pontos acima de U+FFFF, e por isso um emoji conta como dois em Java e JavaScript.

O quinto erro é esquecer que a ordenação depende do idioma. Ordenar uma lista de sobrenomes pelo código dá resultados que o usuário brasileiro considera errados. Use a collation do idioma, como Intl.Collator ou o equivalente do seu banco.

7. Exercícios

  1. Converta a palavra “maçã” para bytes UTF-8 à mão. Quantos caracteres, quantos pontos de código e quantos bytes ela tem? Depois confira com "maçã".encode("utf-8") em Python.
  2. Um byte da sequência é 0xB5, sozinho, sem nenhum byte antes. Pelo fluxo de decisão da figura 6, o que um decodificador deve fazer com ele? Explique com o prefixo do byte.
  3. Explique por que "z" < "é" é verdadeiro em Go, mas uma lista ordenada pela collation pt-BR põe “é” antes de “z”.
  4. Calcule o número de bytes de um texto de 100 letras do alfabeto latino sem acento, e depois de um texto com 100 emojis de rosto. Use o tamanho de cada caractere em UTF-8.

Respostas dos exercícios

  1. “maçã” tem quatro caracteres e quatro pontos de código: m (U+006D), a (U+0061), ç (U+00E7) e ã (U+00E3). Em UTF-8, m e a ocupam um byte cada. O ç (U+00E7) é 000 11100111 com zeros à frente, que se dividem em 00011 e 100111: 110 00011 e 10 100111, ou seja, C3 A7. O ã (U+00E3) segue o mesmo caminho e dá C3 A3. Os seis bytes são 6d 61 c3 a7 c3 a3. A resposta supõe que o ç veio na forma composta, NFC. Se o ç foi digitado decomposto (c seguido do acento combinante U+0327), a palavra tem seis pontos de código e oito bytes: 6d 61 63 cc a7 61 cc 83. Por isso a normalização vem antes da contagem.
  2. O byte 0xB5 tem os bits 10110101, que começam com 10. Como byte inicial de um caractere, o prefixo 10 é proibido, porque ele só aparece como continuação. O decodificador deve devolver U+FFFD e seguir para o próximo byte.
  3. Em Go, a comparação é feita byte a byte, e o primeiro byte de “é” é 0xC3, que é maior que 0x7A de “z”. Na collation pt-BR, a ordem segue o alfabeto e o acento só desempata letras iguais na base: “é” fica depois de “e” e antes de “ê”, e todas essas letras ficam antes de “z”.
  4. Cem letras do alfabeto latino sem acento ocupam 100 bytes, porque cada uma é um byte no ASCII. Cem emojis de rosto ocupam 400 bytes, porque cada um é um ponto de código de quatro bytes em UTF-8.

Glossário

Ponto de código: número que identifica um caractere no Unicode, escrito como U+ seguido de hexadecimal.

Repertório: o conjunto de caracteres que uma convenção reconhece, com um número para cada um.

Codificação: regra que transforma pontos de código em bytes, como UTF-8, UTF-16 ou Latin-1.

UTF-8: codificação do Unicode que usa de um a quatro bytes por ponto, e é compatível com ASCII.

UTF-16: codificação que usa unidades de 16 bits, com substitutos para pontos acima de U+FFFF.

Substitutos: par de unidades entre D800 e DFFF que juntas representam um ponto fora do plano básico.

Grafema: o que o leitor percebe como um caractere, podendo ser formado por vários pontos de código.

ZWJ: joiner de largura zero, U+200D, um caractere invisível que junta pontos de código num só grafema, como nas famílias de emoji.

Normalização: conversão de uma sequência de pontos para uma forma padrão, NFC (composta) ou NFD (decomposta).

Latin-1: codificação de um byte por caractere (ISO 8859-1), com 256 posições e sem o euro.

cp1252: página de código do Windows, parecida com Latin-1, mas que usa as posições de 128 a 159 para o euro e outros sinais.

Página de código: tabela que associa bytes a caracteres. Cada página reaproveita as mesmas posições para letras diferentes.

Runa: nome que o Go dá a um ponto de código.

Collation: regras de ordenação de um idioma, que dizem como comparar letras acentuadas e maiúsculas.

Endian: ordem dos bytes de uma unidade. Big endian grava primeiro o byte mais significativo, e little endian grava primeiro o menos significativo.

Mojibake: texto que aparece com caracteres errados porque foi lido com outra codificação.

BOM: marca de ordem de bytes, U+FEFF, usada no início de arquivos para indicar big ou little endian.

Fontes

Consultadas em 1 de novembro de 2026, salvo indicação.

  • Verbete Baudot code, Wikipedia em inglês, para o código de cinco pulsos, a patente da máquina em 1874 (o código não foi patenteado), a adaptação de Murray para fita perfurada em 1901 e os modos de letras e figuras (ITA2). A data de 1870 para o início do trabalho de Baudot é de referências secundárias.
  • Processo de Mimault contra Baudot em 1877, por resultados de busca que citam a disputa de prioridade e a decisão do tribunal civil de Paris, depois revista. Fonte primária não lida.
  • Verbete ASCII, Wikipedia em inglês, para ASA X3.4-1963, as 28 posições sem atribuição e a revisão de 1967 (USAS X3.4-1967). E ascii-code.com/articles/ASCII-1963, pela afirmação de que a edição de 1963 não tinha minúsculas.
  • Página de estatísticas do Unicode 17.0, unicode.org/versions/stats/charcountv17_0.html, para 159.629 gráficos, 172 de formato, 65 controles, 137.468 de uso privado, 2.048 substitutos, 66 não-caracteres, 814.664 reservados e 297.334 de Total Assigned.
  • Anúncio do Unicode 17.0, blog.unicode.org, 9 de setembro de 2025, para a data e o acréscimo de 4.803 caracteres gráficos. Não foi reaberto nesta revisão.
  • Relato de Rob Pike sobre a origem do UTF-8, transcrito por Markus Kuhn em cl.cam.ac.uk/~mgk25/ucs/utf-8-history.txt, para o jogo americano de papel, New Jersey e “setembro ou por aí de 1992”. E a busca de Russ Cox no arquivo, que data o primeiro rascunho em 2 de setembro de 1992. O original em doc.cat-v.org não abriu (HTTP 402 na rodada anterior).
  • RFC 3629, UTF-8, a transformation format of ISO 10646, rfc-editor.org/info/rfc3629, publicada em novembro de 2003.
  • Documentação de linguagem: especificação do Go para len e range sobre strings, o Java SE para String.length e codePointCount, a documentação do Python para str.encode e unicodedata, e o Intl.Collator e o Intl.Segmenter do Node. Os valores foram conferidos pelos programas, não por leitura de página.

O que não foi confirmado ou medido

Os números de bytes, pontos de código e tamanhos desta aula foram medidos pelos programas citados, com Go 1.27.2, OpenJDK 21.0.8, Python 3.12.8 e Node v24.21.0. A data de 1870 para o código Baudot vem de verbetes e histórias de referência. A afirmação de que a edição de 1963 do ASCII não tinha letras minúsculas vem de fontes secundárias; elas concordam nesse ponto, mas divergem em outros detalhes, e o documento X3.4-1963 não foi lido. O processo de Mimault e a data de 1877 vêm de resultados de busca, sem a sentença original. A data de 9 de setembro de 2025 para o Unicode 17.0 é de memória do anúncio, e o blog não foi reaberto nesta revisão. A regra de unidade de VARCHAR nos três bancos citados (MySQL, PostgreSQL e Oracle) foi escrita de memória, sem reabrir a documentação de cada um. A data do rascunho do UTF-8 em 2 de setembro de 1992 vem de uma busca no arquivo, e o relato de Pike fala só em setembro de 1992. Os nomes das tabelas de codificação de cada país não foram pesquisados um a um, e a aula trata apenas do caso de Latin-1 e cp1252. A tabela ASCII, com todas as 128 posições, não foi reproduzida. Nenhuma medida de desempenho de codificação ou decodificação foi feita nesta aula.

Relacionados

  • Trilha de Hardware
  • CPU
  • Bits e binário
  • Inteiros, complemento de dois e overflow
  • Ponto flutuante
  • Álgebra booleana e portas lógicas

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?