Bem-vindo à aula sobre os pacotes strings e bytes da linguagem Go. Esses pacotes são essenciais para qualquer desenvolvedor Go que precise manipular texto, processar entradas, gerar saídas ou trabalhar com dados binários. Dominar essas ferramentas não apenas torna seu código mais limpo e expressivo, mas também mais eficiente, especialmente em aplicações que lidam com grandes volumes de dados.

Nesta aula, vamos explorar as principais funções do pacote strings para manipulação de cadeias de caracteres, o uso de strings.Builder para construir strings de forma eficiente, e o bytes.Buffer para trabalhar com fatias de bytes. Também discutiremos estratégias de performance e boas práticas para garantir que seu código seja rápido e confiável.

Manipulação de strings

O pacote strings fornece um conjunto rico de funções para operações comuns em strings, como busca, substituição, comparação, divisão e junção. Essas funções operam em strings imutáveis, retornando novas strings quando necessário. Vamos ver algumas das mais utilizadas.

Entre as funções mais importantes estão Contains, HasPrefix, HasSuffix, Index, Replace, Split, Join, ToUpper, ToLower e TrimSpace. Elas permitem realizar tarefas comuns de processamento de texto de forma concisa e segura, evitando erros comuns de manipulação manual de índices e substrings.

package main

import (
    "fmt"
    "strings"
)

func main() {
    texto := "Olá, mundo Go!"
    fmt.Println(strings.Contains(texto, "mundo")) // true
    fmt.Println(strings.HasPrefix(texto, "Olá")) // true
    fmt.Println(strings.HasSuffix(texto, "Go!")) // true
    fmt.Println(strings.Index(texto, "mundo")) // 5
    fmt.Println(strings.Replace(texto, "mundo", "planeta", 1)) // Olá, planeta Go!
    fmt.Println(strings.Split(texto, ", ")) // [Olá mundo Go!]
    fmt.Println(strings.Join([]string{"Go", "é", "legal"}, " ")) // Go é legal
    fmt.Println(strings.ToUpper(texto)) // OLÁ, MUNDO GO!
    fmt.Println(strings.ToLower(texto)) // olá, mundo go!
    fmt.Println(strings.TrimSpace("   espaços   ")) // espaços
}

Além dessas, existem funções como Fields que divide a string em palavras ignorando espaços, Repeat que repete uma string, e Compare que compara duas strings lexicograficamente. É importante lembrar que todas essas funções são imutáveis: elas não alteram a string original, apenas retornam novas strings. Isso é consistente com a filosofia de imutabilidade das strings em Go.

strings.Builder

Quando você precisa concatenar muitas strings, usar o operador + repetidamente pode ser ineficiente porque cada concatenação aloca uma nova string e copia o conteúdo. O pacote strings oferece o tipo Builder, que acumula o resultado em um buffer interno e só gera a string final quando solicitado. Isso reduz drasticamente o número de alocações e cópias.

O strings.Builder é a escolha ideal para construir strings dinamicamente em loops, como na geração de relatórios, montagem de mensagens ou construção de consultas SQL. Ele implementa a interface io.Writer, permitindo que você use funções como fmt.Fprintf diretamente para escrever no builder.

package main

import (
    "fmt"
    "strings"
)

func main() {
    var builder strings.Builder

    // Escreve strings
    builder.WriteString("Go")
    builder.WriteString(" é")
    builder.WriteString(" fantástico!")

    // Escreve bytes
    builder.Write([]byte{' ', 'G', 'o', 'l', 'a', 'n', 'g'})

    // Formatação
    fmt.Fprintf(&builder, " %d %s", 2025, "anos")

    texto := builder.String()
    fmt.Println(texto) // Go é fantástico! Golang 2025 anos

    // Resetar para reutilizar
    builder.Reset()
    builder.WriteString("Novo conteúdo")
    fmt.Println(builder.String()) // Novo conteúdo
}

Uma característica importante do strings.Builder é que ele não copia a string quando você chama String(); ele retorna uma referência ao buffer interno. Isso significa que você não deve modificar o builder depois de chamar String(), pois isso pode corromper os dados. Em Go 1.10+, o método String() retorna uma string que aponta para o buffer interno, e o builder não é mais seguro para uso após essa chamada. Portanto, se precisar reutilizar, use Reset().

bytes.Buffer

O pacote bytes fornece o tipo Buffer, que é um buffer de bytes dinâmico e de tamanho variável. Ele é útil para processar dados binários, ler e escrever dados em memória, e também pode ser usado como um io.Writer e io.Reader. Diferente de strings.Builder, que é otimizado para construção de strings, bytes.Buffer é mais geral e permite leitura e escrita de dados.

O bytes.Buffer é frequentemente usado em conjunto com pacotes como io e encoding para serializar e desserializar dados, ou para armazenar resultados intermediários de processamento. Ele também é útil quando você precisa trabalhar com fatias de bytes e quer evitar cópias desnecessárias.

package main

import (
    "bytes"
    "fmt"
)

func main() {
    var buf bytes.Buffer

    // Escrita
    buf.Write([]byte("Hello, ")
    buf.WriteString("world!")
    buf.WriteByte('\n')

    fmt.Print(buf.String()) // Hello, world!

    // Leitura
    primeiro := buf.Bytes()[:5] // "Hello"
    fmt.Println(string(primeiro))

    // Resetar
    buf.Reset()
    buf.WriteString("Novo")
    fmt.Println(buf.String()) // Novo
}

Uma diferença fundamental entre strings.Builder e bytes.Buffer é que o primeiro é otimizado para escrever strings (e não oferece leitura), enquanto o segundo permite leitura e escrita de bytes. Se você precisa apenas construir uma string, use strings.Builder; se precisa de um buffer de dados mais flexível, use bytes.Buffer.

Performance

A performance é um aspecto crucial em programação, especialmente em sistemas que lidam com grandes volumes de dados. Em Go, a forma como você manipula strings e bytes pode impactar significativamente o desempenho do seu programa. Vamos analisar as melhores práticas para garantir eficiência.

Primeiro, evite concatenação de strings em loops com o operador +. Cada concatenação aloca uma nova string e copia todo o conteúdo, resultando em complexidade O(n²) para n concatenações. Em vez disso, use strings.Builder para acumular o resultado. Isso reduz a complexidade para O(n) e minimiza alocações.

Segundo, quando você precisa converter entre []byte e string, faça isso com cuidado. A conversão direta string(b) ou []byte(s) pode causar cópias, mas o compilador otimiza em muitos casos. Em Go 1.20+, há a função unsafe.String e unsafe.Slice para conversões sem cópia, mas devem ser usadas com extrema cautela. Prefira sempre as conversões seguras e deixe o compilador otimizar.

Terceiro, prefira usar funções do pacote strings em vez de implementar sua própria lógica. As funções padrão são bem testadas e otimizadas em assembly para arquiteturas comuns. Por exemplo, strings.Replace é mais eficiente que uma implementação manual com loops.

Além disso, ao trabalhar com grandes buffers, considere usar bytes.Buffer em vez de strings.Builder se você precisar de operações de leitura e escrita. O bytes.Buffer tem métodos como Read e Write que são eficientes e evitam cópias desnecessárias quando você usa Bytes() para acessar o conteúdo.

Por fim, lembre-se de que a imutabilidade das strings pode levar a cópias em certas operações. Por exemplo, ao modificar uma string, você sempre cria uma nova. Se você precisa modificar muitos caracteres, considere trabalhar com []byte e converter para string apenas no final.

package main

import (
    "fmt"
    "strings"
    "testing"
)

// Exemplo de benchmark para comparar concatenação com Builder
func BenchmarkConcat(b *testing.B) {
    s := ""
    for i := 0; i < b.N; i++ {
        s += "x"
    }
}

func BenchmarkBuilder(b *testing.B) {
    var sb strings.Builder
    for i := 0; i < b.N; i++ {
        sb.WriteString("x")
    }
}

func main() {
    fmt.Println("Rode com: go test -bench=.")
}

Em benchmarks típicos, o strings.Builder é de 10 a 100 vezes mais rápido que a concatenação com + para grandes números de operações. Portanto, sempre use strings.Builder para construção de strings em loops.

Boas práticas

Além da performance, existem outras boas práticas ao trabalhar com strings e bytes em Go. Primeiro, sempre prefira funções do pacote strings em vez de reinventar a roda. Elas são testadas e cobrem casos extremos.

Segundo, quando estiver lidando com dados binários, use []byte em vez de string para evitar conversões desnecessárias. As strings devem ser usadas para texto legível, enquanto bytes são para dados brutos.

Terceiro, aproveite a interface io.Writer para escrever em qualquer destino, seja um bytes.Buffer, um arquivo ou uma conexão de rede. Isso torna seu código mais modular e reutilizável.

Quarto, lembre-se de que strings.Builder não é seguro para concorrência. Se você precisa de um buffer compartilhado entre goroutines, use sincronização explícita ou sync.Pool para gerenciar builders.

Por fim, teste seu código com benchmarks para verificar que as otimizações realmente melhoram a performance. Use o pacote testing e a ferramenta go test -bench para medir o impacto.

Referências

Exercícios

  1. Exercício 1: Escreva uma função que receba uma string e retorne uma nova string com todas as vogais removidas. Use o pacote strings e funções como Contains e Builder.

    ✓ Resposta:
    func removeVowels(s string) string {
        var builder strings.Builder
        for _, r := range s {
            if !strings.ContainsRune("aeiouAEIOU", r) {
                builder.WriteRune(r)
            }
        }
        return builder.String()
    }
  2. Exercício 2: Usando strings.Builder, construa uma string que contenha os números de 1 a 100 separados por vírgula. Imprima o resultado.

    ✓ Resposta:
    func numeros() string {
        var sb strings.Builder
        for i := 1; i <= 100; i++ {
            if i > 1 {
                sb.WriteString(",")
            }
            fmt.Fprintf(&sb, "%d", i)
        }
        return sb.String()
    }
  3. Exercício 3: Crie uma função que use bytes.Buffer para escrever dados binários: primeiro 4 bytes representando um inteiro (usando binary.Write) e depois uma string. Depois leia os dados de volta e imprima o inteiro e a string.

    ✓ Resposta:
    import (
        "bytes"
        "encoding/binary"
        "fmt"
    )
    
    func binario() {
        var buf bytes.Buffer
        num := int32(42)
        binary.Write(&buf, binary.LittleEndian, num)
        buf.WriteString("Go")
    
        var lido int32
        binary.Read(&buf, binary.LittleEndian, &lido)
        str := buf.String()
        fmt.Println(lido, str)
    }
  4. Exercício 4: Escreva um benchmark que compare a concatenação com + e com strings.Builder para 1000 iterações. Execute com go test -bench e observe os resultados.

    ✓ Resposta:
    func BenchmarkConcat(b *testing.B) {
        s := ""
        for i := 0; i < b.N; i++ {
            s += "x"
        }
    }
    
    func BenchmarkBuilder(b *testing.B) {
        var sb strings.Builder
        for i := 0; i < b.N; i++ {
            sb.WriteString("x")
        }
    }
  5. Exercício 5: Usando bytes.Buffer, implemente uma função que receba uma fatia de bytes e retorne uma nova fatia onde cada byte é invertido (0 <-> 255). Use buf.Bytes() para obter o resultado.

    ✓ Resposta:
    func invert(bytes []byte) []byte {
        var buf bytes.Buffer
        for _, b := range bytes {
            buf.WriteByte(^b)
        }
        return buf.Bytes()
    }