Blog

Strings, bytes e runes em Go

Uma string em Go é uma sequência de bytes somente leitura, normalmente UTF-8. Quando você enxerga isso, len contando bytes, o índice dando um byte, o range dando runes e um fatiamento que parte um caractere ao meio passam a fazer sentido.

Texto em Go parece simples até aparecer a primeira letra acentuada ou o primeiro emoji. Aí len dá um número que você não esperava, e um pedaço de uma string imprime um losango estranho.

Este post explica o que uma string em Go guarda de verdade, como bytes e runes se relacionam com ela, e os pacotes strings e strconv que você vai usar todo dia. Todo programa abaixo rodou no Go 1.26, e a saída foi colada da execução.

Uma string é uma fileira de bytes

Uma string em Go é uma sequência imutável de bytes. Go não obriga esses bytes a serem UTF-8, mas os literais de string no código-fonte Go são UTF-8, e quase todo texto com que você vai lidar também é.

package main

import (
	"fmt"
	"unicode/utf8"
)

func main() {
	for _, s := range []string{"hello", "héllo", "日本語", "go🚀"} {
		fmt.Println(s, len(s), utf8.RuneCountInString(s))
	}
}

Ele imprime:

hello 5 5
héllo 6 5
日本語 9 3
go🚀 6 3

len conta bytes, não caracteres. "hello" é ASCII puro, então cada letra ocupa um byte e os dois números batem. é ocupa dois bytes, então "héllo" tem cinco letras e seis bytes. Cada um dos três caracteres japoneses ocupa três bytes. O foguete ocupa quatro.

utf8.RuneCountInString conta runes, o que costuma estar mais perto do que as pessoas querem dizer com “caracteres”. Para isso ele precisa percorrer a string inteira, então o custo cresce com o tamanho. len é instantâneo, porque a contagem de bytes fica guardada junto com a string.

Explicado como se você tivesse dez anos

Imagine um colar de contas num fio. Cada conta é um byte.

As letras comuns do inglês são pequenas, e cada uma ocupa uma única conta. Um é precisa de duas contas. Um caractere japonês precisa de três. Um emoji precisa de quatro. As contas de uma letra ficam sempre uma do lado da outra, e a primeira conta de cada letra tem um formato especial, então dá para saber onde começa uma letra nova.

len conta contas. range, que aparece daqui a pouco, conta letras: ele junta todas as contas que andam juntas e entrega a letra inteira.

A versão precisa

O Unicode dá a cada caractere um número chamado code point, escrito como U+00E9 para é. O nome que Go dá a um code point é rune, e rune é outro nome para int32, como a parte sobre valores e tipos mostrou.

UTF-8 é a regra que transforma um code point em bytes. Ela usa de 1 a 4 bytes, dependendo do tamanho do número:

Code points Bytes Exemplos
U+0000 a U+007F 1 ASCII: a, 7, {
U+0080 a U+07FF 2 é, ñ, grego, cirílico
U+0800 a U+FFFF 3 , a maioria dos outros sistemas de escrita
U+10000 a U+10FFFF 4 emoji como 🚀

O primeiro byte de cada rune codificada diz quantos bytes vêm depois. Os bytes seguintes começam todos com os bits 10, então nunca podem ser confundidos com o começo de uma rune. É por isso que Go consegue achar os limites das runes sem tabela de consulta.

Onde a analogia falha: o que uma pessoa vê como um caractere pode ser vários code points, então “contar letras” com runes nem sempre dá o que um leitor contaria. A seção sobre emoji mais abaixo mostra um caso real.

Indexar dá um byte

Indexar uma string em Go com s[i] retorna o byte na posição i, não o i-ésimo caractere:

package main

import "fmt"

func main() {
	s := "héllo"
	fmt.Println(s[0], s[1], s[2])
	fmt.Printf("%c %c\n", s[0], s[1])
	fmt.Printf("% x\n", s)
}

Ele imprime:

104 195 169
h Ã
68 c3 a9 6c 6c 6f

s[0] é 104, o byte de h. Mas é é guardado como dois bytes, c3 e a9, então s[1] é 195, só a primeira metade dele.

Imprimir essa metade com %c mostra Ã. Na primeira vez, isso surpreende. %c trata 195 como o code point U+00C3, que por acaso é Ã. O byte nunca foi feito para ficar sozinho, então Go imprimiu uma letra totalmente diferente.

O verbo % x, com um espaço entre % e x, imprime cada byte em hexadecimal. É o jeito mais rápido de ver o que uma string guarda de verdade.

for range dá runes

Um laço for range sobre uma string decodifica UTF-8 enquanto avança, e entrega cada rune junto com o deslocamento em bytes onde ela começa:

package main

import "fmt"

func main() {
	for i, r := range "hé日🚀!" {
		fmt.Printf("byte %d: %c (U+%04X)\n", i, r, r)
	}
}

Ele imprime:

byte 0: h (U+0068)
byte 1: é (U+00E9)
byte 3: 日 (U+65E5)
byte 6: 🚀 (U+1F680)
byte 10: ! (U+0021)

Repare nos deslocamentos pulando: 0, 1, 3, 6, 10. O índice não é um contador de caracteres. É onde cada rune começa nos bytes, e a distância até a próxima é quantos bytes aquela rune ocupou.

Então use s[i] quando você se importa com bytes, como ao fazer o parse de protocolos ASCII. Use range quando você se importa com caracteres.

Fatiar uma string pode partir um caractere ao meio

Fatiar uma string, s[low:high], trabalha com posições de byte, igual à indexação. Go não confere se o corte cai no limite de uma rune:

package main

import (
	"fmt"
	"unicode/utf8"
)

func main() {
	s := "héllo"
	bad := s[:2]
	fmt.Printf("%q len=%d valid=%v\n", bad, len(bad), utf8.ValidString(bad))
	for i, r := range bad {
		fmt.Printf("byte %d: %c (U+%04X)\n", i, r, r)
	}

	good := s[:3]
	fmt.Printf("%q len=%d valid=%v\n", good, len(good), utf8.ValidString(good))

	r := []rune(s)
	fmt.Println(string(r[:2]))
}

Ele imprime:

"h\xc3" len=2 valid=false
byte 0: h (U+0068)
byte 1: � (U+FFFD)
"hé" len=3 valid=true
hé

s[:2] fica com h e o primeiro byte de é. Nada causa panic, e nada avisa você. O resultado simplesmente deixa de ser UTF-8 válido. %q mostra o byte solto como \xc3.

Quando o range encontra um byte que não começa uma rune válida, ele entrega U+FFFD, o caractere de substituição do Unicode, e avança um byte. Na tela, esse é o losango . Quando você o vê numa página web ou num log, alguma string foi cortada ou decodificada no lugar errado em algum ponto antes.

s[:3] corta depois do é inteiro, então está tudo certo. Se você quer “os dois primeiros caracteres” e não quer contar bytes, converta para []rune antes, como a última linha faz.

Conversões para []byte e []rune copiam

Converter uma string para []byte ou []rune dá um slice novo com a sua própria cópia dos dados, e é por isso que você pode alterá-lo:

package main

import "fmt"

func main() {
	s := "日本語"
	b := []byte(s)
	r := []rune(s)
	fmt.Println(len(b), b)
	fmt.Println(len(r), r)

	b[0] = 'X'
	r[0] = '月'
	fmt.Println(s, string(r))
}

Ele imprime:

9 [230 151 165 230 156 172 232 170 158]
3 [26085 26412 35486]
日本語 月本語

[]byte(s) guarda os nove bytes UTF-8. []rune(s) guarda três code points, cada um um int32. Mudar qualquer um dos slices não mexe em s, porque cada conversão copiou os dados para um array novo. Converter de volta com string(...) copia de novo.

Essas cópias custam memória e tempo proporcionais ao tamanho. Para uma string curta num handler de requisição, isso não importa. Dentro de um laço sobre um arquivo grande pode importar, e é aí que o pacote bytes, perto do fim deste post, ajuda. Um []rune também ocupa 4 bytes por rune, então o []rune de um texto quase todo ASCII fica umas quatro vezes maior que a string.

O compilador pula a cópia em alguns casos em que consegue provar que é seguro, como string(b) usado só como chave de map. Não escreva código que dependa disso.

Strings não podem ser alteradas

Uma string em Go é somente leitura, então você não pode atribuir a um dos bytes dela:

package main

import "fmt"

func main() {
	s := "hello"
	s[0] = 'H'
	fmt.Println(s)
}

O build falha com:

./main.go:7:2: cannot assign to s[0] (neither addressable nor a map index expression)

A mensagem não diz “strings são imutáveis”. Ela diz que s[0] não é endereçável, que é o jeito de Go dizer que não existe ali uma caixa em que você tenha permissão para escrever.

A imutabilidade é o que deixa as strings baratas de passar adiante. Um valor string é só um ponteiro para os bytes e um tamanho. Copiar, fatiar ou passar uma string para uma função nunca copia os bytes, porque ninguém pode mudá-los por baixo de você.

Para ter uma string alterada, você constrói uma nova:

package main

import "fmt"

func main() {
	s := "hello"
	t := "H" + s[1:]

	b := []byte(s)
	b[0] = 'J'
	u := string(b)

	fmt.Println(s, t, u)
}

Ele imprime:

hello Hello Jello

t junta uma primeira letra nova a um pedaço da string antiga. u passa por uma cópia []byte, altera essa cópia e converte de volta. Nos dois casos, s continua dizendo hello.

Construindo strings: + num laço e strings.Builder

Juntar strings com + num laço e usar strings.Builder dão o mesmo resultado, mas fazem quantidades de trabalho bem diferentes:

package main

import (
	"fmt"
	"strings"
)

func main() {
	words := []string{"strings", "are", "immutable"}

	s := ""
	for i, w := range words {
		if i > 0 {
			s += " "
		}
		s += w
	}

	var sb strings.Builder
	for i, w := range words {
		if i > 0 {
			sb.WriteByte(' ')
		}
		sb.WriteString(w)
	}
	fmt.Fprintf(&sb, " (%d words)", len(words))

	fmt.Println(s)
	fmt.Println(sb.String())
	fmt.Println(strings.Join(words, " "))
}

Ele imprime:

strings are immutable
strings are immutable (3 words)
strings are immutable

As três estão corretas. A diferença está na cópia. Como uma string não pode mudar, cada s += w cria uma string totalmente nova e copia para ela tudo o que foi construído até ali. Construir desse jeito uma string de 10.000 bytes, um byte por vez, copia 1 + 2 + … + 10.000 bytes, ou seja, 50.005.000 bytes, para produzir 10.000.

strings.Builder guarda por dentro um []byte que cresce, do jeito que a parte sobre slices mostrou. String() devolve esse buffer como string sem copiá-lo de novo. Um Builder também funciona como io.Writer, então fmt.Fprintf pode escrever direto nele.

Para poucos pedaços, + serve e fica mais legível. Quando você está juntando um slice que já tem, strings.Join é o mais curto. Num laço com muitos pedaços, use um Builder.

Um passeio pelo pacote strings

O pacote strings cobre a maioria das tarefas com texto para as quais você escreveria laços. Aqui estão as que você vai usar primeiro, sobre algo parecido com uma linha de uma requisição HTTP:

package main

import (
	"fmt"
	"strings"
)

func main() {
	line := "  GET /users/42 HTTP/1.1  "
	clean := strings.TrimSpace(line)
	fmt.Printf("%q\n", clean)

	fmt.Println(strings.Contains(clean, "/users"), strings.HasPrefix(clean, "GET "))
	fmt.Printf("%q\n", strings.Split("a,b,,c", ","))
	fmt.Printf("%q\n", strings.Fields(line))

	parts := strings.Fields(clean)
	fmt.Println(strings.Join(parts, " | "))
	fmt.Println(strings.Replace("a-b-c", "-", "+", 1), strings.ReplaceAll("a-b-c", "-", "+"))
	fmt.Println(strings.ToUpper("héllo, 日本"))

	key, value, found := strings.Cut("Content-Type: text/html", ": ")
	fmt.Printf("%q %q %v\n", key, value, found)
	_, _, found = strings.Cut("no colon here", ": ")
	fmt.Println(found)
}

Ele imprime:

"GET /users/42 HTTP/1.1"
true true
["a" "b" "" "c"]
["GET" "/users/42" "HTTP/1.1"]
GET | /users/42 | HTTP/1.1
a+b-c a+b+c
HÉLLO, 日本
"Content-Type" "text/html" true
false

Alguns detalhes valem a pena saber:

  • TrimSpace remove espaços em branco só das duas pontas. Os espaços do meio ficam.
  • Split mantém os pedaços vazios. "a,b,,c" tem uma string vazia entre as duas vírgulas.
  • Fields separa em qualquer sequência de espaços em branco e nunca retorna pedaços vazios, então é a melhor escolha para palavras.
  • Replace recebe uma contagem. -1 quer dizer todas, e ReplaceAll é o jeito legível de dizer isso.
  • ToUpper entende Unicode, então é virou É. O japonês não tem maiúsculas, então ficou igual.
  • Cut, adicionada no Go 1.18, separa em volta da primeira ocorrência e diz se encontrou alguma. Ela substitui muita aritmética com Index e chamadas a Split com dois elementos.

Nenhuma dessas funções muda line ou clean. Toda função que “modifica” uma string retorna uma nova.

Convertendo números com strconv

O pacote strconv converte entre strings e números, e as funções de parse dele retornam um erro, porque texto vindo de fora do seu programa muitas vezes não é um número:

package main

import (
	"fmt"
	"strconv"
)

func main() {
	s := strconv.Itoa(42)
	fmt.Printf("%q\n", s)

	n, err := strconv.Atoi("123")
	fmt.Println(n, err)

	n, err = strconv.Atoi("12a")
	fmt.Println(n, err)

	f, err := strconv.ParseFloat("3.25", 64)
	fmt.Println(f, err)

	fmt.Println(strconv.Quote("tab\there, \"quotes\", é"))
	fmt.Println(strconv.QuoteToASCII("é🚀"))
}

Ele imprime:

"42"
123 <nil>
0 strconv.Atoi: parsing "12a": invalid syntax
3.25 <nil>
"tab\there, \"quotes\", é"
"\u00e9\U0001f680"

Itoa transforma um int no seu texto decimal. Atoi faz o caminho inverso. Com entrada ruim, ele retorna 0 e um erro que cita a função e a entrada, o que já dá uma boa linha de log do jeito que está. Confira esse erro toda vez que fizer o parse de um parâmetro de query ou um campo de formulário. A parte sobre erros mostra o que fazer com ele.

ParseFloat recebe o tamanho em bits, 64 para um float64. Quote coloca uma string entre aspas duplas e escapa o que precisa ser escapado, do mesmo jeito que %q. QuoteToASCII também escapa tudo o que está fora do ASCII.

Uma armadilha: se n é um int com 42, string(n) não dá "42". Ele trata o número como o code point U+002A, que é *. O compilador aceita, mas o go vet barra você com conversion from int to string yields a string of one rune, not a string of digits. O que você queria era strconv.Itoa.

Literais de string raw

Um literal de string raw é escrito entre crases, e Go pega tudo o que está dentro exatamente como está: barras invertidas continuam barras invertidas, e quebras de linha continuam quebras de linha.

package main

import (
	"fmt"
	"regexp"
)

func main() {
	interpreted := "C:\\temp\\new\n"
	raw := `C:\temp\new\n`
	fmt.Print(interpreted)
	fmt.Println(raw)

	re := regexp.MustCompile(`^\d{3}-\d{4}$`)
	fmt.Println(re.MatchString("555-1234"))

	usage := `Usage:
  tool [flags]
  tool help`
	fmt.Println(usage)
}

Ele imprime:

C:\temp\new
C:\temp\new\n
true
Usage:
  tool [flags]
  tool help

Na string entre aspas duplas, \\ quer dizer uma barra invertida e \n quer dizer uma quebra de linha. Na string entre crases, \n é só uma barra invertida e um n.

Use crases quando o texto está cheio de barras invertidas ou ocupa várias linhas: expressões regulares, caminhos do Windows, SQL, JSON num teste ou uma mensagem de uso. A única coisa que uma string raw não pode conter é uma crase.

Quando um caractere é mais de uma rune

Uma rune é um code point, mas o que uma pessoa vê como um caractere pode ser formado por vários. Emoji com tom de pele e letras com um acento separado são os casos comuns:

package main

import "fmt"

func main() {
	wave := "👋🏽"
	fmt.Println(wave, len(wave), len([]rune(wave)))
	for i, r := range wave {
		fmt.Printf("byte %d: U+%X\n", i, r)
	}

	composed := "caf\u00e9"
	decomposed := "cafe\u0301"
	fmt.Println(composed, decomposed, composed == decomposed)
	fmt.Println(len([]rune(composed)), len([]rune(decomposed)))
}

Ele imprime:

👋🏽 8 2
byte 0: U+1F44B
byte 4: U+1F3FD
café café false
4 5

A mão acenando é um símbolo só na tela, mas são duas runes: a mão, U+1F44B, e um modificador de tom de pele, U+1F3FD. Cada uma ocupa quatro bytes, então len é 8 e []rune tem tamanho 2.

Os dois cafés parecem idênticos, mas não são iguais. O primeiro usa o code point único é. O segundo é um e simples seguido de U+0301, um acento combinante que se encaixa na letra anterior. Por isso um tem quatro runes e o outro tem cinco.

Aqui termina o que a biblioteca padrão faz por você. Contar caracteres do jeito que um leitor os vê, ou tratar os dois cafés como iguais, exige normalização Unicode e segmentação em grafemas. Nenhuma das duas está na biblioteca padrão. A normalização fica no pacote golang.org/x/text/unicode/norm, mantido pelo time do Go fora da biblioteca padrão. Para a maioria dos programas, saber que runes não são exatamente caracteres já basta para evitar o bug.

O pacote bytes espelha o strings

O pacote bytes tem as mesmas funções que strings, mas para []byte, então você pode trabalhar com dados de um arquivo ou de uma conexão de rede sem convertê-los para string e de volta:

package main

import (
	"bytes"
	"fmt"
	"strings"
)

func main() {
	data := []byte("  hello, world  ")
	fmt.Printf("%q\n", bytes.TrimSpace(data))
	fmt.Println(bytes.Contains(data, []byte("world")), strings.Contains(string(data), "world"))
	fmt.Printf("%q\n", bytes.ToUpper(data))

	var buf bytes.Buffer
	buf.WriteString("status: ")
	buf.WriteString("ok")
	fmt.Println(buf.String(), buf.Len())
}

Ele imprime:

"hello, world"
true true
"  HELLO, WORLD  "
status: ok 10

bytes.TrimSpace, bytes.Contains e bytes.ToUpper fazem o mesmo que as gêmeas em strings. bytes.Buffer é parecido com strings.Builder, mas você também pode ler de volta dele. A maior parte da I/O em Go lida com []byte, então você vai encontrar esse pacote de novo quando a série chegar aos handlers HTTP.

O que lembrar

  • Uma string é uma fileira imutável de bytes, normalmente UTF-8. len conta bytes, e utf8.RuneCountInString conta runes.
  • s[i] e s[low:high] trabalham com bytes. Um fatiamento pode partir uma rune ao meio, e o byte quebrado aparece como .
  • for range sobre uma string decodifica runes e entrega o deslocamento em bytes onde cada uma começa.
  • []byte(s), []rune(s) e string(...) copiam os dados.
  • Você não pode mudar uma string. Construa uma nova, e use strings.Builder quando for adicionar muitos pedaços num laço.
  • Faça o parse de números com strconv e confira o erro. string(n) num int dá uma rune, não dígitos.
  • Uma rune é um code point, nem sempre um caractere que um leitor vê. Um emoji com tom de pele são duas runes.

Strings em Go guardam bytes. Caracteres são algo que você decodifica a partir deles.

Quanto este post te ajudou?

Clique em um coração para avaliar!

Média das avaliações 0 / 5. Total de votos: 0

Nenhum voto até agora. Seja o primeiro a avaliar este post.