← Voltar para o blog

Go 1.21: Novidades e Recursos

Descubra as principais novidades do Go 1.21, incluindo novos recursos, melhorias de performance e mudancas na biblioteca padrao.

Go 1.21: O Que Há de Novo na Linguagem da Google

A versão 1.21 da linguagem Go foi lançada em 8 de agosto de 2023, trazendo consigo uma série de melhorias e novidades que visam otimizar o desenvolvimento, aprimorar a performance e expandir as capacidades da linguagem. Esta versão foca em estabilidade, usabilidade e refinamento das funcionalidades existentes. Vamos explorar as principais mudanças e como elas impactam o desenvolvimento em Go.

Principais Novidades do Go 1.21

O Go 1.21 apresenta diversas novidades, mas destacaremos as mais significativas para o dia a dia de um desenvolvedor Go.

1. Profile Guided Optimization (PGO)

O Profile Guided Optimization (PGO) é, sem dúvida, a principal novidade desta versão. PGO é uma técnica de otimização de compilador que utiliza dados de perfil de execução reais para melhorar o desempenho do código. Em outras palavras, o compilador aprende como o seu programa se comporta em tempo de execução e otimiza o código com base nessas informações.

Antes do Go 1.21, o compilador Go realizava otimizações estáticas, analisando o código sem conhecimento do comportamento em tempo de execução. Com o PGO, o compilador pode tomar decisões mais inteligentes sobre inlining de funções, layout de memória e outras otimizações, resultando em um código mais rápido e eficiente.

Como utilizar o PGO:

  1. Coletar o perfil: Execute o seu programa com um workload representativo e gere um arquivo de perfil usando o comando go test -cpuprofile=cpu.prof.

    go test -cpuprofile=cpu.prof ./...
    
  2. Construir o programa com o perfil: Utilize a flag -pgo com o compilador go build para instruir o compilador a usar o perfil coletado.

    go build -pgo=cpu.prof .
    

    Se o perfil estiver no diretório raiz do módulo com o nome default.pgo, o compilador o utilizará automaticamente.

Exemplo:

Suponha que você tenha um programa que realiza cálculos complexos. Sem PGO, o compilador pode não ter informações suficientes para otimizar o código de forma eficaz. Com PGO, o compilador pode identificar as partes do código que são executadas com mais frequência e otimizá-las, resultando em um ganho significativo de desempenho.

package main

import (
	"fmt"
	"time"
)

func fibonacci(n int) int {
	if n <= 1 {
		return n
	}
	return fibonacci(n-1) + fibonacci(n-2)
}

func main() {
	start := time.Now()
	result := fibonacci(40)
	elapsed := time.Since(start)

	fmt.Printf("Resultado: %d\n", result)
	fmt.Printf("Tempo de execução: %s\n", elapsed)
}

Para otimizar este código com PGO:

  1. Execute go test -cpuprofile=cpu.prof .
  2. Execute go build -pgo=cpu.prof .

A diferença de desempenho pode ser notável, especialmente para programas com gargalos de CPU.

Considerações:

  • O perfil deve ser representativo do workload real do seu programa. Perfis coletados com workloads diferentes podem levar a otimizações subótimas ou até mesmo a regressões de desempenho.
  • O PGO adiciona uma etapa extra ao processo de build, mas o ganho de desempenho geralmente compensa o custo adicional.
  • É recomendado monitorar o desempenho do seu programa após habilitar o PGO para garantir que as otimizações estão funcionando conforme o esperado.

2. Novas Funções na Biblioteca slices

O pacote slices recebeu novas funções que facilitam a manipulação de slices, tornando o código mais conciso e legível.

  • slices.Clone(s []T) []T: Cria uma cópia profunda de um slice. Isso é útil para evitar modificações indesejadas no slice original.

    package main
    
    import (
    	"fmt"
    	"slices"
    )
    
    func main() {
    	original := []int{1, 2, 3}
    	copia := slices.Clone(original)
    	copia[0] = 10
    
    	fmt.Println("Original:", original) // Output: Original: [1 2 3]
    	fmt.Println("Cópia:", copia)   // Output: Cópia: [10 2 3]
    }
    
  • slices.Compare(s1, s2 []T) int: Compara dois slices e retorna um inteiro indicando a ordem lexicográfica. Retorna -1 se s1 < s2, 0 se s1 == s2 e 1 se s1 > s2.

    package main
    
    import (
    	"fmt"
    	"slices"
    )
    
    func main() {
    	s1 := []int{1, 2, 3}
    	s2 := []int{1, 2, 4}
    	s3 := []int{1, 2, 3}
    
    	fmt.Println(slices.Compare(s1, s2)) // Output: -1
    	fmt.Println(slices.Compare(s1, s3)) // Output: 0
    	fmt.Println(slices.Compare(s2, s1)) // Output: 1
    }
    
  • slices.DeleteFunc(s []T, del func(T) bool) []T: Remove todos os elementos de um slice que satisfazem uma determinada condição definida pela função del.

    package main
    
    import (
    	"fmt"
    	"slices"
    )
    
    func main() {
    	numeros := []int{1, 2, 3, 4, 5, 6}
    	numeros = slices.DeleteFunc(numeros, func(n int) bool {
    		return n%2 == 0 // Remove números pares
    	})
    
    	fmt.Println(numeros) // Output: [1 3 5]
    }
    
  • slices.Insert(s []T, i int, v ...T) []T: Insere um ou mais elementos em um slice na posição i.

    package main
    
    import (
    	"fmt"
    	"slices"
    )
    
    func main() {
    	numeros := []int{1, 2, 3}
    	numeros = slices.Insert(numeros, 1, 10, 20)
    
    	fmt.Println(numeros) // Output: [1 10 20 2 3]
    }
    

Estas funções simplificam tarefas comuns de manipulação de slices e reduzem a quantidade de código boilerplate que você precisa escrever. Consulte a documentação oficial para obter informações detalhadas sobre estas e outras funções no pacote slices: https://pkg.go.dev/slices

3. Novas Funções na Biblioteca maps

Similarmente ao pacote slices, o pacote maps também recebeu novas funções para auxiliar na manipulação de mapas.

  • maps.Clone(m map[K]V) map[K]V: Cria uma cópia profunda de um mapa.

    package main
    
    import (
    	"fmt"
    	"maps"
    )
    
    func main() {
    	original := map[string]int{"a": 1, "b": 2}
    	copia := maps.Clone(original)
    	copia["a"] = 10
    
    	fmt.Println("Original:", original) // Output: Original: map[a:1 b:2]
    	fmt.Println("Cópia:", copia)   // Output: Cópia: map[a:10 b:2]
    }
    
  • maps.Copy(dst, src map[K]V): Copia todos os pares chave-valor de um mapa de origem (src) para um mapa de destino (dst). Se uma chave já existir no mapa de destino, o valor será substituído.

    package main
    
    import (
    	"fmt"
    	"maps"
    )
    
    func main() {
    	destino := map[string]int{"a": 1, "b": 2}
    	origem := map[string]int{"b": 3, "c": 4}
    
    	maps.Copy(destino, origem)
    
    	fmt.Println("Destino:", destino) // Output: Destino: map[a:1 b:3 c:4]
    }
    
  • maps.DeleteFunc(m map[K]V, del func(K, V) bool): Remove todos os pares chave-valor de um mapa que satisfazem uma determinada condição definida pela função del.

    package main
    
    import (
    	"fmt"
    	"maps"
    )
    
    func main() {
    	idades := map[string]int{"Alice": 30, "Bob": 25, "Charlie": 35}
    	maps.DeleteFunc(idades, func(nome string, idade int) bool {
    		return idade < 30 // Remove pessoas com menos de 30 anos
    	})
    
    	fmt.Println(idades) // Output: map[Alice:30 Charlie:35]
    }
    

Estas funções simplificam a manipulação de mapas, tornando o código mais claro e menos propenso a erros. Consulte a documentação oficial para mais detalhes: https://pkg.go.dev/maps

4. context.WithCancelCause

A função context.WithCancelCause permite associar uma causa específica ao cancelamento de um contexto. Isso facilita a depuração e o tratamento de erros, pois o receptor do sinal de cancelamento pode determinar o motivo do cancelamento.

package main

import (
	"context"
	"errors"
	"fmt"
	"time"
)

func main() {
	ctx, cancel := context.WithCancelCause(context.Background())

	go func() {
		time.Sleep(2 * time.Second)
		cancel(errors.New("timeout"))
	}()

	select {
	case <-ctx.Done():
		err := context.Cause(ctx)
		fmt.Println("Contexto cancelado:", err) // Output: Contexto cancelado: timeout
	case <-time.After(5 * time.Second):
		fmt.Println("Programa finalizado")
	}
}

Neste exemplo, o contexto é cancelado após 2 segundos com a causa “timeout”. O receptor do sinal de cancelamento pode então acessar a causa usando context.Cause(ctx).

5. Loop Variable Capture

O problema de captura de variáveis de loop em Goroutines foi atenuado nesta versão. Anteriormente, era comum encontrar erros onde Goroutines acessavam o valor da variável de loop após o loop ter terminado, resultando em valores inesperados. O Go 1.21 introduz uma mudança que mitiga este problema.

package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	numeros := []int{1, 2, 3, 4, 5}

	for _, num := range numeros {
		wg.Add(1)
		go func(n int) {
			defer wg.Done()
			fmt.Println(n)
		}(num)
	}

	wg.Wait()
}

Neste exemplo, cada Goroutine recebe uma cópia do valor da variável num como argumento, garantindo que cada Goroutine trabalhe com o valor correto. Em versões anteriores do Go, sem a passagem explícita do valor para a Goroutine, todas as Goroutines poderiam acabar imprimindo o último valor de num (5). Esta alteração no comportamento padrão torna o código mais robusto e menos suscetível a erros sutis.

Melhorias de Performance

Além do PGO, o Go 1.21 inclui outras melhorias de performance, como otimizações no garbage collector (GC) e no compilador. Essas melhorias podem resultar em tempos de execução menores e menor consumo de memória para seus programas. Os ganhos de performance podem variar dependendo do workload específico do seu aplicativo.

Mudanças na Biblioteca Padrão

Além das novas funções nos pacotes slices e maps, o Go 1.21 traz outras mudanças e adições à biblioteca padrão:

  • Novo pacote cmp: Este pacote fornece funções para comparar valores de diferentes tipos. Ele é especialmente útil para implementar tipos genéricos que precisam ser comparados.
  • Melhorias no pacote net/http: Diversas melhorias foram feitas no pacote net/http, incluindo suporte para HTTP/2 Push e melhor tratamento de erros.
  • Atualizações no pacote time: O pacote time recebeu algumas atualizações, incluindo novas funções para formatar e analisar datas e horários.

Consulte as notas de lançamento para obter uma lista completa de todas as mudanças na biblioteca padrão: https://go.dev/doc/go1.21

Como Atualizar para o Go 1.21

Para atualizar para o Go 1.21, você pode usar o comando go install:

go install golang.org/dl/go1.21@latest
go1.21 download
go1.21 use

Este comando irá baixar e instalar a versão mais recente do Go 1.21. Após a instalação, você pode verificar a versão do Go usando o comando go version.

É importante notar que atualizar a versão do Go pode exigir a atualização de algumas dependências do seu projeto. Recomenda-se executar go mod tidy após a atualização para garantir que todas as dependências estejam atualizadas e compatíveis com o Go 1.21.

Conclusão Prática

O Go 1.21 representa um passo importante na evolução da linguagem, trazendo melhorias significativas em performance, usabilidade e funcionalidade. O PGO é uma adição poderosa que pode otimizar o desempenho de seus programas, enquanto as novas funções nos pacotes slices e maps simplificam a manipulação de dados. As outras melhorias na biblioteca padrão e a mitigação do problema de captura de variáveis de loop tornam o Go 1.21 uma atualização valiosa para qualquer desenvolvedor Go.

Recomenda-se experimentar as novas funcionalidades e avaliar o impacto do PGO em seus projetos. A atualização para o Go 1.21 é um investimento que pode trazer benefícios significativos em termos de desempenho, produtividade e qualidade do código. Não deixe de consultar a documentação oficial para obter informações detalhadas sobre todas as mudanças e novidades do Go 1.21.