Skip to content

Erros comuns de Go

Esta página é um resumo dos erros do 100 Go Mistakes and How to Avoid Them book. Enquanto isso, também está aberto à comunidade. Se você acredita que um erro comum do Go deve ser adicionado, crie uma issue.

Jobs

Sua empresa est√° contratando? Patrocine este reposit√≥rio e informe um p√ļblico significativo de desenvolvedores Go (cerca de 1 mil visitantes √ļnicos por semana) sobre suas oportunidades nesta se√ß√£o.

Beta

Voc√™ est√° visualizando uma vers√£o beta enriquecida com muito mais conte√ļdo. No entanto, esta vers√£o ainda n√£o est√° completa e estou procurando volunt√°rios para me ajudar a resumir os erros restantes (GitHub issue #43).

Progresso:

Código e Organização do Projeto

Sombreamento n√£o intencional de vari√°vel (#1)

TL;DR

Evitar vari√°veis ‚Äč‚Äčsombreadas pode ajudar a evitar erros, como fazer refer√™ncia √† vari√°vel errada ou confundir os desenvolvedores.

O sombreamento de vari√°vel ocorre quando um nome de vari√°vel √© redeclarado em um bloco interno, mas essa pr√°tica est√° sujeita a erros. A imposi√ß√£o de uma regra para proibir vari√°veis ‚Äč‚Äčobscuras depende do gosto pessoal. Por exemplo, √†s vezes pode ser conveniente reutilizar um nome de vari√°vel existente, como err no caso de erros. Por√©m, em geral, devemos ser cautelosos porque agora sabemos que podemos enfrentar um cen√°rio onde o c√≥digo compila, mas a vari√°vel que recebe o valor n√£o √© a esperada.

Código fonte

Código aninhado desnecessário (#2)

TL;DR

Evitar níveis aninhados e manter o caminho feliz alinhado à esquerda facilita a construção de um modelo de código mental.

Em geral, quanto mais n√≠veis aninhados uma fun√ß√£o exigir, mais complexa ser√° sua leitura e compreens√£o. Vamos ver algumas aplica√ß√Ķes diferentes desta regra para otimizar a legibilidade do nosso c√≥digo:

  • Quando um bloco if retorna, devemos omitir o else em todos os casos. Por exemplo, n√£o dever√≠amos escrever:
if foo() {
    // ...
    return true
} else {
    // ...
}

Em vez disso, omitimos o bloco else assim:

if foo() {
    // ...
    return true
}
// ...
  • Tamb√©m podemos seguir esta l√≥gica com um caminho n√£o feliz:
if s != "" {
    // ...
} else {
    return errors.New("empty string")
}

Aqui, um s vazio representa o caminho não feliz. Portanto, devemos inverter a condição assim:

if s == "" {
    return errors.New("empty string")
}
// ...

Escrever c√≥digo leg√≠vel √© um desafio importante para todo desenvolvedor. Esfor√ßar-se para reduzir o n√ļmero de blocos aninhados, alinhar o caminho feliz √† esquerda e retornar o mais cedo poss√≠vel s√£o meios concretos para melhorar a legibilidade do nosso c√≥digo.

Código fonte

Uso indevido de fun√ß√Ķes init (#3)

TL;DR

Ao inicializar vari√°veis, lembre-se de que as fun√ß√Ķes init t√™m tratamento de erros limitado e tornam o tratamento de estado e os testes mais complexos. Na maioria dos casos, as inicializa√ß√Ķes devem ser tratadas como fun√ß√Ķes espec√≠ficas.

Uma fun√ß√£o init √© uma fun√ß√£o usada para inicializar o estado de um aplicativo. N√£o aceita argumentos e n√£o retorna nenhum resultado (uma fun√ß√£o func()). Quando um pacote √© inicializado, todas as declara√ß√Ķes de constantes e vari√°veis ‚Äč‚Äčdo pacote s√£o avaliadas. Ent√£o, as fun√ß√Ķes init s√£o executadas.

As fun√ß√Ķes de inicializa√ß√£o podem levar a alguns problemas:

  • Elas podem limitar o gerenciamento de erros.
  • Elas podem complicar a implementa√ß√£o de testes (por exemplo, uma depend√™ncia externa deve ser configurada, o que pode n√£o ser necess√°rio para o escopo dos testes unit√°rios).
  • Se a inicializa√ß√£o exigir que definamos um estado, isso dever√° ser feito por meio de vari√°veis ‚Äč‚Äčglobais.

Devemos ser cautelosos com as fun√ß√Ķes init. No entanto, elas podem ser √ļteis em algumas situa√ß√Ķes, como na defini√ß√£o de configura√ß√£o est√°tica. Caso contr√°rio, e na maioria dos casos, devemos tratar as inicializa√ß√Ķes atrav√©s de fun√ß√Ķes ad hoc.

Código fonte

Uso excessivo de getters e setters (#4)

TL;DR

Forcing the use of getters and setters isn’t idiomatic in Go. Being pragmatic and finding the right balance between efficiency and blindly following certain idioms should be the way to go.

O encapsulamento de dados refere-se a ocultar os valores ou o estado de um objeto. Getters e setters são meios de habilitar o encapsulamento, fornecendo métodos exportados sobre campos de objetos não exportados.

No Go, n√£o h√° suporte autom√°tico para getters e setters como vemos em algumas linguagens. Tamb√©m n√£o √© considerado obrigat√≥rio nem idiom√°tico o uso de getters e setters para acessar campos struct. N√£o devemos sobrecarregar nosso c√≥digo com getters e setters em structs se eles n√£o trouxerem nenhum valor. Dever√≠amos ser pragm√°ticos e nos esfor√ßar para encontrar o equil√≠brio certo entre efici√™ncia e seguir express√Ķes que √†s vezes s√£o consideradas indiscut√≠veis em outros paradigmas de programa√ß√£o.

Lembre-se de que Go √© uma linguagem √ļnica projetada para muitas caracter√≠sticas, incluindo simplicidade. No entanto, se encontrarmos necessidade de getters e setters ou, como mencionado, prevermos uma necessidade futura e ao mesmo tempo garantirmos a compatibilidade futura, n√£o h√° nada de errado em us√°-los.

Interface poluidas (#5)

TL;DR

Abstra√ß√Ķes devem ser descobertas, n√£o criadas. Para evitar complexidade desnecess√°ria, crie uma interface quando precisar dela e n√£o quando voc√™ prev√™r que ser√° necess√°ria, ou se puder pelo menos provar que a abstra√ß√£o √© v√°lida.

Leia a seção completa aqui.

Código fonte

Interface do lado do producer (#6)

TL;DR

Manter interfaces no lado do cliente evita abstra√ß√Ķes desnecess√°rias.

As interfaces s√£o satisfeitas implicitamente em Go, o que tende a ser um divisor de √°guas em compara√ß√£o com linguagens com implementa√ß√£o expl√≠cita. Na maioria dos casos, a abordagem a seguir √© semelhante √† que descrevemos na se√ß√£o anterior: as abstra√ß√Ķes devem ser descobertas, n√£o criadas. Isso significa que n√£o cabe ao producer for√ßar uma determinada abstra√ß√£o para todos os clientes. Em vez disso, cabe ao cliente decidir se precisa de alguma forma de abstra√ß√£o e ent√£o determinar o melhor n√≠vel de abstra√ß√£o para suas necessidades.

Uma interface deve residir no lado do consumidor na maioria dos casos. Contudo, em contextos espec√≠ficos (por exemplo, quando sabemos ‚Äď e n√£o prevemos ‚Äď que uma abstra√ß√£o ser√° √ļtil para os consumidores), podemos querer t√™-la do lado do procuder. Se o fizermos, devemos nos esfor√ßar para mant√™-lo o m√≠nimo poss√≠vel, aumentando o seu potencial de reutiliza√ß√£o e tornando-o mais facilmente combin√°vel.

Código fonte

Interfaces de retorno (#7)

TL;DR

Para evitar restri√ß√Ķes em termos de flexibilidade, uma fun√ß√£o n√£o deve retornar interfaces, mas implementa√ß√Ķes concretas na maioria dos casos. Por outro lado, uma fun√ß√£o deve aceitar interfaces sempre que poss√≠vel.

Na maioria dos casos, n√£o devemos retornar interfaces, mas implementa√ß√Ķes concretas. Caso contr√°rio, isso pode tornar nosso design mais complexo devido √†s depend√™ncias do pacote e pode restringir a flexibilidade porque todos os clientes teriam que contar com a mesma abstra√ß√£o. Novamente, a conclus√£o √© semelhante √†s se√ß√Ķes anteriores: se sabemos (n√£o prevemos) que uma abstra√ß√£o ser√° √ļtil para os clientes, podemos considerar o retorno de uma interface. Caso contr√°rio, n√£o dever√≠amos for√ßar abstra√ß√Ķes; eles devem ser descobertas pelos clientes. Se um cliente precisar abstrair uma implementa√ß√£o por qualquer motivo, ele ainda poder√° fazer isso do lado do cliente.

any n√£o diz nada (#8)

TL;DR

Use apenas any se precisar aceitar ou retornar qualquer tipo poss√≠vel, como json.Marshal. Caso contr√°rio, any n√£o fornece informa√ß√Ķes significativas e pode levar a problemas de tempo de compila√ß√£o, permitindo que um chamador chame m√©todos com qualquer tipo de dados.

O tipo any pode ser √ļtil se houver uma necessidade genu√≠na de aceitar ou retornar qualquer tipo poss√≠vel (por exemplo, quando se trata de empacotamento ou formata√ß√£o). Em geral, devemos evitar a todo custo generalizar demais o c√≥digo que escrevemos. Talvez um pouco de c√≥digo duplicado possa ocasionalmente ser melhor se melhorar outros aspectos, como a expressividade do c√≥digo.

Código fonte

Ficar confuso sobre quando usar genéricos (#9)

TL;DR

Depender de par√Ęmetros gen√©ricos e de tipo pode impedir a grava√ß√£o de c√≥digo clich√™ (boilerplate) para fatorar elementos ou comportamentos. No entanto, n√£o use par√Ęmetros de tipo prematuramente, mas somente quando voc√™ perceber uma necessidade concreta deles. Caso contr√°rio, introduzem abstra√ß√Ķes e complexidade desnecess√°rias.

Leia a seção completa aqui.

Código fonte

Não estar ciente dos possíveis problemas com a incorporação de tipos (#10)

TL;DR

Usar a incorporação de tipo (type embedding) também pode ajudar a evitar código clichê (boilerplate); no entanto, certifique-se de que isso não leve a problemas de visibilidade onde alguns campos deveriam ter permanecido ocultos.

Ao criar uma struct, Go oferece a op√ß√£o de incorporar tipos. Mas isso √†s vezes pode levar a comportamentos inesperados se n√£o compreendermos todas as implica√ß√Ķes da incorpora√ß√£o de tipos. Ao longo desta se√ß√£o, veremos como incorporar tipos, o que eles trazem e os poss√≠veis problemas.

No Go, um campo struct é chamado de incorporado se for declarado sem nome. Por exemplo,

type Foo struct {
    Bar // Embedded field
}

type Bar struct {
    Baz int
}

Na estrutura Foo, o tipo Bar é declarado sem nome associado; portanto, é um campo incorporado.

Usamos incorporação para promover os campos e métodos de um tipo incorporado. Como Bar contém um campo Baz, esse campo é promovido para Foo. Portanto, Baz fica disponível a partir de Foo.

O que podemos dizer sobre a incorporação de tipos? Primeiro, observemos que raramente é uma necessidade e significa que, qualquer que seja o caso de uso, provavelmente também poderemos resolvê-lo sem incorporação de tipo. A incorporação de tipos é usada principalmente por conveniência: na maioria dos casos, para promover comportamentos.

Se decidirmos usar incorpora√ß√£o de tipo, precisamos ter em mente duas restri√ß√Ķes principais:

  • N√£o deve ser usado apenas como um a√ß√ļcar sint√°tico para simplificar o acesso a um campo (como Foo.Baz() em vez de Foo.Bar.Baz()). Se esta for a √ļnica justificativa, n√£o vamos incorporar o tipo interno e usar um campo.
  • N√£o deve promover dados (campos) ou um comportamento (m√©todos) que queremos ocultar do exterior: por exemplo, se permitir que os clientes acessem um comportamento de bloqueio que deve permanecer privado da struct.

Usar a incorpora√ß√£o de tipo de forma consciente, mantendo essas restri√ß√Ķes em mente, pode ajudar a evitar c√≥digo clich√™ (boilerplate) com m√©todos de encaminhamento adicionais. No entanto, vamos garantir que n√£o o fazemos apenas por motivos cosm√©ticos e n√£o promovemos elementos que deveriam permanecer ocultos.

Código fonte

N√£o usar o padr√£o de op√ß√Ķes funcionais (functional options pattern) (#11)

TL;DR

Para lidar com op√ß√Ķes de maneira conveniente e amig√°vel √† API, use o padr√£o de op√ß√Ķes funcionais.

Embora existam diferentes implementa√ß√Ķes com pequenas varia√ß√Ķes, a ideia principal √© a seguinte:

  • Uma estrutura n√£o exportada cont√©m a configura√ß√£o: op√ß√Ķes.
  • Cada op√ß√£o √© uma fun√ß√£o que retorna o mesmo tipo: type Option func(options *options) error. Por exemplo, WithPort aceita um argumento int que representa a porta e retorna um tipo Option que representa como atualizar a struct options.

type options struct {
  port *int
}

type Option func(options *options) error

func WithPort(port int) Option {
  return func(options *options) error {
    if port < 0 {
    return errors.New("port should be positive")
  }
  options.port = &port
  return nil
  }
}

func NewServer(addr string, opts ...Option) ( *http.Server, error) {
  var options options
  for _, opt := range opts {
    err := opt(&options)
    if err != nil {
      return nil, err
    }
  }

  // At this stage, the options struct is built and contains the config
  // Therefore, we can implement our logic related to port configuration
  var port int
  if options.port == nil {
    port = defaultHTTPPort
  } else {
      if *options.port == 0 {
      port = randomPort()
    } else {
      port = *options.port
    }
  }

  // ...
}

O padr√£o de op√ß√Ķes funcionais fornece uma maneira pr√°tica e amig√°vel √† API de lidar com op√ß√Ķes. Embora o padr√£o do construtor possa ser uma op√ß√£o v√°lida, ele tem algumas desvantagens menores (ter que passar uma estrutura de configura√ß√£o que pode estar vazia ou uma maneira menos pr√°tica de lidar com o gerenciamento de erros) que tendem a tornar o padr√£o de op√ß√Ķes funcionais a maneira idiom√°tica de lidar com esse tipo de problema no Go.

Código fonte

Desorganização do projeto (estrutura do projeto e organização do pacote) (#12)

No que diz respeito à organização geral, existem diferentes escolas de pensamento. Por exemplo, devemos organizar a nossa aplicação por contexto ou por camada? Depende de nossas preferências. Podemos preferir agrupar o código por contexto (como o contexto do cliente, o contexto do contrato, etc.), ou podemos preferir seguir os princípios da arquitetura hexagonal e agrupar por camada técnica. Se a decisão que tomarmos se adequar ao nosso caso de uso, não pode ser uma decisão errada, desde que permaneçamos consistentes com ela.

Em rela√ß√£o aos pacotes, existem v√°rias pr√°ticas recomendadas que devemos seguir. Primeiro, devemos evitar pacotes prematuros porque podem complicar demais um projeto. √Äs vezes, √© melhor usar uma organiza√ß√£o simples e fazer nosso projeto evoluir quando entendemos o que ele cont√©m, em vez de nos for√ßarmos a fazer a estrutura perfeita desde o in√≠cio. A granularidade √© outra coisa essencial a considerar. Devemos evitar dezenas de pacotes nano contendo apenas um ou dois arquivos. Se o fizermos, √© porque provavelmente perdemos algumas conex√Ķes l√≥gicas entre esses pacotes, tornando nosso projeto mais dif√≠cil de ser compreendido pelos leitores. Por outro lado, tamb√©m devemos evitar pacotes grandes que diluem o significado do nome de um pacote.

A nomenclatura dos pacotes tamb√©m deve ser considerada com cuidado. Como todos sabemos (como desenvolvedores), nomear √© dif√≠cil. Para ajudar os clientes a entender um projeto Go, devemos nomear nossos pacotes de acordo com o que eles fornecem, n√£o com o que cont√™m. Al√©m disso, a nomenclatura deve ser significativa. Portanto, o nome de um pacote deve ser curto, conciso, expressivo e, por conven√ß√£o, uma √ļnica palavra min√ļscula.

Quanto ao que exportar, a regra √© bastante simples. Devemos minimizar o que deve ser exportado tanto quanto poss√≠vel para reduzir o acoplamento entre pacotes e manter ocultos os elementos exportados desnecess√°rios. Se n√£o tivermos certeza se devemos ou n√£o exportar um elemento, devemos optar por n√£o export√°-lo. Mais tarde, se descobrirmos que precisamos export√°-lo, poderemos ajustar nosso c√≥digo. Vamos tamb√©m ter em mente algumas exce√ß√Ķes, como fazer com que os campos sejam exportados para que uma estrutura possa ser desempacotada com encoding/json.

Organizar um projeto não é simples, mas seguir essas regras deve ajudar a facilitar sua manutenção. No entanto, lembre-se de que a consistência também é vital para facilitar a manutenção. Portanto, vamos nos certificar de manter as coisas o mais consistentes possível dentro de uma base de código.

Note

Em 2023, a equipe Go publicou uma diretriz oficial para organizar/estruturar um projeto Go: go.dev/doc/modules/layout

Criando pacotes de utilit√°rios (#13)

TL;DR

A nomenclatura é uma parte crítica do design do aplicativo. Criar pacotes como common, util e shared não traz muito valor para o leitor. Refatore esses pacotes em nomes de pacotes significativos e específicos.

Além disso, tenha em mente que nomear um pacote com base no que ele fornece e não no que ele contém pode ser uma forma eficiente de aumentar sua expressividade.

Código fonte

Ignorando colis√Ķes de nomes de pacotes (#14)

TL;DR

Para evitar colis√Ķes de nomes entre vari√°veis ‚Äč‚Äče pacotes, levando a confus√£o ou talvez at√© bugs, use nomes exclusivos para cada um. Se isso n√£o for vi√°vel, use um alias de importa√ß√£o para alterar o qualificador para diferenciar o nome do pacote do nome da vari√°vel ou pense em um nome melhor.

As colis√Ķes de pacotes ocorrem quando um nome de vari√°vel colide com um nome de pacote existente, impedindo que o pacote seja reutilizado. Devemos evitar colis√Ķes de nomes de vari√°veis ‚Äč‚Äčpara evitar ambiguidade. Se enfrentarmos uma colis√£o, devemos encontrar outro nome significativo ou usar um alias de importa√ß√£o.

Documentação de código ausente (#15)

TL;DR

Para ajudar clientes e mantenedores a entender a finalidade do seu código, documente os elementos exportados.

A documentação é um aspecto importante da programação. Simplifica como os clientes podem consumir uma API, mas também pode ajudar na manutenção de um projeto. No Go, devemos seguir algumas regras para tornar nosso código idiomático:

Primeiro, cada elemento exportado deve ser documentado. Seja uma estrutura, uma interface, uma função ou qualquer outra coisa, se for exportado deve ser documentado. A convenção é adicionar comentários, começando com o nome do elemento exportado.

Por conven√ß√£o, cada coment√°rio deve ser uma frase completa que termina com pontua√ß√£o. Tenha tamb√©m em mente que quando documentamos uma fun√ß√£o (ou um m√©todo), devemos destacar o que a fun√ß√£o pretende fazer, n√£o como o faz; isso pertence ao n√ļcleo de uma fun√ß√£o e coment√°rios, n√£o √† documenta√ß√£o. Al√©m disso, o ideal √© que a documenta√ß√£o forne√ßa informa√ß√Ķes suficientes para que o consumidor n√£o precise olhar nosso c√≥digo para entender como usar um elemento exportado.

Quando se trata de documentar uma vari√°vel ou constante, podemos estar interessados ‚Äč‚Äčem transmitir dois aspectos: sua finalidade e seu conte√ļdo. O primeiro deve funcionar como documenta√ß√£o de c√≥digo para ser √ļtil para clientes externos. Este √ļltimo, por√©m, n√£o deveria ser necessariamente p√ļblico.

Para ajudar clientes e mantenedores a entender o escopo de um pacote, devemos tamb√©m documentar cada pacote. A conven√ß√£o √© iniciar o coment√°rio com // Package seguido do nome do pacote. A primeira linha de um coment√°rio de pacote deve ser concisa. Isso porque ele aparecer√° no pacote. Ent√£o, podemos fornecer todas as informa√ß√Ķes que precisamos nas linhas seguintes.

Documentar nosso código não deve ser uma restrição. Devemos aproveitar a oportunidade para garantir que isso ajude os clientes e mantenedores a entender o propósito do nosso código.

N√£o usando linters (#16)

TL;DR

Para melhorar a qualidade e consistência do código, use linters e formatadores.

Um linter é uma ferramenta automática para analisar código e detectar erros. O escopo desta seção não é fornecer uma lista exaustiva dos linters existentes; caso contrário, ele ficará obsoleto rapidamente. Mas devemos entender e lembrar por que os linters são essenciais para a maioria dos projetos Go.

No entanto, se você não é um usuário regular de linters, aqui está uma lista que você pode usar diariamente:

Além dos linters, também devemos usar formatadores de código para corrigir o estilo do código. Aqui está uma lista de alguns formatadores de código para você experimentar:

Enquanto isso, devemos tamb√©m dar uma olhada em golangci-lint (https://github.com/golangci/golangci-lint). √Č uma ferramenta de linting que fornece uma fachada sobre muitos linters e formatadores √ļteis. Al√©m disso, permite executar os linters em paralelo para melhorar a velocidade de an√°lise, o que √© bastante √ļtil.

Linters e formatadores são uma forma poderosa de melhorar a qualidade e consistência de nossa base de código. Vamos dedicar um tempo para entender qual deles devemos usar e garantir que automatizamos sua execução (como um precommit hook de CI ou Git).

Tipos de dados

Criando confus√£o com literais octais (#17)

TL;DR

Ao ler o c√≥digo existente, lembre-se de que literais inteiros come√ßando com 0 s√£o n√ļmeros octais. Al√©m disso, para melhorar a legibilidade, torne os inteiros octais expl√≠citos prefixando-os com 0o.

Os n√ļmeros octais come√ßam com 0 (por exemplo, 010 √© igual a 8 na base 10). Para melhorar a legibilidade e evitar poss√≠veis erros para futuros leitores de c√≥digo, devemos tornar os n√ļmeros octais expl√≠citos usando o prefixo 0o (por exemplo, 0o10).

Devemos tamb√©m observar as outras representa√ß√Ķes literais inteiras:

  • Bin√°rio‚ÄĒUsa um prefixo 0b ou 0B(por exemplo, 0b100 √© igual a 4 na base 10)
  • Hexadecimal‚ÄĒUsa um prefixo 0x ou 0X (por exemplo, 0xF √© igual a 15 na base 10)
  • Imagin√°rio‚ÄĒUsa um i sufixo (por exemplo, 3i)

Tamb√©m podemos usar um caractere de sublinhado (_) como separador para facilitar a leitura. Por exemplo, podemos escrever 1 bilh√£o desta forma: 1_000_000_000. Tamb√©m podemos usar o caractere sublinhado com outras representa√ß√Ķes (por exemplo, 0b00_00_01).

Código fonte

Negligenciando estouros de n√ļmero inteiro (#18)

TL;DR

Como os overflows e underflows de n√ļmeros inteiros s√£o tratados silenciosamente no Go, voc√™ pode implementar suas pr√≥prias fun√ß√Ķes para captur√°-los.

No Go, um estouro de n√ļmero inteiro que pode ser detectado em tempo de compila√ß√£o gera um erro de compila√ß√£o. Por exemplo,

var counter int32 = math.MaxInt32 + 1
constant 2147483648 overflows int32

No entanto, em tempo de execu√ß√£o, um overflow ou underflow de inteiro √© silencioso; isso n√£o leva ao p√Ęnico do aplicativo. √Č essencial ter esse comportamento em mente, pois ele pode levar a bugs sorrateiros (por exemplo, um incremento de n√ļmero inteiro ou adi√ß√£o de n√ļmeros inteiros positivos que leva a um resultado negativo).

Código fonte

N√£o entendendo os pontos flutuantes (#19)

TL;DR

Fazer compara√ß√Ķes de ponto flutuante dentro de um determinado delta pode garantir que seu c√≥digo seja port√°til. Ao realizar adi√ß√£o ou subtra√ß√£o, agrupe as opera√ß√Ķes com ordem de grandeza semelhante para favorecer a precis√£o. Al√©m disso, execute multiplica√ß√£o e divis√£o antes da adi√ß√£o e subtra√ß√£o.

Em Go, existem dois tipos de ponto flutuante (se omitirmos os n√ļmeros imagin√°rios): float32 e float64. O conceito de ponto flutuante foi inventado para resolver o principal problema dos n√ļmeros inteiros: sua incapacidade de representar valores fracion√°rios. Para evitar surpresas desagrad√°veis, precisamos saber que a aritm√©tica de ponto flutuante √© uma aproxima√ß√£o da aritm√©tica real.

Para isso, veremos um exemplo de multiplicação:

var n float32 = 1.0001
fmt.Println(n * n)

Podemos esperar que este código imprima o resultado de 1.0001 * 1.0001 = 1,00020001, certo? No entanto, executá-lo na maioria dos processadores x86 imprime 1.0002.

Como os tipos float32 e float64 em Go s√£o aproxima√ß√Ķes, temos que ter algumas regras em mente:

  • Ao comparar dois n√ļmeros de ponto flutuante, verifique se a diferen√ßa est√° dentro de um intervalo aceit√°vel.
  • Ao realizar adi√ß√Ķes ou subtra√ß√Ķes, agrupe opera√ß√Ķes com ordem de magnitude semelhante para melhor precis√£o.
  • Para favorecer a precis√£o, se uma sequ√™ncia de opera√ß√Ķes exigir adi√ß√£o, subtra√ß√£o, multiplica√ß√£o ou divis√£o, execute primeiro as opera√ß√Ķes de multiplica√ß√£o e divis√£o.

Código fonte

N√£o entendendo o comprimento e a capacidade de slice (#20)

TL;DR

Compreender a diferen√ßa entre comprimento e capacidade da slice deve fazer parte do conhecimento b√°sico de um desenvolvedor Go. O comprimento de slice √© o n√ļmero de elementos dispon√≠veis na slice, enquanto a capacidade de slice √© o n√ļmero de elementos na matriz de apoio.

Leia a seção completa aqui.

Código fonte

Inicialização de slice ineficiente (#21)

TL;DR

Ao criar uma fatia, inicialize-a com um determinado comprimento ou capacidade se o seu comprimento j√° for conhecido. Isso reduz o n√ļmero de aloca√ß√Ķes e melhora o desempenho.

Ao inicializar uma fatia usando make, podemos fornecer um comprimento e uma capacidade opcional. Esquecer de passar um valor apropriado para ambos os par√Ęmetros quando faz sentido √© um erro generalizado. Na verdade, isso pode levar a m√ļltiplas c√≥pias e esfor√ßo adicional para o GC limpar as matrizes de apoio tempor√°rias. Em termos de desempenho, n√£o h√° uma boa raz√£o para n√£o ajudar o tempo de execu√ß√£o do Go.

Nossas op√ß√Ķes s√£o alocar uma fatia com determinada capacidade ou comprimento. Destas duas solu√ß√Ķes, vimos que a segunda tende a ser um pouco mais r√°pida. Mas usar uma determinada capacidade e anexar pode ser mais f√°cil de implementar e ler em alguns contextos.

Código fonte

Estar confuso sobre slice nula vs. slice vazia (#22)

TL;DR

To prevent common confusions such as when using the encoding/json or the reflect package, you need to understand the difference between nil and empty slices. Both are zero-length, zero-capacity slices, but only a nil slice doesn’t require allocation.

No Go, há uma distinção entre slices nulas e vazias. Uma slice nula é igual a nil, enquanto uma slice vazia tem comprimento zero. Uma slice nula está vazia, mas uma slice vazia não é necessariamente nil. Enquanto isso, uma slice nula não requer nenhuma alocação. Vimos ao longo desta seção como inicializar uma slice dependendo do contexto usando

  • var s []string se n√£o tivermos certeza sobre o comprimento final e a fatia pode estar vazia
  • []string(nil) como a√ß√ļcar sint√°tico para criar uma fatia nula e vazia
  • make([]string, length) se o comprimento futuro for conhecido

A √ļltima op√ß√£o, []string{} deve ser evitada se inicializarmos a fatia sem elementos. Finalmente, vamos verificar se as bibliotecas que usamos fazem distin√ß√Ķes entre fatias nulas e vazias para evitar comportamentos inesperados.

Código fonte

N√£o verificar corretamente se um slice est√° vazio (#23)

TL;DR

Para verificar se uma fatia não contém nenhum elemento, verifique seu comprimento. Esta verificação funciona independentemente de o slice estar nil ou vazio. O mesmo vale para maps. Para projetar APIs inequívocas, você não deve distinguir entre slice nulos e vazios.

Para determinar se um slice possui elementos, podemos fazê-lo verificando se o slice é nulo ou se seu comprimento é igual a 0. Verificar o comprimento é a melhor opção a seguir, pois cobrirá ambos se o slice estiver vazio ou se o slice é nulo.

Enquanto isso, ao projetar interfaces, devemos evitar distinguir slices nulos e vazios, o que leva a erros sutis de programa√ß√£o. Ao retornar slices, n√£o deve haver diferen√ßa sem√Ęntica nem t√©cnica se retornarmos um slice nulo ou vazio. Ambos devem significar a mesma coisa para quem liga. Este princ√≠pio √© o mesmo com maps. Para verificar se um map est√° vazio, verifique seu comprimento, n√£o se √© nulo.

Código fonte

Não fazer cópias de slcies corretamente (#24)

TL;DR

Para copiar um slice para outro usando a fun√ß√£o copy, lembre-se que o n√ļmero de elementos copiados corresponde ao m√≠nimo entre os comprimentos dos dois slices.

Copiar elementos de um slice para outro √© uma opera√ß√£o razoavelmente frequente. Ao utilizar a c√≥pia, devemos lembrar que o n√ļmero de elementos copiados para o destino corresponde ao m√≠nimo entre os comprimentos dos dois slices. Tenha tamb√©m em mente que existem outras alternativas para copiar um slice, por isso n√£o devemos nos surpreender se as encontrarmos em uma base de c√≥digo.

Código fonte

Efeitos colaterais inesperados usando o slice append (#25)

TL;DR

Usar copy ou a express√£o de slice completa √© uma forma de evitar que append crie conflitos se duas fun√ß√Ķes diferentes usarem slices apoiados pela mesmo array. No entanto, apenas uma c√≥pia de slice evita vazamentos de mem√≥ria se voc√™ quiser reduzir um slice grande.

Ao usar o slicing, devemos lembrar que podemos enfrentar uma situação que leva a efeitos colaterais não intencionais. Se o slice resultante tiver um comprimento menor que sua capacidade, o acréscimo poderá alterar o slice original. Se quisermos restringir a gama de possíveis efeitos colaterais, podemos usar uma cópia de slice ou a expressão de slice completa, o que nos impede de fazer uma cópia.

Note

s[low:high:max](expressão de slice completo): Esta instrução cria um slice semelhante àquele criado com s[low:high], exceto que a capacidade de slice resultante é igual a max - low.

Código fonte

Slices e vazamentos de memória (#26)

TL;DR

Trabalhando com um slice de ponteiros ou estruturas com campos de ponteiro, você pode evitar vazamentos de memória marcando como nulos os elementos excluídos por uma operação de fatiamento.

Vazamento de capacidade

Lembre-se de que fatiar um slice ou array grande pode levar a um potencial alto consumo de memória. O espaço restante não será recuperado pelo GC e podemos manter um grande array de apoio, apesar de usarmos apenas alguns elementos. Usar uma cópia em slice é a solução para evitar tal caso.

Código fonte

Slice e ponteiros

Quando usamos a opera√ß√£o de fatiamento com ponteiros ou estruturas com campos de ponteiro, precisamos saber que o GC n√£o recuperar√° esses elementos. Nesse caso, as duas op√ß√Ķes s√£o realizar uma c√≥pia ou marcar explicitamente os elementos restantes ou seus campos como nil.

Código fonte

Inicialização ineficiente do mapa (#27)

TL;DR

Ao criar um mapa, inicialize-o com um determinado comprimento se o seu comprimento j√° for conhecido. Isso reduz o n√ļmero de aloca√ß√Ķes e melhora o desempenho.

Um mapa fornece uma coleção não ordenada de pares chave-valor em que todas as chaves são distintas. No Go, um mapa é baseado na estrutura de dados da tabela hash. Internamente, uma tabela hash é uma matriz de intervalos e cada intervalo é um ponteiro para uma matriz de pares de valores-chave.

Se soubermos de antem√£o o n√ļmero de elementos que um mapa conter√°, devemos cri√°-lo fornecendo um tamanho inicial. Fazer isso evita o crescimento potencial do mapa, o que √© bastante pesado em termos de computa√ß√£o porque requer a realoca√ß√£o de espa√ßo suficiente e o reequil√≠brio de todos os elementos.

Código fonte

Mapas e vazamentos de memória (#28)

TL;DR

Um mapa sempre pode crescer na mem√≥ria, mas nunca diminui. Portanto, se isso causar alguns problemas de mem√≥ria, voc√™ pode tentar diferentes op√ß√Ķes, como for√ßar Go a recriar o mapa ou usar ponteiros.

Leia a seção completa aqui.

Código fonte

Comparando valores incorretamente (#29)

TL;DR

Para comparar tipos em Go, voc√™ pode usar os operadores == e != se dois tipos forem compar√°veis: booleanos, numerais, strings, ponteiros, canais e estruturas s√£o compostos inteiramente de tipos compar√°veis. Caso contr√°rio, voc√™ pode usar reflect.DeepEquale pagar o pre√ßo da reflex√£o ou usar implementa√ß√Ķes e bibliotecas personalizadas.

√Č essencial entender como usar == e != para fazer compara√ß√Ķes de forma eficaz. Podemos usar esses operadores em operandos compar√°veis:

  • Booleans‚ÄĒCompara se dois booleanos s√£o iguais.
  • Numerics (int, float, and complex types)‚ÄĒCompare se dois n√ļmeros s√£o iguais.
  • Strings‚ÄĒCompare se duas strings s√£o iguais.
  • Channels‚ÄĒCompare se dois canais foram criados pela mesma chamada a ser feita ou se ambos s√£o nulos.
  • Interfaces‚ÄĒCompare se duas interfaces t√™m tipos din√Ęmicos id√™nticos e valores din√Ęmicos iguais ou se ambas s√£o nulas.
  • Pointers‚ÄĒCompare se dois ponteiros apontam para o mesmo valor na mem√≥ria ou se ambos s√£o nulos.
  • Structs and arrays‚ÄĒCompare se s√£o compostas de tipos semelhantes.
Note

Também podemos usar os operadores ?, >=, < e > com tipos numéricos para comparar valores e com strings para comparar sua ordem lexical.

Se os operandos n√£o forem compar√°veis ‚Äč‚Äč(por exemplo, slices e mapas), teremos que usar outras op√ß√Ķes, como reflex√£o. A reflex√£o √© uma forma de metaprograma√ß√£o e se refere √† capacidade de um aplicativo de introspectar e modificar sua estrutura e comportamento. Por exemplo, em Go, podemos usar reflect.DeepEqual. Esta fun√ß√£o informa se dois elementos s√£o profundamente iguais percorrendo recursivamente dois valores. Os elementos que ele aceita s√£o tipos b√°sicos mais arrays, estruturas, slices, mapas, ponteiros, interfaces e fun√ß√Ķes. No entanto, o principal problema √© a penalidade de desempenho.

Se o desempenho for crucial em tempo de execução, implementar nosso método customizado pode ser a melhor solução. Uma observação adicional: devemos lembrar que a biblioteca padrão possui alguns métodos de comparação existentes. Por exemplo, podemos usar a função bytes.Compare otimizada para comparar duas slices de bytes. Antes de implementar um método customizado, precisamos ter certeza de não reinventar a roda.

Código fonte

Estruturas de Controle

Ignorando que os elementos s√£o copiados em loops de range (#30)

TL;DR

O elemento de valor em um loop de range é uma cópia. Portanto, para modificar uma struct, por exemplo, acesse-a através de seu índice ou através de um loop for clássico (a menos que o elemento ou campo que você deseja modificar seja um ponteiro).

Um range loop permite iterar em diferentes estruturas de dados:

  • String
  • Array
  • Pointer to an array
  • Slice
  • Map
  • Receiving channel

Comparado a um for loop clássico, um loop range é uma maneira conveniente de iterar todos os elementos de uma dessas estruturas de dados, graças à sua sintaxe concisa.

Ainda assim, devemos lembrar que o elemento de valor em um range loop √© uma c√≥pia. Portanto, se o valor for uma estrutura que precisamos sofrer muta√ß√£o, atualizaremos apenas a c√≥pia, n√£o o elemento em si, a menos que o valor ou campo que modificamos seja um ponteiro. As op√ß√Ķes preferidas s√£o acessar o elemento atrav√©s do √≠ndice usando um range loop ou um loop for cl√°ssico.

Código fonte

Ignorando como os argumentos s√£o avaliados em range loops (canais e arrays) (#31)

TL;DR

Entender que a expressão passada ao operador range é avaliada apenas uma vez antes do início do loop pode ajudar a evitar erros comuns, como atribuição ineficiente em canal ou iteração de slice.

O range loop avalia a expressão fornecida apenas uma vez, antes do início do loop, fazendo uma cópia (independentemente do tipo). Devemos lembrar deste comportamento para evitar erros comuns que podem, por exemplo, nos levar a acessar o elemento errado. Por exemplo:

a := [3]int{0, 1, 2}
for i, v := range a {
    a[2] = 10
    if i == 2 {
        fmt.Println(v)
    }
}

Este c√≥digo atualiza o √ļltimo √≠ndice para 10. No entanto, se executarmos este c√≥digo, ele n√£o imprimir√° 10; imprime 2.

Código fonte

‚ö† Ignorando os impactos do uso de elementos ponteiros em range loops (#32)

Warning

Este erro não é mais relevante no Go 1.22 (detalhes).

Fazendo suposi√ß√Ķes erradas durante as itera√ß√Ķes de maps (ordena√ß√£o e inser√ß√£o do mapa durante a itera√ß√£o) (#33)

TL;DR

Para garantir resultados previsíveis ao usar maps, lembre-se de que uma estrutura de dados de mapa:

  • N√£o ordena os dados por chaves
  • N√£o preserva o pedido de inser√ß√£o
  • N√£o tem uma ordem de itera√ß√£o determin√≠stica
  • N√£o garante que um elemento adicionado durante uma itera√ß√£o ser√° produzido durante esta itera√ß√£o

Código fonte

Ignorando como a declaração break funciona (#34)

TL;DR

Usar break ou continue com um r√≥tulo imp√Ķe a quebra de uma instru√ß√£o espec√≠fica. Isso pode ser √ļtil com instru√ß√Ķes switch ou select dentro de loops.

Uma instru√ß√£o break √© comumente usada para encerrar a execu√ß√£o de um loop. Quando loops s√£o usados ‚Äč‚Äčem conjunto com switch ou select, os desenvolvedores frequentemente cometem o erro de quebrar a instru√ß√£o errada. Por exemplo:

for i := 0; i < 5; i++ {
    fmt.Printf("%d ", i)

    switch i {
    default:
    case 2:
        break
    }
}

A instrução break não encerra o loop for: em vez disso, ela encerra a instrução switch. Portanto, em vez de iterar de 0 a 2, este código itera de 0 a 4: 0 1 2 3 4.

Uma regra essencial a ter em mente é que uma instrução break encerra a execução da instrução for, switch, ou mais interna select. No exemplo anterior, ele encerra a instrução switch.

Para quebrar o loop em vez da instrução switch, a maneira mais idiomática é usar um rótulo:

loop:
    for i := 0; i < 5; i++ {
        fmt.Printf("%d ", i)

        switch i {
        default:
        case 2:
            break loop
        }
    }

Aqui, associamos o looprótulo ao for loop. Então, como fornecemos o loop rótulo para a instrução break, ela interrompe o loop, não a opção. Portanto, esta nova versão será impressa 0 1 2, como esperávamos.

Código fonte

Usando defer dentro de um loop (#35)

TL;DR

Extrair a lógica do loop dentro de uma função leva à execução de uma instrução defer no final de cada iteração.

A instru√ß√£o defer atrasa a execu√ß√£o de uma chamada at√© que a fun√ß√£o circundante retorne. √Č usado principalmente para reduzir o c√≥digo padr√£o. Por exemplo, se um recurso precisar ser fechado eventualmente, podemos usar defer para evitar a repeti√ß√£o das chamadas de fechamento antes de cada return.

Um erro comum com defer é esquecer que ele agenda uma chamada de função quando a função circundante retorna. Por exemplo:

func readFiles(ch <-chan string) error {
    for path := range ch {
        file, err := os.Open(path)
        if err != nil {
            return err
        }

        defer file.Close()

        // Do something with file
    }
    return nil
}

As chamadas defer não são executadas durante cada iteração do loop, mas quando a função readFiles retorna. Se readFiles não retornar, os descritores de arquivos ficarão abertos para sempre, causando vazamentos.

Uma opção comum para corrigir esse problema é criar uma função circundante após defer, chamada durante cada iteração:

func readFiles(ch <-chan string) error {
    for path := range ch {
        if err := readFile(path); err != nil {
            return err
        }
    }
    return nil
}

func readFile(path string) error {
    file, err := os.Open(path)
    if err != nil {
        return err
    }

    defer file.Close()

    // Do something with file
    return nil
}

Outra solução é tornar a função readFile um encerramento, mas intrinsecamente, esta permanece a mesma solução: adicionar outra função circundante para executar as chamadas defer durante cada iteração.

Código fonte

Strings

N√£o entendendo o conceito de rune (#36)

TL;DR

Entender que uma runa corresponde ao conceito de um ponto de c√≥digo Unicode e que pode ser composta de m√ļltiplos bytes deve fazer parte do conhecimento b√°sico do desenvolvedor Go para trabalhar com precis√£o com strings.

Como as runas estão por toda parte no Go, é importante entender o seguinte:

  • Um conjunto de caracteres √© um conjunto de caracteres, enquanto uma codifica√ß√£o descreve como traduzir um conjunto de caracteres em bin√°rio.
  • No Go, uma string faz refer√™ncia a uma fatia imut√°vel de bytes arbitr√°rios.
  • O c√≥digo-fonte Go √© codificado usando UTF-8. Portanto, todos os literais de string s√£o strings UTF-8. Mas como uma string pode conter bytes arbitr√°rios, se for obtida de outro lugar (n√£o do c√≥digo-fonte), n√£o h√° garantia de que seja baseada na codifica√ß√£o UTF-8.
  • A rune corresponde ao conceito de ponto de c√≥digo Unicode, significando um item representado por um √ļnico valor.
  • Usando UTF-8, um ponto de c√≥digo Unicode pode ser codificado em 1 a 4 bytes.
  • Usar len() na string em Go retorna o n√ļmero de bytes, n√£o o n√ļmero de runas.

Código fonte

Iteração de string imprecisa (#37)

TL;DR

Iterar em uma string com o operador range itera nas runas com o índice correspondente ao índice inicial da sequência de bytes da runa. Para acessar um índice de runa específico (como a terceira runa), converta a string em um arquivo []rune.

Iterar em uma string é uma operação comum para desenvolvedores. Talvez queiramos realizar uma operação para cada runa na string ou implementar uma função personalizada para procurar uma substring específica. Em ambos os casos, temos que iterar nas diferentes runas de uma string. Mas é fácil ficar confuso sobre como funciona a iteração.

For example, consider the following example:

s := "hêllo"
for i := range s {
    fmt.Printf("position %d: %c\n", i, s[i])
}
fmt.Printf("len=%d\n", len(s))
position 0: h
position 1: Ã
position 3: l
position 4: l
position 5: o
len=6

Vamos destacar três pontos que podem ser confusos:

  • A segunda runa √© √É na sa√≠da em vez de √™.
  • Saltamos da posi√ß√£o 1 para a posi√ß√£o 3: o que h√° na posi√ß√£o 2?
  • len retorna uma contagem de 6, enquanto s cont√©m apenas 5 runas.

Vamos come√ßar com a √ļltima observa√ß√£o. J√° mencionamos que len retorna o n√ļmero de bytes em uma string, n√£o o n√ļmero de runas. Como atribu√≠mos uma string literal a s, s √© uma string UTF-8. Enquanto isso, o caractere especial ‚Äú√™‚ÄĚ n√£o √© codificado em um √ļnico byte; requer 2 bytes. Portanto, chamar len(s) retorna 6.

Enquanto isso, no exemplo anterior, temos que entender que não repetimos cada runa; em vez disso, iteramos sobre cada índice inicial de uma runa:

Imprimir s[i] não imprime a i-ésima runa; imprime a representação UTF-8 do byte em index i. Portanto, imprimimos "hÃllo" em vez de "hêllo".

Se quisermos imprimir todas as diferentes runas, podemos usar o elemento value do operador range:

s := "hêllo"
for i, r := range s {
    fmt.Printf("position %d: %c\n", i, r)
}

Ou podemos converter a string em uma fatia de runas e iterar sobre ela:

s := "hêllo"
runes := []rune(s)
for i, r := range runes {
    fmt.Printf("position %d: %c\n", i, r)
}

Observe que esta solu√ß√£o introduz uma sobrecarga de tempo de execu√ß√£o em compara√ß√£o com a anterior. Na verdade, converter uma string em uma fatia de runas requer a aloca√ß√£o de uma fatia adicional e a convers√£o dos bytes em runas: uma complexidade de tempo O(n) com n o n√ļmero de bytes na string. Portanto, se quisermos iterar todas as runas, devemos usar a primeira solu√ß√£o.

Porém, se quisermos acessar a i-ésima runa de uma string com a primeira opção, não teremos acesso ao índice da runa; em vez disso, conhecemos o índice inicial de uma runa na sequência de bytes.

s := "hêllo"
r := []rune(s)[4]
fmt.Printf("%c\n", r) // o

Código fonte

Uso indevido de fun√ß√Ķes de trim (#38)

TL;DR

strings.TrimRight/strings.TrimLeft remove todas as runas finais/iniciais contidas em um determinado conjunto, enquanto strings.TrimSuffix/strings.TrimPrefix retorna uma string sem um sufixo/prefixo fornecido.

Por exemplo:

fmt.Println(strings.TrimRight("123oxo", "xo"))

O exemplo imprime 123:

Por outro lado, strings.TrimLeft remove todas as runas principais contidas em um conjunto.

Por outro lado, strings.TrimSuffix/strings.TrimPrefix retorna uma string sem o sufixo/prefixo final fornecido.

Código fonte

Concatenação de strings subotimizada (#39)

TL;DR

A concatenação de uma lista de strings deve ser feita com strings.Builder para evitar a alocação de uma nova string durante cada iteração.

Vamos considerar uma função concat que concatena todos os elementos string de uma fatia usando o operador +=:

func concat(values []string) string {
    s := ""
    for _, value := range values {
        s += value
    }
    return s
}

Durante cada iteração, o operador += concatena com s a sequência de valores. À primeira vista, esta função pode não parecer errada. Mas com esta implementação, esquecemos uma das principais características de uma string: a sua imutabilidade. Portanto, cada iteração não é atualizada s; ele realoca uma nova string na memória, o que impacta significativamente o desempenho desta função.

Felizmente, existe uma solução para lidar com esse problema, usando strings.Builder:

func concat(values []string) string {
    sb := strings.Builder{}
    for _, value := range values {
        _, _ = sb.WriteString(value)
    }
    return sb.String()
}

Durante cada itera√ß√£o, constru√≠mos a string resultante chamando o m√©todo WriteString que anexa o conte√ļdo do valor ao seu buffer interno, minimizando assim a c√≥pia da mem√≥ria.

Note

WriteString retorna um erro como segunda sa√≠da, mas n√≥s o ignoramos propositalmente. Na verdade, este m√©todo nunca retornar√° um erro diferente de zero. Ent√£o, qual √© o prop√≥sito deste m√©todo retornar um erro como parte de sua assinatura? strings.Builder implementa a io.StringWriter interface, que cont√©m um √ļnico m√©todo: WriteString(s string) (n int, err error). Portanto, para estar em conformidade com esta interface, WriteString deve retornar um erro.

Internamente, strings.Builder cont√©m uma fatia de bytes. Cada chamada para WriteString resulta em uma chamada para anexar nesta fatia. Existem dois impactos. Primeiro, esta estrutura n√£o deve ser usada simultaneamente, pois as chamadas append levariam a condi√ß√Ķes de corrida. O segundo impacto √© algo que vimos no mistake #21, "Inicializa√ß√£o de slice ineficiente": se o comprimento futuro de uma slice j√° for conhecido, devemos pr√©-aloc√°-la. Para isso, strings.Builder exp√Ķe um m√©todo Grow(n int) para garantir espa√ßo para outros n bytes:

func concat(values []string) string {
    total := 0
    for i := 0; i < len(values); i++ {
        total += len(values[i])
    }

    sb := strings.Builder{}
    sb.Grow(total) (2)
    for _, value := range values {
        _, _ = sb.WriteString(value)
    }
    return sb.String()
}

Vamos executar um benchmark para comparar as tr√™s vers√Ķes (v1 usando +=; v2 usando strings.Builder{} sem pr√©-aloca√ß√£o; e v3 usando strings.Builder{} com pr√©-aloca√ß√£o). A slice de entrada cont√©m 1.000 strings e cada string cont√©m 1.000 bytes:

BenchmarkConcatV1-4             16      72291485 ns/op
BenchmarkConcatV2-4           1188        878962 ns/op
BenchmarkConcatV3-4           5922        190340 ns/op

Como podemos ver, a versão mais recente é de longe a mais eficiente: 99% mais rápida que a v1 e 78% mais rápida que a v2.

strings.Builder é a solução recomendada para concatenar uma lista de strings. Normalmente, esta solução deve ser usada dentro de um loop. Na verdade, se precisarmos apenas concatenar algumas strings (como um nome e um sobrenome), o uso strings.Builder não é recomendado, pois isso tornará o código um pouco menos legível do que usar o operador += or fmt.Sprintf.

Código fonte

Convers√Ķes de string in√ļteis (#40)

TL;DR

Lembrar que o pacote bytes oferece as mesmas opera√ß√Ķes que o pacote strings pode ajudar a evitar convers√Ķes extras de bytes/string.

Ao optar por trabalhar com uma string ou um []byte, a maioria dos programadores tende a preferir strings por conveniência. Mas a maior parte da E/S é realmente feita com []byte. Por exemplo, io.Reader, io.Writer e io.ReadAll trabalham com []byte, não com strings.

Quando nos perguntamos se devemos trabalhar com strings ou []byte, lembremos que trabalhar com []byten√£o √© necessariamente menos conveniente. Na verdade, todas as fun√ß√Ķes exportadas do pacote strings tamb√©m possuem alternativas no pacote bytes: Split, Count, Contains, Index e assim por diante. Portanto, estejamos fazendo I/O ou n√£o, devemos primeiro verificar se poder√≠amos implementar um fluxo de trabalho completo usando bytes em vez de strings e evitar o pre√ßo de convers√Ķes adicionais.

Código fonte

Vazamentos de substring e memória (#41)

TL;DR

Usar cópias em vez de substrings pode evitar vazamentos de memória, pois a string retornada por uma operação de substring será apoiada pela mesma matriz de bytes.

In mistake #26, ‚ÄúSlices and memory leaks,‚ÄĚ we saw how slicing a slice or array may lead to memory leak situations. This principle also applies to string and substring operations.

We need to keep two things in mind while using the substring operation in Go. First, the interval provided is based on the number of bytes, not the number of runes. Second, a substring operation may lead to a memory leak as the resulting substring will share the same backing array as the initial string. The solutions to prevent this case from happening are to perform a string copy manually or to use strings.Clone from Go 1.18.

Código fonte

Functions and Methods

N√£o saber que tipo de receptor usar (#42)

TL;DR

A decis√£o de usar um valor ou um receptor de ponteiro deve ser tomada com base em fatores como o tipo, se deve sofrer muta√ß√£o, se cont√©m um campo que n√£o pode ser copiado e o tamanho do objeto. Em caso de d√ļvida, use um receptor de ponteiro.

Choosing between value and pointer receivers isn’t always straightforward. Let’s discuss some of the conditions to help us choose.

A receiver must be a pointer

  • If the method needs to mutate the receiver. This rule is also valid if the receiver is a slice and a method needs to append elements:
type slice []int

func (s *slice) add(element int) {
    *s = append(*s, element)
}

A receiver should be a pointer

  • If the receiver is a large object. Using a pointer can make the call more efficient, as doing so prevents making an extensive copy. When in doubt about how large is large, benchmarking can be the solution; it‚Äôs pretty much impossible to state a specific size, because it depends on many factors.

A receiver must be a value

  • If we have to enforce a receiver‚Äôs immutability.
  • If the receiver is a map, function, or channel. Otherwise, a compilation error occurs.

A receiver should be a value

  • If the receiver is a slice that doesn‚Äôt have to be mutated.
  • If the receiver is a small array or struct that is naturally a value type without mutable fields, such as time.Time.
  • If the receiver is a basic type such as int, float64, or string.

Of course, it’s impossible to be exhaustive, as there will always be edge cases, but this section’s goal was to provide guidance to cover most cases. By default, we can choose to go with a value receiver unless there’s a good reason not to do so. In doubt, we should use a pointer receiver.

Código fonte

Nunca usando par√Ęmetros de resultado nomeados (#43)

TL;DR

Usar par√Ęmetros de resultado nomeados pode ser uma maneira eficiente de melhorar a legibilidade de uma fun√ß√£o/m√©todo, especialmente se v√°rios par√Ęmetros de resultado tiverem o mesmo tipo. Em alguns casos, esta abordagem tamb√©m pode ser conveniente porque os par√Ęmetros de resultado nomeados s√£o inicializados com seu valor zero. Mas tenha cuidado com os poss√≠veis efeitos colaterais.

When we return parameters in a function or a method, we can attach names to these parameters and use them as regular variables. When a result parameter is named, it’s initialized to its zero value when the function/method begins. With named result parameters, we can also call a naked return statement (without arguments). In that case, the current values of the result parameters are used as the returned values.

Here’s an example that uses a named result parameter b:

func f(a int) (b int) {
    b = a
    return
}

In this example, we attach a name to the result parameter: b. When we call return without arguments, it returns the current value of b.

In some cases, named result parameters can also increase readability: for example, if two parameters have the same type. In other cases, they can also be used for convenience. Therefore, we should use named result parameters sparingly when there’s a clear benefit.

Código fonte

Efeitos colaterais n√£o intencionais com par√Ęmetros de resultado nomeados (#44)

TL;DR

Consulte #43.

We mentioned why named result parameters can be useful in some situations. But as these result parameters are initialized to their zero value, using them can sometimes lead to subtle bugs if we’re not careful enough. For example, can you spot what’s wrong with this code?

func (l loc) getCoordinates(ctx context.Context, address string) (
    lat, lng float32, err error) {
    isValid := l.validateAddress(address) (1)
    if !isValid {
        return 0, 0, errors.New("invalid address")
    }

    if ctx.Err() != nil { (2)
        return 0, 0, err
    }

    // Get and return coordinates
}

The error might not be obvious at first glance. Here, the error returned in the if ctx.Err() != nil scope is err. But we haven’t assigned any value to the err variable. It’s still assigned to the zero value of an error type: nil. Hence, this code will always return a nil error.

When using named result parameters, we must recall that each parameter is initialized to its zero value. As we have seen in this section, this can lead to subtle bugs that aren’t always straightforward to spot while reading code. Therefore, let’s remain cautious when using named result parameters, to avoid potential side effects.

Código fonte

Retornando um receptor nulo (#45)

TL;DR

Ao retornar uma interface, tenha cuidado para não retornar um ponteiro nulo, mas um valor nulo explícito. Caso contrário, poderão ocorrer consequências não intencionais e o chamador receberá um valor diferente de zero.

Código fonte

Usando um nome de arquivo como entrada de função (#46)

TL;DR

Projetar fun√ß√Ķes para receber tipos io.Reader em vez de nomes de arquivos melhora a capacidade de reutiliza√ß√£o de uma fun√ß√£o e facilita o teste.

Accepting a filename as a function input to read from a file should, in most cases, be considered a code smell (except in specific functions such as os.Open). Indeed, it makes unit tests more complex because we may have to create multiple files. It also reduces the reusability of a function (although not all functions are meant to be reused). Using the io.Reader interface abstracts the data source. Regardless of whether the input is a file, a string, an HTTP request, or a gRPC request, the implementation can be reused and easily tested.

Código fonte

Ignorando como argumentos defer e receptores são avaliados (avaliação de argumentos, ponteiros e receptores de valor) (#47)

TL;DR

Passar um ponteiro para uma fun√ß√£o defer e agrupar uma chamada dentro de um closure s√£o duas solu√ß√Ķes poss√≠veis para superar a avalia√ß√£o imediata de argumentos e receptores.

In a defer function the arguments are evaluated right away, not once the surrounding function returns. For example, in this code, we always call notify and incrementCounter with the same status: an empty string.

const (
    StatusSuccess  = "success"
    StatusErrorFoo = "error_foo"
    StatusErrorBar = "error_bar"
)

func f() error {
    var status string
    defer notify(status)
    defer incrementCounter(status)

    if err := foo(); err != nil {
        status = StatusErrorFoo
        return err
    }

    if err := bar(); err != nil {
        status = StatusErrorBar
        return err
    }

    status = StatusSuccess <5>
    return nil
}

Indeed, we call notify(status) and incrementCounter(status) as defer functions. Therefore, Go will delay these calls to be executed once f returns with the current value of status at the stage we used defer, hence passing an empty string.

Two leading options if we want to keep using defer.

The first solution is to pass a string pointer:

func f() error {
    var status string
    defer notify(&status) 
    defer incrementCounter(&status)

    // The rest of the function unchanged
}

Using defer evaluates the arguments right away: here, the address of status. Yes, status itself is modified throughout the function, but its address remains constant, regardless of the assignments. Hence, if notify or incrementCounter uses the value referenced by the string pointer, it will work as expected. But this solution requires changing the signature of the two functions, which may not always be possible.

There’s another solution: calling a closure (an anonymous function value that references variables from outside its body) as a defer statement:

func f() error {
    var status string
    defer func() {
        notify(status)
        incrementCounter(status)
    }()

    // The rest of the function unchanged
}

Here, we wrap the calls to both notify and incrementCounter within a closure. This closure references the status variable from outside its body. Therefore, status is evaluated once the closure is executed, not when we call defer. This solution also works and doesn’t require notify and incrementCounter to change their signature.

Let's also note this behavior applies with method receiver: the receiver is evaluated immediately.

Código fonte

Error Management

P√Ęnico (#48)

TL;DR

Usar panic √© uma op√ß√£o para lidar com erros no Go. No entanto, s√≥ deve ser usado com modera√ß√£o em condi√ß√Ķes irrecuper√°veis: por exemplo, para sinalizar um erro do programador ou quando voc√™ n√£o consegue carregar uma depend√™ncia obrigat√≥ria.

In Go, panic is a built-in function that stops the ordinary flow:

func main() {
    fmt.Println("a")
    panic("foo")
    fmt.Println("b")
}

This code prints a and then stops before printing b:

a
panic: foo

goroutine 1 [running]:
main.main()
        main.go:7 +0xb3

Panicking in Go should be used sparingly. There are two prominent cases, one to signal a programmer error (e.g., sql.Register that panics if the driver is nil or has already been register) and another where our application fails to create a mandatory dependency. Hence, exceptional conditions that lead us to stop the application. In most other cases, error management should be done with a function that returns a proper error type as the last return argument.

Código fonte

Ignorando quando embrulhar um erro (#49)

TL;DR

Embrulhar um erro permite marcar um erro e/ou fornecer contexto adicional. No entanto, o agrupamento de erros cria um acoplamento potencial, pois disponibiliza o erro de origem para o chamador. Se você quiser evitar isso, não use a agrupamento automático de erros.

Since Go 1.13, the %w directive allows us to wrap errors conveniently. Error wrapping is about wrapping or packing an error inside a wrapper container that also makes the source error available. In general, the two main use cases for error wrapping are the following:

  • Adding additional context to an error
  • Marking an error as a specific error

When handling an error, we can decide to wrap it. Wrapping is about adding additional context to an error and/or marking an error as a specific type. If we need to mark an error, we should create a custom error type. However, if we just want to add extra context, we should use fmt.Errorf with the %w directive as it doesn’t require creating a new error type. Yet, error wrapping creates potential coupling as it makes the source error available for the caller. If we want to prevent it, we shouldn’t use error wrapping but error transformation, for example, using fmt.Errorf with the %v directive.

Código fonte

Comparando um tipo de erro de forma imprecisa (#50)

TL;DR

Se voc√™ usar o agrupamento de erros do Go 1.13 com a diretiva %w e fmt.Errorf, a compara√ß√£o de um erro com um tipo dever√° ser feita usando errors.As. Caso contr√°rio, se o erro retornado que voc√™ deseja verificar for embrulhado, as verifica√ß√Ķes falhar√£o.

Código fonte

Comparando um valor de erro incorretamente (#51)

TL;DR

Se voc√™ usar o agrupamento de erros do Go 1.13 com a diretiva %w e fmt.Errorf, a compara√ß√£o de um erro ou de um valor dever√° ser feita usando errors.As. Caso contr√°rio, se o erro retornado que voc√™ deseja verificar for embrulhado, as verifica√ß√Ķes falhar√£o.

A sentinel error is an error defined as a global variable:

import "errors"

var ErrFoo = errors.New("foo")

In general, the convention is to start with Err followed by the error type: here, ErrFoo. A sentinel error conveys an expected error, an error that clients will expect to check. As general guidelines:

  • Expected errors should be designed as error values (sentinel errors): var ErrFoo = errors.New("foo").
  • Unexpected errors should be designed as error types: type BarError struct { ... }, with BarError implementing the error interface.

If we use error wrapping in our application with the %w directive and fmt.Errorf, checking an error against a specific value should be done using errors.Is instead of ==. Thus, even if the sentinel error is wrapped, errors.Is can recursively unwrap it and compare each error in the chain against the provided value.

Código fonte

Lidando com um erro duas vezes (#52)

TL;DR

Na maioria das situa√ß√Ķes, um erro deve ser tratado apenas uma vez. Registrar um erro √© tratar um erro. Portanto, voc√™ deve escolher entre registrar ou retornar um erro. Em muitos casos, o embrulho autom√°tico de erros √© a solu√ß√£o, pois permite fornecer contexto adicional a um erro e retornar o erro de origem.

Handling an error multiple times is a mistake made frequently by developers, not specifically in Go. This can cause situations where the same error is logged multiple times make debugging harder.

Let's remind us that handling an error should be done only once. Logging an error is handling an error. Hence, we should either log or return an error. By doing this, we simplify our code and gain better insights into the error situation. Using error wrapping is the most convenient approach as it allows us to propagate the source error and add context to an error.

Código fonte

N√£o tratando de um erro (#53)

TL;DR

Ignorar um erro, seja durante uma chamada de função ou em uma função defer, deve ser feito explicitamente usando o identificador em branco. Caso contrário, os futuros leitores poderão ficar confusos sobre se foi intencional ou um erro.

Código fonte

N√£o tratando erros de defer (#54)

TL;DR

Em muitos casos, você não deve ignorar um erro retornado por uma função defer. Manipule-o diretamente ou propague-o para o chamador, dependendo do contexto. Se você quiser ignorá-lo, use o identificador em branco.

Consider the following code:

func f() {
  // ...
  notify() // Error handling is omitted
}

func notify() error {
  // ...
}

From a maintainability perspective, the code can lead to some issues. Let’s consider a new reader looking at it. This reader notices that notify returns an error but that the error isn’t handled by the parent function. How can they guess whether or not handling the error was intentional? How can they know whether the previous developer forgot to handle it or did it purposely?

For these reasons, when we want to ignore an error, there's only one way to do it, using the blank identifier (_):

_ = notify

In terms of compilation and run time, this approach doesn’t change anything compared to the first piece of code. But this new version makes explicit that we aren’t interested in the error. Also, we can add a comment that indicates the rationale for why an error is ignored:

// At-most once delivery.
// Hence, it's accepted to miss some of them in case of errors.
_ = notify()

Código fonte

Concurrency: Foundations

Misturando simultaneidade e paralelismo (#55)

TL;DR

Compreender as diferenças fundamentais entre simultaneidade e paralelismo é a base do conhecimento do desenvolvedor Go. A simultaneidade tem a ver com estrutura, enquanto o paralelismo tem a ver com execução.

Concurrency and parallelism are not the same:

  • Concurrency is about structure. We can change a sequential implementation into a concurrent one by introducing different steps that separate concurrent goroutines can tackle.
  • Meanwhile, parallelism is about execution. We can use parallism at the steps level by adding more parallel goroutines.

In summary, concurrency provides a structure to solve a problem with parts that may be parallelized. Therefore, concurrency enables parallelism.

Pensar que a simultaneidade é sempre mais rápida (#56)

TL;DR

Para ser um desenvolvedor proficiente, voc√™ deve reconhecer que a simultaneidade nem sempre √© mais r√°pida. As solu√ß√Ķes que envolvem a paraleliza√ß√£o de cargas de trabalho m√≠nimas podem n√£o ser necessariamente mais r√°pidas do que uma implementa√ß√£o sequencial. A avalia√ß√£o comparativa de solu√ß√Ķes sequenciais versus solu√ß√Ķes simult√Ęneas deve ser a forma de validar suposi√ß√Ķes.

Read the full section here.

Código fonte

Ficar confuso sobre quando usar canais ou mutexes (#57)

TL;DR

Estar ciente das intera√ß√Ķes goroutine tamb√©m pode ser √ļtil ao decidir entre canais e mutexes. Em geral, goroutines paralelas requerem sincroniza√ß√£o e, portanto, mutexes. Por outro lado, goroutines simult√Ęneas geralmente requerem coordena√ß√£o e orquestra√ß√£o e, portanto, canais.

Given a concurrency problem, it may not always be clear whether we can implement a solution using channels or mutexes. Because Go promotes sharing memory by communication, one mistake could be to always force the use of channels, regardless of the use case. However, we should see the two options as complementary.

When should we use channels or mutexes? We will use the example in the next figure as a backbone. Our example has three different goroutines with specific relationships:

  • G1 and G2 are parallel goroutines. They may be two goroutines executing the same function that keeps receiving messages from a channel, or perhaps two goroutines executing the same HTTP handler at the same time.
  • On the other hand, G1 and G3 are concurrent goroutines, as are G2 and G3. All the goroutines are part of an overall concurrent structure, but G1 and G2 perform the first step, whereas G3 does the next step.

In general, parallel goroutines have to synchronize: for example, when they need to access or mutate a shared resource such as a slice. Synchronization is enforced with mutexes but not with any channel types (not with buffered channels). Hence, in general, synchronization between parallel goroutines should be achieved via mutexes.

Conversely, in general, concurrent goroutines have to coordinate and orchestrate. For example, if G3 needs to aggregate results from both G1 and G2, G1 and G2 need to signal to G3 that a new intermediate result is available. This coordination falls under the scope of communication‚ÄĒtherefore, channels.

Regarding concurrent goroutines, there’s also the case where we want to transfer the ownership of a resource from one step (G1 and G2) to another (G3); for example, if G1 and G2 are enriching a shared resource and at some point, we consider this job as complete. Here, we should use channels to signal that a specific resource is ready and handle the ownership transfer.

Mutexes and channels have different semantics. Whenever we want to share a state or access a shared resource, mutexes ensure exclusive access to this resource. Conversely, channels are a mechanic for signaling with or without data (chan struct{} or not). Coordination or ownership transfer should be achieved via channels. It’s important to know whether goroutines are parallel or concurrent because, in general, we need mutexes for parallel goroutines and channels for concurrent ones.

N√£o entender os problemas de corrida (corridas de dados vs. condi√ß√Ķes de corrida e o modelo de mem√≥ria Go) (#58)

TL;DR

Ser proficiente em simultaneidade tamb√©m significa compreender que corridas de dados e condi√ß√Ķes de corrida s√£o conceitos diferentes. As corridas de dados ocorrem quando v√°rias goroutines acessam simultaneamente o mesmo local de mem√≥ria e pelo menos uma delas est√° gravando. Enquanto isso, estar livre de disputa de dados n√£o significa necessariamente execu√ß√£o determin√≠stica. Quando um comportamento depende da sequ√™ncia ou do tempo de eventos que n√£o podem ser controlados, esta √© uma condi√ß√£o de corrida.

Race problems can be among the hardest and most insidious bugs a programmer can face. As Go developers, we must understand crucial aspects such as data races and race conditions, their possible impacts, and how to avoid them.

Data Race

A data race occurs when two or more goroutines simultaneously access the same memory location and at least one is writing. In this case, the result can be hazardous. Even worse, in some situations, the memory location may end up holding a value containing a meaningless combination of bits.

We can prevent a data race from happening using different techniques. For example:

  • Using the sync/atomic package
  • In synchronizing the two goroutines with an ad hoc data structure like a mutex
  • Using channels to make the two goroutines communicating to ensure that a variable is updated by only one goroutine at a time

Race Condition

Depending on the operation we want to perform, does a data-race-free application necessarily mean a deterministic result? Not necessarily.

A race condition occurs when the behavior depends on the sequence or the timing of events that can’t be controlled. Here, the timing of events is the goroutines’ execution order.

In summary, when we work in concurrent applications, it’s essential to understand that a data race is different from a race condition. A data race occurs when multiple goroutines simultaneously access the same memory location and at least one of them is writing. A data race means unexpected behavior. However, a data-race-free application doesn’t necessarily mean deterministic results. An application can be free of data races but still have behavior that depends on uncontrolled events (such as goroutine execution, how fast a message is published to a channel, or how long a call to a database lasts); this is a race condition. Understanding both concepts is crucial to becoming proficient in designing concurrent applications.

Código fonte

N√£o compreender os impactos de simultaneidade de um tipo de carga de trabalho (#59)

TL;DR

Ao criar um determinado n√ļmero de goroutines, considere o tipo de carga de trabalho. Criar goroutines vinculadas √† CPU significa limitar esse n√ļmero pr√≥ximo √† vari√°vel GOMAXPROCS (baseado por padr√£o no n√ļmero de n√ļcleos de CPU no host). A cria√ß√£o de goroutines vinculadas a E/S depende de outros fatores, como o sistema externo.

In programming, the execution time of a workload is limited by one of the following:

  • The speed of the CPU‚ÄĒFor example, running a merge sort algorithm. The workload is called CPU-bound.
  • The speed of I/O‚ÄĒFor example, making a REST call or a database query. The workload is called I/O-bound.
  • The amount of available memory‚ÄĒThe workload is called memory-bound.
Note

The last is the rarest nowadays, given that memory has become very cheap in recent decades. Hence, this section focuses on the two first workload types: CPU- and I/O-bound.

If the workload executed by the workers is I/O-bound, the value mainly depends on the external system. Conversely, if the workload is CPU-bound, the optimal number of goroutines is close to the number of available CPU cores (a best practice can be to use runtime.GOMAXPROCS). Knowing the workload type (I/O or CPU) is crucial when designing concurrent applications.

Código fonte

Incompreens√£o dos contextos Go (#60)

TL;DR

Os contextos Go também são um dos pilares da simultaneidade em Go. Um contexto permite que você carregue um prazo, um sinal de cancelamento e/ou uma lista de valores-chave.

https://pkg.go.dev/context

A Context carries a deadline, a cancellation signal, and other values across API boundaries.

Deadline

A deadline refers to a specific point in time determined with one of the following:

  • A time.Duration from now (for example, in 250 ms)
  • A time.Time (for example, 2023-02-07 00:00:00 UTC)

The semantics of a deadline convey that an ongoing activity should be stopped if this deadline is met. An activity is, for example, an I/O request or a goroutine waiting to receive a message from a channel.

Cancellation signals

Another use case for Go contexts is to carry a cancellation signal. Let’s imagine that we want to create an application that calls CreateFileWatcher(ctx context.Context, filename string) within another goroutine. This function creates a specific file watcher that keeps reading from a file and catches updates. When the provided context expires or is canceled, this function handles it to close the file descriptor.

Context values

The last use case for Go contexts is to carry a key-value list. What’s the point of having a context carrying a key-value list? Because Go contexts are generic and mainstream, there are infinite use cases.

For example, if we use tracing, we may want different subfunctions to share the same correlation ID. Some developers may consider this ID too invasive to be part of the function signature. In this regard, we could also decide to include it as part of the provided context.

Catching a context cancellation

The context.Context type exports a Done method that returns a receive-only notification channel: <-chan struct{}. This channel is closed when the work associated with the context should be canceled. For example,

  • The Done channel related to a context created with context.WithCancel is closed when the cancel function is called.
  • The Done channel related to a context created with context.WithDeadline is closed when the deadline has expired.

One thing to note is that the internal channel should be closed when a context is canceled or has met a deadline, instead of when it receives a specific value, because the closure of a channel is the only channel action that all the consumer goroutines will receive. This way, all the consumers will be notified once a context is canceled or a deadline is reached.

In summary, to be a proficient Go developer, we have to understand what a context is and how to use it. In general, a function that users wait for should take a context, as doing so allows upstream callers to decide when calling this function should be aborted.

Código fonte

Concurrency: Practice

Propagando um contexto impróprio (#61)

TL;DR

Compreender as condi√ß√Ķes em que um contexto pode ser cancelado deve ser importante ao propag√°-lo: por exemplo, um manipulador HTTP cancelando o contexto quando a resposta for enviada.

In many situations, it is recommended to propagate Go contexts. However, context propagation can sometimes lead to subtle bugs, preventing subfunctions from being correctly executed.

Let’s consider the following example. We expose an HTTP handler that performs some tasks and returns a response. But just before returning the response, we also want to send it to a Kafka topic. We don’t want to penalize the HTTP consumer latency-wise, so we want the publish action to be handled asynchronously within a new goroutine. We assume that we have at our disposal a publish function that accepts a context so the action of publishing a message can be interrupted if the context is canceled, for example. Here is a possible implementation:

func handler(w http.ResponseWriter, r *http.Request) {
    response, err := doSomeTask(r.Context(), r)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
    return
    }
    go func() {
        err := publish(r.Context(), response)
        // Do something with err
    }()
    writeResponse(response)
}

What’s wrong with this piece of code? We have to know that the context attached to an HTTP request can cancel in different conditions:

  • When the client‚Äôs connection closes
  • In the case of an HTTP/2 request, when the request is canceled
  • When the response has been written back to the client

In the first two cases, we probably handle things correctly. For example, if we get a response from doSomeTask but the client has closed the connection, it’s probably OK to call publish with a context already canceled so the message isn’t published. But what about the last case?

When the response has been written to the client, the context associated with the request will be canceled. Therefore, we are facing a race condition:

  • If the response is written after the Kafka publication, we both return a response and publish a message successfully
  • However, if the response is written before or during the Kafka publication, the message shouldn‚Äôt be published.

In the latter case, calling publish will return an error because we returned the HTTP response quickly.

Note

From Go 1.21, there is a way to create a new context without cancel. context.WithoutCancel returns a copy of parent that is not canceled when parent is canceled.

In summary, propagating a context should be done cautiously.

Código fonte

Iniciando uma goroutine sem saber quando interrompê-la (#62)

TL;DR

Evitar vazamentos significa estar ciente de que sempre que uma goroutine for iniciada, você deve ter um plano para interrompê-la eventualmente.

Goroutines are easy and cheap to start‚ÄĒso easy and cheap that we may not necessarily have a plan for when to stop a new goroutine, which can lead to leaks. Not knowing when to stop a goroutine is a design issue and a common concurrency mistake in Go.

Let’s discuss a concrete example. We will design an application that needs to watch some external configuration (for example, using a database connection). Here’s a first implementation:

func main() {
    newWatcher()
    // Run the application
}

type watcher struct { /* Some resources */ }

func newWatcher() {
    w := watcher{}
    go w.watch() // Creates a goroutine that watches some external configuration
}

The problem with this code is that when the main goroutine exits (perhaps because of an OS signal or because it has a finite workload), the application is stopped. Hence, the resources created by watcher aren’t closed gracefully. How can we prevent this from happening?

One option could be to pass to newWatcher a context that will be canceled when main returns:

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    newWatcher(ctx)
    // Run the application
}

func newWatcher(ctx context.Context) {
    w := watcher{}
    go w.watch(ctx)
}

We propagate the context created to the watch method. When the context is canceled, the watcher struct should close its resources. However, can we guarantee that watch will have time to do so? Absolutely not‚ÄĒand that‚Äôs a design flaw.

The problem is that we used signaling to convey that a goroutine had to be stopped. We didn’t block the parent goroutine until the resources had been closed. Let’s make sure we do:

func main() {
    w := newWatcher()
    defer w.close()
    // Run the application
}

func newWatcher() watcher {
    w := watcher{}
    go w.watch()
    return w
}

func (w watcher) close() {
    // Close the resources
}

Instead of signaling watcher that it’s time to close its resources, we now call this close method, using defer to guarantee that the resources are closed before the application exits.

In summary, let’s be mindful that a goroutine is a resource like any other that must eventually be closed to free memory or other resources. Starting a goroutine without knowing when to stop it is a design issue. Whenever a goroutine is started, we should have a clear plan about when it will stop. Last but not least, if a goroutine creates resources and its lifetime is bound to the lifetime of the application, it’s probably safer to wait for this goroutine to complete before exiting the application. This way, we can ensure that the resources can be freed.

Código fonte

‚ö† N√£o ter cuidado com goroutines e vari√°veis ‚Äč‚Äčde loop (#63)

Warning

Este erro não é mais relevante no Go 1.22 (detalhes).

Esperando um comportamento determinístico usando seleção e canais (#64)

TL;DR

Compreender que com select v√°rios canais escolhe o caso aleatoriamente se m√ļltiplas op√ß√Ķes forem poss√≠veis evita fazer suposi√ß√Ķes erradas que podem levar a erros sutis de simultaneidade.

One common mistake made by Go developers while working with channels is to make wrong assumptions about how select behaves with multiple channels.

For example, let's consider the following case (disconnectCh is a unbuffered channel):

go func() {
  for i := 0; i < 10; i++ {
      messageCh <- i
    }
    disconnectCh <- struct{}{}
}()

for {
    select {
    case v := <-messageCh:
        fmt.Println(v)
    case <-disconnectCh:
        fmt.Println("disconnection, return")
        return
    }
}

If we run this example multiple times, the result will be random:

0
1
2
disconnection, return

0
disconnection, return

Instead of consuming the 10 messages, we only received a few of them. What’s the reason? It lies in the specification of the select statement with multiple channels (https:// go.dev/ref/spec):

Quote

If one or more of the communications can proceed, a single one that can proceed is chosen via a uniform pseudo-random selection.

Unlike a switch statement, where the first case with a match wins, the select statement selects randomly if multiple options are possible.

This behavior might look odd at first, but there’s a good reason for it: to prevent possible starvation. Suppose the first possible communication chosen is based on the source order. In that case, we may fall into a situation where, for example, we only receive from one channel because of a fast sender. To prevent this, the language designers decided to use a random selection.

When using select with multiple channels, we must remember that if multiple options are possible, the first case in the source order does not automatically win. Instead, Go selects randomly, so there’s no guarantee about which option will be chosen. To overcome this behavior, in the case of a single producer goroutine, we can use either unbuffered channels or a single channel.

Código fonte

Não usar canais de notificação (#65)

TL;DR

Envie notifica√ß√Ķes usando um tipo chan struct{}.

Channels are a mechanism for communicating across goroutines via signaling. A signal can be either with or without data.

Let’s look at a concrete example. We will create a channel that will notify us whenever a certain disconnection occurs. One idea is to handle it as a chan bool:

disconnectCh := make(chan bool)

Now, let’s say we interact with an API that provides us with such a channel. Because it’s a channel of Booleans, we can receive either true or false messages. It’s probably clear what true conveys. But what does false mean? Does it mean we haven’t been disconnected? And in this case, how frequently will we receive such a signal? Does it mean we have reconnected? Should we even expect to receive false? Perhaps we should only expect to receive true messages.

If that’s the case, meaning we don’t need a specific value to convey some information, we need a channel without data. The idiomatic way to handle it is a channel of empty structs: chan struct{}.

N√£o usar canais nulos (#66)

TL;DR

O uso de canais nulos deve fazer parte do seu conjunto de ferramentas de simultaneidade porque permite remover casos de instru√ß√Ķes select, por exemplo.

What should this code do?

var ch chan int
<-ch

ch is a chan int type. The zero value of a channel being nil, ch is nil. The goroutine won’t panic; however, it will block forever.

The principle is the same if we send a message to a nil channel. This goroutine blocks forever:

var ch chan int
ch <- 0

Then what’s the purpose of Go allowing messages to be received from or sent to a nil channel? For example, we can use nil channels to implement an idiomatic way to merge two channels:

func merge(ch1, ch2 <-chan int) <-chan int {
    ch := make(chan int, 1)

    go func() {
        for ch1 != nil || ch2 != nil { // Continue if at least one channel isn’t nil
            select {
            case v, open := <-ch1:
                if !open {
                    ch1 = nil // Assign ch1 to a nil channel once closed
                    break
                }
                ch <- v
            case v, open := <-ch2:
                if !open {
                    ch2 = nil // Assigns ch2 to a nil channel once closed
                    break
                }
                ch <- v
            }
        }
        close(ch)
    }()

    return ch
}

This elegant solution relies on nil channels to somehow remove one case from the select statement.

Let’s keep this idea in mind: nil channels are useful in some conditions and should be part of the Go developer’s toolset when dealing with concurrent code.

Código fonte

Ficar intrigado com o tamanho do canal (#67)

TL;DR

Decida cuidadosamente o tipo de canal correto a ser usado, considerando o problema. Somente canais sem buffer oferecem fortes garantias de sincronização. Para canais em buffer, você deve ter um bom motivo para especificar um tamanho de canal diferente de um.

An unbuffered channel is a channel without any capacity. It can be created by either omitting the size or providing a 0 size:

ch1 := make(chan int)
ch2 := make(chan int, 0)

With an unbuffered channel (sometimes called a synchronous channel), the sender will block until the receiver receives data from the channel.

Conversely, a buffered channel has a capacity, and it must be created with a size greater than or equal to 1:

ch3 := make(chan int, 1)

With a buffered channel, a sender can send messages while the channel isn’t full. Once the channel is full, it will block until a receiver goroutine receives a message:

ch3 := make(chan int, 1)
ch3 <-1 // Non-blocking
ch3 <-2 // Blocking

The first send isn’t blocking, whereas the second one is, as the channel is full at this stage.

What's the main difference between unbuffered and buffered channels:

  • An unbuffered channel enables synchronization. We have the guarantee that two goroutines will be in a known state: one receiving and another sending a message.
  • A buffered channel doesn‚Äôt provide any strong synchronization. Indeed, a producer goroutine can send a message and then continue its execution if the channel isn‚Äôt full. The only guarantee is that a goroutine won‚Äôt receive a message before it is sent. But this is only a guarantee because of causality (you don‚Äôt drink your coffee before you prepare it).

If we need a buffered channel, what size should we provide?

The default value we should use for buffered channels is its minimum: 1. So, we may approach the problem from this standpoint: is there any good reason not to use a value of 1? Here’s a list of possible cases where we should use another size:

  • While using a worker pooling-like pattern, meaning spinning a fixed number of goroutines that need to send data to a shared channel. In that case, we can tie the channel size to the number of goroutines created.
  • When using channels for rate-limiting problems. For example, if we need to enforce resource utilization by bounding the number of requests, we should set up the channel size according to the limit.

If we are outside of these cases, using a different channel size should be done cautiously. Let’s bear in mind that deciding about an accurate queue size isn’t an easy problem:

Martin Thompson

Queues are typically always close to full or close to empty due to the differences in pace between consumers and producers. They very rarely operate in a balanced middle ground where the rate of production and consumption is evenly matched.

Esquecendo os possíveis efeitos colaterais da formatação de strings (#68)

TL;DR

Estar ciente de que a formata√ß√£o de strings pode levar √† chamada de fun√ß√Ķes existentes significa estar atento a poss√≠veis impasses e outras disputas de dados.

It’s pretty easy to forget the potential side effects of string formatting while working in a concurrent application.

etcd data race

github.com/etcd-io/etcd/pull/7816 shows an example of an issue where a map's key was formatted based on a mutable values from a context.

Deadlock

Can you see what the problem is in this code with a Customer struct exposing an UpdateAge method and implementing the fmt.Stringer interface?

type Customer struct {
    mutex sync.RWMutex // Uses a sync.RWMutex to protect concurrent accesses
    id    string
    age   int
}

func (c *Customer) UpdateAge(age int) error {
    c.mutex.Lock() // Locks and defers unlock as we update Customer
    defer c.mutex.Unlock()

    if age < 0 { // Returns an error if age is negative
        return fmt.Errorf("age should be positive for customer %v", c)
    }

    c.age = age
    return nil
}

func (c *Customer) String() string {
    c.mutex.RLock() // Locks and defers unlock as we read Customer
    defer c.mutex.RUnlock()
    return fmt.Sprintf("id %s, age %d", c.id, c.age)
}

The problem here may not be straightforward. If the provided age is negative, we return an error. Because the error is formatted, using the %s directive on the receiver, it will call the String method to format Customer. But because UpdateAge already acquires the mutex lock, the String method won’t be able to acquire it. Hence, this leads to a deadlock situation. If all goroutines are also asleep, it leads to a panic.

One possible solution is to restrict the scope of the mutex lock:

func (c *Customer) UpdateAge(age int) error {
    if age < 0 {
        return fmt.Errorf("age should be positive for customer %v", c)
    }

    c.mutex.Lock() <1>
    defer c.mutex.Unlock()

    c.age = age
    return nil
}

Yet, such an approach isn't always possible. In these conditions, we have to be extremely careful with string formatting.

Another approach is to access the id field directly:

func (c *Customer) UpdateAge(age int) error {
    c.mutex.Lock()
    defer c.mutex.Unlock()

    if age < 0 {
        return fmt.Errorf("age should be positive for customer id %s", c.id)
    }

    c.age = age
    return nil
}

In concurrent applications, we should remain cautious about the possible side effects of string formatting.

Código fonte

Criando corridas de dados com acréscimo (#69)

TL;DR

As chamadas append nem sempre s√£o isentas de disputa de dados; portanto, n√£o deve ser usado simultaneamente em uma slice compartilhada.

Should adding an element to a slice using append is data-race-free? Spoiler: it depends.

Do you believe this example has a data race?

s := make([]int, 1)

go func() { // In a new goroutine, appends a new element on s
    s1 := append(s, 1)
    fmt.Println(s1)
}()

go func() { // Same
    s2 := append(s, 1)
    fmt.Println(s2)
}()

The answer is no.

In this example, we create a slice with make([]int, 1). The code creates a one-length, one-capacity slice. Thus, because the slice is full, using append in each goroutine returns a slice backed by a new array. It doesn’t mutate the existing array; hence, it doesn’t lead to a data race.

Now, let’s run the same example with a slight change in how we initialize s. Instead of creating a slice with a length of 1, we create it with a length of 0 but a capacity of 1. How about this new example? Does it contain a data race?

s := make([]int, 0, 1)

go func() { 
    s1 := append(s, 1)
    fmt.Println(s1)
}()

go func() {
    s2 := append(s, 1)
    fmt.Println(s2)
}()

The answer is yes. We create a slice with make([]int, 0, 1). Therefore, the array isn’t full. Both goroutines attempt to update the same index of the backing array (index 1), which is a data race.

How can we prevent the data race if we want both goroutines to work on a slice containing the initial elements of s plus an extra element? One solution is to create a copy of s.

We should remember that using append on a shared slice in concurrent applications can lead to a data race. Hence, it should be avoided.

Código fonte

Usando mutexes imprecisamente com slices e maps (#70)

TL;DR

Lembrar que slices e maps s√£o ponteiros pode evitar corridas comuns de dados.

Let's implement a Cache struct used to handle caching for customer balances. This struct will contain a map of balances per customer ID and a mutex to protect concurrent accesses:

type Cache struct {
    mu       sync.RWMutex
    balances map[string]float64
}

Next, we add an AddBalance method that mutates the balances map. The mutation is done in a critical section (within a mutex lock and a mutex unlock):

func (c *Cache) AddBalance(id string, balance float64) {
    c.mu.Lock()
    c.balances[id] = balance
    c.mu.Unlock()
}

Meanwhile, we have to implement a method to calculate the average balance for all the customers. One idea is to handle a minimal critical section this way:

func (c *Cache) AverageBalance() float64 {
    c.mu.RLock()
    balances := c.balances // Creates a copy of the balances map
    c.mu.RUnlock()

    sum := 0.
    for _, balance := range balances { // Iterates over the copy, outside of the critical section
        sum += balance
    }
    return sum / float64(len(balances))
}

What's the problem with this code?

If we run a test using the -race flag with two concurrent goroutines, one calling AddBalance (hence mutating balances) and another calling AverageBalance, a data race occurs. What’s the problem here?

Internally, a map is a runtime.hmap struct containing mostly metadata (for example, a counter) and a pointer referencing data buckets. So, balances := c.balances doesn’t copy the actual data. Therefore, the two goroutines perform operations on the same data set, and one mutates it. Hence, it's a data race.

One possible solution is to protect the whole AverageBalance function:

func (c *Cache) AverageBalance() float64 {
    c.mu.RLock()
    defer c.mu.RUnlock() // Unlocks when the function returns

    sum := 0.
    for _, balance := range c.balances {
        sum += balance
    }
    return sum / float64(len(c.balances))
}

Another option, if the iteration operation isn’t lightweight, is to work on an actual copy of the data and protect only the copy:

func (c *Cache) AverageBalance() float64 {
    c.mu.RLock()
    m := make(map[string]float64, len(c.balances)) // Copies the map
    for k, v := range c.balances {
        m[k] = v
    }
    c.mu.RUnlock()

    sum := 0.
    for _, balance := range m {
        sum += balance
    }
    return sum / float64(len(m))
}

Once we have made a deep copy, we release the mutex. The iterations are done on the copy outside of the critical section.

In summary, we have to be careful with the boundaries of a mutex lock. In this section, we have seen why assigning an existing map (or an existing slice) to a map isn’t enough to protect against data races. The new variable, whether a map or a slice, is backed by the same data set. There are two leading solutions to prevent this: protect the whole function, or work on a copy of the actual data. In all cases, let’s be cautious when designing critical sections and make sure the boundaries are accurately defined.

Código fonte

Uso indevido sync.WaitGroup (#71)

TL;DR

Para usar com precisão sync.WaitGroup, chame o método Add antes de ativar goroutines.

Código fonte

Esquecendo sync.Cond (#72)

TL;DR

Voc√™ pode enviar notifica√ß√Ķes repetidas para v√°rios goroutines com sync.Cond.

Código fonte

N√£o usando errgroup (#73)

TL;DR

Você pode sincronizar um grupo de goroutines e lidar com erros e contextos com o pacote errgroup.

Código fonte

Copiando um tipo sync (#74)

TL;DR

Tipos sync n√£o devem ser copiados.

Código fonte

Standard Library

Fornecendo uma duração de tempo errada (#75)

TL;DR

Seja cauteloso com fun√ß√Ķes que aceitam um arquivo time.Duration. Mesmo que a passagem de um n√ļmero inteiro seja permitida, tente usar a API time para evitar qualquer poss√≠vel confus√£o.

Many common functions in the standard library accept a time.Duration, which is an alias for the int64 type. However, one time.Duration unit represents one nanosecond, instead of one millisecond, as commonly seen in other programming languages. As a result, passing numeric types instead of using the time.Duration API can lead to unexpected behavior.

A developer with experience in other languages might assume that the following code creates a new time.Ticker that delivers ticks every second, given the value 1000:

ticker := time.NewTicker(1000)
for {
    select {
    case <-ticker.C:
        // Do something
    }
}

However, because 1,000 time.Duration units = 1,000 nanoseconds, ticks are delivered every 1,000 nanoseconds = 1 microsecond, not every second as assumed.

We should always use the time.Duration API to avoid confusion and unexpected behavior:

ticker = time.NewTicker(time.Microsecond)
// Or
ticker = time.NewTicker(1000 * time.Nanosecond)

Código fonte

time.After e vazamentos de memória (#76)

TL;DR

Evitar chamadas para fun√ß√Ķes time.After repetidas (como loops ou manipuladores HTTP) pode evitar pico de consumo de mem√≥ria. Os recursos criados por time.After s√£o liberados somente quando o cron√īmetro expira.

Developers often use time.After in loops or HTTP handlers repeatedly to implement the timing function. But it can lead to unintended peak memory consumption due to the delayed release of resources, just like the following code:

func consumer(ch <-chan Event) {
    for {
        select {
        case event := <-ch:
            handle(event)
        case <-time.After(time.Hour):
            log.Println("warning: no messages received")
        }
    }
}

The source code of the function time.After is as follows:

func After(d Duration) <-chan Time {
    return NewTimer(d).C
}

As we see, it returns receive-only channel.

When time.After is used in a loop or repeated context, a new channel is created in each iteration. If these channels are not properly closed or if their associated timers are not stopped, they can accumulate and consume memory. The resources associated with each timer and channel are only released when the timer expires or the channel is closed.

To avoid this happening, We can use context's timeout setting instead of time.After, like below:

func consumer(ch <-chan Event) {
    for {
        ctx, cancel := context.WithTimeout(context.Background(), time.Hour)
        select {
        case event := <-ch:
            cancel()
            handle(event)
        case <-ctx.Done():
            log.Println("warning: no messages received")
        }
    }
}

We can also use time.NewTimer like so:

func consumer(ch <-chan Event) {
    timerDuration := 1 * time.Hour
    timer := time.NewTimer(timerDuration)

    for {
        timer.Reset(timerDuration)
        select {
        case event := <-ch:
            handle(event)
        case <-timer.C:
            log.Println("warning: no messages received")
        }
    }
}

Código fonte

Lidando com erros comuns JSON (#77)

  • Comportamento inesperado devido √† incorpora√ß√£o de tipo

Tenha cuidado ao usar campos incorporados em estruturas Go. Fazer isso pode levar a bugs sorrateiros, como um campo time.Time incorporado que implementa a interface json.Marshaler, substituindo assim o comportamento de empacotamento padr√£o.

Código fonte

  • JSON e o rel√≥gio monot√īnico

Ao comparar duas estruturas time.Time, lembre-se de que time.Time cont√©m um rel√≥gio de parede e um rel√≥gio monot√īnico, e a compara√ß√£o usando o operador == √© feita em ambos os rel√≥gios.

Código fonte

  • Map de any

Para evitar suposi√ß√Ķes erradas ao fornecer um map ao desempacotar (unmarshaling) dados JSON, lembre-se de que os valores num√©ricos s√£o convertidos para float64 por padr√£o.

Código fonte

Erros comuns de SQL (#78)

  • Esquecer sql.Open n√£o necessariamente estabelece conex√Ķes com um banco de dados

Esquecer sql.Open n√£o necessariamente estabelece conex√Ķes com um banco de dados Chame o m√©todo Ping ou PingContext se precisar testar sua configura√ß√£o e garantir que um banco de dados esteja acess√≠vel.

Código fonte

  • Esquecendo o pool de conex√Ķes

Configure os par√Ęmetros de conex√£o do banco de dados para aplicativos de n√≠vel de produ√ß√£o.

  • N√£o usar declara√ß√Ķes preparadas

O uso de instru√ß√Ķes preparadas em SQL torna as consultas mais eficientes e seguras.

Código fonte

  • Tratamento incorreto de valores nulos

Lide com colunas anul√°veis ‚Äč‚Äčem tabelas usando ponteiros ou tipos sql.NullXXX.

Código fonte

  • N√£o tratando de erros de itera√ß√£o de linhas

Chame o m√©todo Err de sql.Rows itera√ß√Ķes posteriores √† linha para garantir que voc√™ n√£o perdeu nenhum erro ao preparar a pr√≥xima linha.

Código fonte

Não fechando recursos transitórios (body HTTP, sql.Rows e os.File) (#79)

TL;DR

Eventualmente feche todas as estruturas implementadas io.Closer para evitar possíveis vazamentos.

Código fonte

Esquecendo a instrução return após responder a uma solicitação HTTP (#80)

TL;DR

Para evitar comportamentos inesperados nas implementa√ß√Ķes do manipulador HTTP, certifique-se de n√£o perder a instru√ß√£o return se quiser que um manipulador pare ap√≥s http.Error.

Consider the following HTTP handler that handles an error from foo using http.Error:

func handler(w http.ResponseWriter, req *http.Request) {
    err := foo(req)
    if err != nil {
        http.Error(w, "foo", http.StatusInternalServerError)
    }

    _, _ = w.Write([]byte("all good"))
    w.WriteHeader(http.StatusCreated)
}

If we run this code and err != nil, the HTTP response would be:

foo
all good

The response contains both the error and success messages, and also the first HTTP status code, 500. There would also be a warning log indicating that we attempted to write the status code multiple times:

2023/10/10 16:45:33 http: superfluous response.WriteHeader call from main.handler (main.go:20)

The mistake in this code is that http.Error does not stop the handler's execution, which means the success message and status code get written in addition to the error. Beyond an incorrect response, failing to return after writing an error can lead to the unwanted execution of code and unexpected side-effects. The following code adds the return statement following the http.Error and exhibits the desired behavior when ran:

func handler(w http.ResponseWriter, req *http.Request) {
    err := foo(req)
    if err != nil {
        http.Error(w, "foo", http.StatusInternalServerError)
        return // Adds the return statement
    }

    _, _ = w.Write([]byte("all good"))
    w.WriteHeader(http.StatusCreated)
}

Código fonte

Usando o cliente e servidor HTTP padr√£o (#81)

TL;DR

Para aplicativos de n√≠vel de produ√ß√£o, n√£o use as implementa√ß√Ķes de cliente e servidor HTTP padr√£o. Essas implementa√ß√Ķes n√£o possuem tempos limite e comportamentos que deveriam ser obrigat√≥rios na produ√ß√£o.

Código fonte

Teste

N√£o categorizar testes (tags de constru√ß√£o, vari√°veis ‚Äč‚Äčde ambiente e modo abreviado) (#82)

TL;DR

Categorizar testes usando sinalizadores de constru√ß√£o, vari√°veis ‚Äč‚Äčde ambiente ou modo curto torna o processo de teste mais eficiente. Voc√™ pode criar categorias de teste usando sinalizadores de constru√ß√£o ou vari√°veis ‚Äč‚Äčde ambiente (por exemplo, testes de unidade versus testes de integra√ß√£o) e diferenciar testes curtos de testes de longa dura√ß√£o para decidir quais tipos de testes executar.

Código fonte

N√£o habilitando a bandeira de corrida (#83)

TL;DR

A ativa√ß√£o do sinalizador -race √© altamente recomendada ao escrever aplicativos simult√Ęneos. Isso permite que voc√™ detecte poss√≠veis corridas de dados que podem levar a bugs de software.

In Go, the race detector isn’t a static analysis tool used during compilation; instead, it’s a tool to find data races that occur at runtime. To enable it, we have to enable the -race flag while compiling or running a test. For example:

go test -race ./...

Once the race detector is enabled, the compiler instruments the code to detect data races. Instrumentation refers to a compiler adding extra instructions: here, tracking all memory accesses and recording when and how they occur.

Enabling the race detector adds an overhead in terms of memory and execution time; hence, it's generally recommended to enable it only during local testing or continuous integration, not production.

If a race is detected, Go raises a warning. For example:

package main

import (
    "fmt"
)

func main() {
    i := 0
    go func() { i++ }()
    fmt.Println(i)
}

Runnig this code with the -race logs the following warning:

==================
WARNING: DATA RACE
Write at 0x00c000026078 by goroutine 7: # (1)
  main.main.func1()
      /tmp/app/main.go:9 +0x4e

Previous read at 0x00c000026078 by main goroutine: # (2)
  main.main()
      /tmp/app/main.go:10 +0x88

Goroutine 7 (running) created at: # (3)
  main.main()
      /tmp/app/main.go:9 +0x7a
==================
  1. Indicates that goroutine 7 was writing
  2. Indicates that the main goroutine was reading
  3. Indicates when the goroutine 7 was created

Let’s make sure we are comfortable reading these messages. Go always logs the following:

  • The concurrent goroutines that are incriminated: here, the main goroutine and goroutine 7.
  • Where accesses occur in the code: in this case, lines 9 and 10.
  • When these goroutines were created: goroutine 7 was created in main().

In addition, if a specific file contains tests that lead to data races, we can exclude it from race detection using the !race build tag:

//go:build !race

package main

import (
    "testing"
)

func TestFoo(t *testing.T) {
    // ...
}

Não usar modos de execução de teste (paralelo e aleatório) (#84)

TL;DR

Usar o sinalizador -parallel √© uma forma eficiente de acelerar testes, especialmente os de longa dura√ß√£o. Use o sinalizador -shuffle para ajudar a garantir que um conjunto de testes n√£o se baseie em suposi√ß√Ķes erradas que possam ocultar bugs.

N√£o usar testes baseados em tabela (#85)

TL;DR

Os testes baseados em tabelas s√£o uma maneira eficiente de agrupar um conjunto de testes semelhantes para evitar a duplica√ß√£o de c√≥digo e facilitar o manuseio de atualiza√ß√Ķes futuras.

Código fonte

Dormindo em testes unit√°rios (#86)

TL;DR

Evite interrup√ß√Ķes usando a sincroniza√ß√£o para tornar o teste menos inst√°vel e mais robusto. Se a sincroniza√ß√£o n√£o for poss√≠vel, considere uma abordagem de nova tentativa.

Código fonte

N√£o lidar com a API de tempo de forma eficiente (#87)

TL;DR

Entender como lidar com fun√ß√Ķes usando a API time √© outra maneira de tornar um teste menos complicado. Voc√™ pode usar t√©cnicas padr√£o, como lidar com o tempo como parte de uma depend√™ncia oculta ou solicitar que os clientes o forne√ßam.

Código fonte

N√£o usar pacotes de utilit√°rios de teste ( httptest e iotest) (#88)

  • O pacote httptest √© √ļtil para lidar com aplicativos HTTP. Ele fornece um conjunto de utilit√°rios para testar clientes e servidores.

Código fonte

  • O pacote iotest ajuda a escrever io.Reader e testar se um aplicativo √© tolerante a erros.

Código fonte

Escrevendo benchmarks imprecisos (#89)

TL;DR

Regarding benchmarks:

  • Use m√©todos de tempo para preservar a precis√£o de um benchmark.
  • Aumentar o tempo de teste ou usar ferramentas como o benchstat pode ser √ļtil ao lidar com micro-benchmarks.
  • Tenha cuidado com os resultados de um micro-benchmark se o sistema que executa o aplicativo for diferente daquele que executa o micro-benchmark.
  • Certifique-se de que a fun√ß√£o em teste cause um efeito colateral, para evitar que as otimiza√ß√Ķes do compilador enganem voc√™ sobre os resultados do benchmark.
  • Para evitar o efeito observador, force um benchmark a recriar os dados usados ‚Äč‚Äčpor uma fun√ß√£o vinculada √† CPU.

Leia a seção completa aqui.

Código fonte

N√£o explorando todos os recursos de teste do Go (#90)

  • Cobertura de c√≥digo

Use a cobertura de código com o sinalizador -coverprofile para ver rapidamente qual parte do código precisa de mais atenção.

  • Testando de um pacote diferente

Coloque os testes unit√°rios em um pacote diferente para impor testes de escrita que se concentrem em um comportamento exposto, n√£o em internos.

Código fonte

  • Fun√ß√Ķes utilit√°rias

O tratamento de erros usando a variável *testing.T em vez do clássico if err != nil torna o código mais curto e fácil de ler.

Código fonte

  • Configura√ß√£o e desmontagem

Voc√™ pode usar fun√ß√Ķes de setup e teardown para configurar um ambiente complexo, como no caso de testes de integra√ß√£o.

Código fonte

N√£o usar fuzzing (erro da comunidade)

TL;DR

Fuzzing √© uma estrat√©gia eficiente para detectar entradas aleat√≥rias, inesperadas ou malformadas em fun√ß√Ķes e m√©todos complexos, a fim de descobrir vulnerabilidades, bugs ou at√© mesmo travamentos potenciais.

Credits: @jeromedoucet

Otimiza√ß√Ķes

N√£o entendendo os caches da CPU (#91)

  • Arquitetura da CPU

Compreender como usar caches de CPU é importante para otimizar aplicativos vinculados à CPU porque o cache L1 é cerca de 50 a 100 vezes mais rápido que a memória principal.

  • Linha de cache

Estar consciente do conceito de linha de cache é fundamental para entender como organizar dados em aplicativos com uso intensivo de dados. Uma CPU não busca memória palavra por palavra; em vez disso, geralmente copia um bloco de memória para uma linha de cache de 64 bytes. Para aproveitar ao máximo cada linha de cache individual, imponha a localidade espacial.

Código fonte

  • Slice de estruturas vs. estrutura de slices

Código fonte

  • Previsibilidade

Tornar o c√≥digo previs√≠vel para a CPU tamb√©m pode ser uma forma eficiente de otimizar certas fun√ß√Ķes. Por exemplo, uma passada unit√°ria ou constante √© previs√≠vel para a CPU, mas uma passada n√£o unit√°ria (por exemplo, uma lista vinculada) n√£o √© previs√≠vel.

Código fonte

  • Pol√≠tica de posicionamento de cache

Para evitar um avanço crítico e, portanto, utilizar apenas uma pequena parte do cache, esteja ciente de que os caches são particionados.

Escrevendo c√≥digo simult√Ęneo que leva a compartilhamento falso (#92)

TL;DR

Saber que n√≠veis mais baixos de caches de CPU n√£o s√£o compartilhados entre todos os n√ļcleos ajuda a evitar padr√Ķes que degradam o desempenho, como compartilhamento falso ao escrever c√≥digo de simultaneidade. Compartilhar mem√≥ria √© uma ilus√£o.

Leia a seção completa aqui.

Código fonte

Não levando em consideração o paralelismo no nível de instrução (#93)

TL;DR

Use o ILP para otimizar partes espec√≠ficas do seu c√≥digo para permitir que uma CPU execute tantas instru√ß√Ķes paralelas quanto poss√≠vel. Identificar perigos nos dados √© uma das etapas principais.

Código fonte

N√£o estar ciente do alinhamento dos dados (#94)

TL;DR

Você pode evitar erros comuns lembrando que no Go os tipos básicos são alinhados com seu próprio tamanho. Por exemplo, tenha em mente que reorganizar os campos de uma estrutura por tamanho em ordem decrescente pode levar a estruturas mais compactas (menos alocação de memória e potencialmente uma melhor localidade espacial).

Código fonte

N√£o entendendo stack vs. heap (#95)

TL;DR

Compreender as diferen√ßas fundamentais entre heap e pilha tamb√©m deve fazer parte do seu conhecimento b√°sico ao otimizar um aplicativo Go. As aloca√ß√Ķes de pilha s√£o quase gratuitas, enquanto as aloca√ß√Ķes de heap s√£o mais lentas e dependem do GC para limpar a mem√≥ria.

Código fonte

N√£o saber como reduzir aloca√ß√Ķes (mudan√ßa de API, otimiza√ß√Ķes de compilador e sync.Pool) (#96)

TL;DR

A redu√ß√£o das aloca√ß√Ķes tamb√©m √© um aspecto essencial da otimiza√ß√£o de um aplicativo Go. Isso pode ser feito de diferentes maneiras, como projetar a API cuidadosamente para evitar compartilhamento, compreender as otimiza√ß√Ķes comuns do compilador Go e usar sync.Pool.

Código fonte

N√£o dependendo do inlining (#97)

TL;DR

Use a técnica de inlining de caminho rápido para reduzir com eficiência o tempo amortizado para chamar uma função.

Não usar ferramentas de diagnóstico Go (#98)

TL;DR

Confie na criação de perfil e no rastreador de execução para entender o desempenho de um aplicativo e as partes a serem otimizadas.

Leia a seção completa aqui.

N√£o entendendo como funciona o GC (#99)

TL;DR

Compreender como ajustar o GC pode levar a vários benefícios, como lidar com aumentos repentinos de carga com mais eficiência.

Não entendendo os impactos da execução do Go no Docker e Kubernetes (#100)

TL;DR

Para ajudar a evitar a limitação da CPU quando implantado no Docker e no Kubernetes, lembre-se de que Go não reconhece CFS.

By default, GOMAXPROCS is set to the number of OS-apparent logical CPU cores.

When running some Go code inside Docker and Kubernetes, we must know that Go isn't CFS-aware (github.com/golang/go/issues/33803). Therefore, GOMAXPROCS isn't automatically set to the value of spec.containers.resources.limits.cpu (see Kubernetes Resource Management for Pods and Containers); instead, it's set to the number of logical cores on the host machine. The main implication is that it can lead to an increased tail latency in some specific situations.

One solution is to rely on uber-go/automaxprocs that automatically set GOMAXPROCS to match the Linux container CPU quota.

Community

Thanks to all the contributors:

Description of the image

Comments