Para esclarecer, vou fornecer diretrizes gerais, não regras rígidas e inflexíveis. Use o seu próprio julgamento. Mas se não tiver certeza, recomendo usar as diretrizes discutidas aqui.Para esclarecer, vou fornecer diretrizes gerais, não regras rígidas e inflexíveis. Use o seu próprio julgamento. Mas se não tiver certeza, recomendo usar as diretrizes discutidas aqui.

Go: Quando deve utilizar Generics? Quando não deve?

2025/10/04 23:00

Introdução

Esta é a versão em blog das minhas palestras no Google Open Source Live:

https://youtu.be/nr8EpUO9jhw?si=jlWTapr6NM6isLgt&embedable=true

e na GopherCon 2021:

https://youtu.be/Pae9EeCdy8?si=M-87Eisb2nU1qmJ&embedable=true

\ O lançamento do Go 1.18 adiciona uma nova funcionalidade importante à linguagem: suporte para programação genérica. Neste artigo, não vou descrever o que são genéricos nem como usá-los. Este artigo trata de quando usar genéricos no código Go e quando não usá-los.

\ Para ser claro, fornecerei diretrizes gerais, não regras rígidas. Use o seu próprio julgamento. Mas se não tiver certeza, recomendo usar as diretrizes discutidas aqui.

Escreva código

Vamos começar com uma diretriz geral para programar em Go: escreva programas Go escrevendo código, não definindo tipos. Quando se trata de genéricos, se começar a escrever o seu programa definindo restrições de parâmetros de tipo, provavelmente está no caminho errado. Comece escrevendo funções. É fácil adicionar parâmetros de tipo mais tarde, quando ficar claro que serão úteis.

Quando os parâmetros de tipo são úteis?

Dito isso, vamos analisar casos em que os parâmetros de tipo podem ser úteis.

Ao usar tipos de contentores definidos pela linguagem

Um caso é ao escrever funções que operam nos tipos especiais de contentores definidos pela linguagem: slices, maps e channels. Se uma função tem parâmetros com esses tipos e o código da função não faz suposições específicas sobre os tipos de elementos, pode ser útil usar um parâmetro de tipo.

\ Por exemplo, aqui está uma função que retorna um slice de todas as chaves em um map de qualquer tipo:

// MapKeys returns a slice of all the keys in m. // The keys are not returned in any particular order. func MapKeys[Key comparable, Val any](m map[Key]Val) []Key {     s := make([]Key, 0, len(m))     for k := range m {         s = append(s, k)     }     return s } 

\ Este código não assume nada sobre o tipo de chave do map e não usa o tipo de valor do map. Funciona para qualquer tipo de map. Isso o torna um bom candidato para usar parâmetros de tipo.

\ A alternativa aos parâmetros de tipo para este tipo de função é tipicamente usar reflexão, mas esse é um modelo de programação mais desajeitado, não é verificado estaticamente em tempo de compilação e muitas vezes é mais lento em tempo de execução.

Estruturas de dados de uso geral

Outro caso em que os parâmetros de tipo podem ser úteis é para estruturas de dados de uso geral. Uma estrutura de dados de uso geral é algo como um slice ou map, mas que não está incorporado na linguagem, como uma lista ligada ou uma árvore binária.

\ Hoje, programas que precisam de tais estruturas de dados tipicamente fazem uma de duas coisas: escrevê-las com um tipo de elemento específico ou usar um tipo de interface. Substituir um tipo de elemento específico por um parâmetro de tipo pode produzir uma estrutura de dados mais geral que pode ser usada em outras partes do programa ou por outros programas. Substituir um tipo de interface por um parâmetro de tipo pode permitir que os dados sejam armazenados de forma mais eficiente, economizando recursos de memória; também pode permitir que o código evite asserções de tipo e seja totalmente verificado em tempo de compilação.

\ Por exemplo, aqui está parte do que uma estrutura de dados de árvore binária pode parecer usando parâmetros de tipo:

// Tree is a binary tree. type Tree[T any] struct {     cmp  func(T, T) int     root *node[T] }  // A node in a Tree. type node[T any] struct {     left, right  *node[T]     val          T }  // find returns a pointer to the node containing val, // or, if val is not present, a pointer to where it // would be placed if added. func (bt *Tree[T]) find(val T) **node[T] {     pl := &bt.root     for *pl != nil {         switch cmp := bt.cmp(val, (*pl).val); {         case cmp < 0:             pl = &(*pl).left         case cmp > 0:             pl = &(*pl).right         default:             return pl         }     }     return pl }  // Insert inserts val into bt if not already there, // and reports whether it was inserted. func (bt *Tree[T]) Insert(val T) bool {     pl := bt.find(val)     if *pl != nil {         return false     }     *pl = &node[T]{val: val}     return true } 

\ Cada nó na árvore contém um valor do parâmetro de tipo T. Quando a árvore é instanciada com um argumento de tipo específico, valores desse tipo serão armazenados diretamente nos nós. Eles não serão armazenados como tipos de interface.

\ Este é um uso razoável de parâmetros de tipo porque a estrutura de dados Tree, incluindo o código nos métodos, é amplamente independente do tipo de elemento T.

\ A estrutura de dados Tree precisa saber como comparar valores do tipo de elemento T; ela usa uma função de comparação passada para isso. Pode ver isso na quarta linha do método find, na chamada para bt.cmp. Além disso, o parâmetro de tipo não importa.

Para parâmetros de tipo, prefira funções a métodos

O exemplo Tree ilustra outra diretriz geral: quando precisar de algo como uma função de comparação, prefira uma função a um método.

\ Poderíamos ter definido o tipo Tree de forma que o tipo de elemento fosse obrigado a ter um método Compare ou Less. Isso seria feito escrevendo uma restrição que exige o método, o que significa que qualquer argumento de tipo usado para instanciar o tipo Tree precisaria ter esse método.

\ Uma consequência seria que qualquer pessoa que quisesse usar Tree com um tipo de dados simples como int teria que definir seu próprio tipo inteiro e escrever seu próprio método de comparação. Se definirmos Tree para receber uma função de comparação, como no código mostrado acima, então é fácil passar a função desejada. É tão fácil escrever essa função de comparação quanto escrever um método.

\ Se o tipo de elemento Tree já tiver um método Compare, então podemos simplesmente usar uma expressão de método como ElementType.Compare como função de comparação.

\ Em outras palavras, é muito mais simples transformar um método em uma função do que adicionar um método a um tipo. Portanto, para tipos de dados de uso geral, prefira uma função em vez de escrever uma restrição que exija um método.

Implementando um método comum

Outro caso em que os parâmetros de tipo podem ser úteis é quando diferentes tipos precisam implementar algum método comum, e as implementações para os diferentes tipos são todas iguais.

\ Por exemplo, considere a interface sort.Interface da biblioteca padrão. Ela exige que um tipo implemente três métodos: Len, Swap e Less.

\ Aqui está um exemplo de um tipo genérico SliceFn que implementa sort.Interface para qualquer tipo de slice:

// SliceFn implements sort.Interface for a slice of T. type SliceFn[T any] struct {     s    []T     less func(T, T) bool }  func (s SliceFn[T]) Len() int {     return len(s.s) } func (s SliceFn[T]) Swap(i, j int) {     s.s[i], s.s[j] = s.s[j], s.s[i] } func (s SliceFn[T]) Less(i, j int) bool {     return s.less(s.s[i], s.s[j]) } 

\ Para qualquer tipo de slice, os métodos Len e Swap são exatamente os mesmos. O método Less requer uma comparação, que é a parte Fn do nome SliceFn. Como no exemplo anterior de Tree, passaremos uma função quando criarmos um SliceFn.

\ Aqui está como usar SliceFn para ordenar qualquer slice usando uma função de comparação:

// SortFn sorts s in place using a comparison function. func SortFn[T any](s []T, less func(T, T) bool) {     sort.Sort(SliceFn[T]{s, less}) } 

\ Isso é semelhante à função da biblioteca padrão sort.Slice, mas a função de comparação é escrita usando valores em vez de índices de slice.

\ Usar parâmetros de tipo para este tipo de código é apropriado porque os métodos parecem exatamente os mesmos para todos os tipos de slice.

\ (Devo mencionar que o Go 1.19–não o 1.18–provavelmente incluirá uma função genérica para ordenar um slice usando uma função de comparação, e essa função genérica provavelmente não usará sort.Interface. Veja a proposta #47619. Mas o ponto geral ainda é verdadeiro mesmo que este exemplo específico provavelmente não seja útil: é razoável usar parâmetros de tipo quando você precisa implementar métodos que parecem os mesmos para todos os tipos relevantes.)

Quando os parâmetros de tipo não são úteis?

Agora vamos falar sobre o outro lado da questão: quando não usar parâmetros de tipo.

Não substitua tipos de interface por parâmetros de tipo

Como todos sabemos, Go tem tipos de interface. Os tipos de interface permitem um tipo de programação genérica.

\ Por exemplo, a interface io.Reader amplamente utilizada fornece um mecanismo genérico para ler dados de qualquer valor que contenha informações (por exemplo, um arquivo) ou que produza informações (por exemplo, um gerador de números aleatórios). Se tudo o que você precisa fazer com um valor de algum tipo é chamar um método nesse valor, use um tipo de interface, não um parâmetro de tipo. io.Reader é fácil de ler, eficiente e eficaz. Não há necessidade de usar um parâmetro de tipo para ler dados de um valor chamando o método Read.

\ Por exemplo, pode ser tentador mudar a primeira assinatura de função aqui, que usa apenas um tipo de interface, para a segunda versão, que usa um parâmetro de tipo.

func ReadSome(r io.Reader) ([]byte, error)  func ReadSome[T io.Reader](r T) ([]byte, error) 

\ Não faça esse tipo de mudança. Omitir o parâmetro de tipo torna a função mais fácil de escrever, mais fácil de ler, e o tempo de execução provavelmente será o mesmo.

\ Vale a pena enfatizar o último ponto. Embora seja possível implementar genéricos de várias maneiras diferentes, e as implementações mudarão e melhorarão com o tempo, a implementação usada no Go 1.18 tratará em muitos casos valores cujo tipo é um parâmetro de tipo muito como valores cujo tipo é um tipo de interface. O que isso significa é que usar um parâmetro de tipo geralmente não será mais rápido do que usar um tipo de interface. Portanto, não mude de tipos de interface para parâmetros de tipo apenas por velocidade, porque provavelmente não será mais rápido.

\

Não use parâmetros de tipo se as implementações de métodos diferirem

Ao decidir se deve usar um parâmetro de tipo ou um tipo de interface, considere a implementação dos métodos. Anteriormente, dissemos que se a implementação de um método é a mesma para todos os tipos, use um parâmetro de tipo. Inversamente, se a implementação é diferente para cada tipo, então use um tipo de interface e escreva diferentes implementações de método, não use um parâmetro de tipo.

\ Por exemplo, a implementação de Read de um arquivo não é nada parecida com a implementação de Read de um gerador de números aleatórios. Isso significa que devemos escrever dois métodos Read diferentes e usar um tipo de interface como io.Reader.

Use reflexão quando apropriado

Go tem reflexão em tempo de execução. A reflexão permite um tipo de programação genérica, na medida em que permite escrever código que funciona com qualquer tipo.

\ Se alguma operação tiver que suportar até mesmo tipos que não têm métodos (de modo que os tipos de interface não ajudam), e se a operação for diferente para cada tipo (de modo que os parâmetros de tipo não são apropriados), use reflexão.

\ Um exemplo disso é o pacote encoding/json. Não queremos exigir que cada tipo que codificamos tenha um método MarshalJSON, então não podemos usar tipos de interface. Mas codificar um tipo de interface não é nada parecido com codificar um tipo struct, então não devemos usar parâmetros de tipo. Em vez disso, o pacote usa reflexão. O código não é simples

Isenção de responsabilidade: Os artigos republicados neste site são provenientes de plataformas públicas e são fornecidos apenas para fins informativos. Eles não refletem necessariamente a opinião da MEXC. Todos os direitos permanecem com os autores originais. Se você acredita que algum conteúdo infringe direitos de terceiros, entre em contato pelo e-mail [email protected] para solicitar a remoção. A MEXC não oferece garantias quanto à precisão, integridade ou atualidade das informações e não se responsabiliza por quaisquer ações tomadas com base no conteúdo fornecido. O conteúdo não constitui aconselhamento financeiro, jurídico ou profissional, nem deve ser considerado uma recomendação ou endosso por parte da MEXC.