Voltar ao blog

10 erros comuns em Go (Golang) que deves evitar

Descobre os 10 erros mais comuns ao programar em Go (Golang) e como evitá-los: bytes em strings, append em slices, valores zero em maps, encapsulamento de erros, recetores por valor, defer em ciclos, channels sem buffer e muito mais.

10 erros comuns em Go (Golang) que deves evitar

Go, muitas vezes pesquisado como Golang, é uma linguagem pequena, e por isso é fácil subestimá-la. Programadores com anos de Go continuam a cair no mesmo punhado de armadilhas. A maioria destes bugs compila sem problemas. O programa simplesmente dá a resposta errada, deixa recursos por libertar ou entra em deadlock em produção.

Seguem-se os 10 mais comuns. Cada um tem um exemplo em Golang que podes executar e a correção idiomática.

Índice


1. Tratar strings como caracteres em vez de bytes

Uma string em Go é um slice de bytes só de leitura, não de caracteres. len(s) devolve o número de bytes, e indexar com s[i] devolve um byte. Com texto ASCII, não há problema. Falha assim que um carácter ocupa mais do que um byte.

example.gogo
package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {
    word := "kött"
    fmt.Println("len:", len(word))
    fmt.Println("runes:", utf8.RuneCountInString(word))
}

A palavra sueca para carne tem quatro caracteres, mas cinco bytes. Cortar por índice de byte pode partir uma rune ao meio e produzir UTF-8 inválido.

Solução: Usa utf8.RuneCountInString para contar caracteres. Ao iterar, for i, r := range s dá-te runes. Para cortar uma string, converte-a primeiro para []rune.

2. Não reatribuir o resultado de append

append não atualiza o slice que lhe passas. Quando a capacidade se esgota, aloca um novo array subjacente e devolve um slice que aponta para ele. Se ignorares o valor devolvido, os elementos acrescentados perdem-se.

example.gogo
package main

import "fmt"

func main() {
    nums := []int{1, 2, 3}
    appendWrong(nums)
    fmt.Println("wrong:", nums)

    nums = appendRight(nums)
    fmt.Println("right:", nums)
}

func appendWrong(s []int) {
    _ = append(s, 4)
}

func appendRight(s []int) []int {
    return append(s, 4)
}

Em Go, uma chamada isolada a append(s, 4) é mesmo um erro de compilação, porque o valor devolvido tem de ser usado. O _ = em appendWrong só lá está para passar pelo compilador. Comporta-se da mesma forma que chamar append sem nunca voltar a atribuir o resultado.

Solução: Reatribui sempre com s = append(s, x). Se uma função auxiliar faz crescer um slice, faz com que devolva o novo slice e reatribui-o no local da chamada.

3. Valores zero que escondem chaves em falta num map

Ler uma chave inexistente de um map não devolve um erro. Devolve o valor zero do tipo de valor do map. Num map[string]int isso é 0, que não consegues distinguir de uma chave definida como zero de propósito.

example.gogo
package main

import "fmt"

func main() {
    scores := map[string]int{"alice": 0}

    if v := scores["alice"]; v == 0 {
        fmt.Println("alice looks the same as missing users")
    }

    if v, ok := scores["bob"]; ok {
        fmt.Println("bob:", v)
    } else {
        fmt.Println("bob is missing")
    }
}

Solução: Usa a expressão idiomática comma-ok, v, ok := m[key]. O booleano diz-te se a chave existia.

4. Usar %v em vez de %w ao encapsular erros

fmt.Errorf com %v formata o erro original numa nova string e deita fora o valor do erro. Com %w, o novo erro encapsula o original, pelo que errors.Is e errors.As continuam a conseguir encontrá-lo.

example.gogo
package main

import (
    "errors"
    "fmt"
    "os"
)

func main() {
    _, err := os.Open("does-not-exist.txt")
    wrapped := fmt.Errorf("reading config: %w", err)

    fmt.Println(wrapped)
    fmt.Println("is not-exist:", errors.Is(wrapped, os.ErrNotExist))
}

Solução: Encapsula com %w sempre que quem chama a função possa precisar de verificar o erro subjacente. Usa %v apenas quando o erro original for um detalhe de implementação de que não queres que quem chama dependa.

5. Recetores por valor que descartam alterações em silêncio

Um método com recetor por valor recebe uma cópia da struct. O que quer que altere nessa cópia desaparece quando o método termina.

example.gogo
package main

import "fmt"

type Counter struct {
    n int
}

func (c Counter) IncValue() {
    c.n++
}

func (c *Counter) IncPointer() {
    c.n++
}

func main() {
    c := Counter{}
    c.IncValue()
    fmt.Println("after value receiver:", c.n)

    c.IncPointer()
    fmt.Println("after pointer receiver:", c.n)
}

Solução: Usa um recetor por ponteiro quando o método altera o recetor ou quando a struct é grande o suficiente para que copiá-la seja um desperdício. Se alguns métodos de um tipo precisam de recetor por ponteiro, normalmente fica mais limpo dar um a todos.

6. Variáveis sombreadas que devolvem erros nil

Uma declaração curta de variável (:=) dentro de um if cria uma nova variável que só existe nesse bloco. Se ela sombrear sem querer um err exterior, o exterior continua a ser nil e a função reporta sucesso depois de algo ter falhado.

example.gogo
func loadUser(id string) (*User, error) {
    var err error
    user, err := fetch(id)
    if err != nil {
        return nil, err
    }

    if err := validate(user); err != nil {
        log.Println(err)
        // the outer err is still nil here
    }

    return user, err
}

Solução: Atribui à variável exterior com = em vez de :=, ou retorna logo dentro do bloco if. Ativa go vet -shadow ou o linter govet para apanhar estes casos antes da revisão.

7. Campos não exportados de uma struct que desaparecem no JSON

encoding/json só vê campos exportados. Um campo em minúsculas é invisível para o marshaller, por isso fica de fora da saída sem qualquer erro.

example.gogo
package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    Name  string `json:"name"`
    email string
    Age   int    `json:"age,omitempty"`
}

func main() {
    u := User{Name: "Ada", email: "[email protected]", Age: 36}
    data, _ := json.Marshal(u)
    fmt.Println(string(data))
}

A saída não tem nenhuma chave email, e nada te avisa disso.

Solução: Escreve com maiúscula inicial os campos que queres serializar e usa tags de struct para definir o nome da chave JSON. Acrescenta omitempty aos campos opcionais.

8. Usar defer dentro de ciclos

defer é executado quando a função envolvente retorna, não no fim de cada iteração do ciclo. Se um ciclo abre mil ficheiros e faz defer de file.Close() para cada um, os mil descritores de ficheiro ficam abertos até a função terminar.

example.gogo
func processAll(paths []string) error {
    for _, p := range paths {
        f, err := os.Open(p)
        if err != nil {
            return err
        }
        defer f.Close() // all closes pile up until processAll returns
        // ... read file ...
    }
    return nil
}

Solução: Passa o trabalho de cada iteração para uma função auxiliar, para que o defer seja executado no fim de cada chamada.

example.gogo
func processAll(paths []string) error {
    for _, p := range paths {
        if err := processOne(p); err != nil {
            return err
        }
    }
    return nil
}

func processOne(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    defer f.Close()
    // ... read file ...
    return nil
}

9. Ignorar erros com o identificador em branco

result, _ := doThing() deita fora qualquer falha que a função reporte. O programa segue em frente com um valor zero ou um estado a meio, e quando finalmente rebenta, é num sítio longe da causa real.

example.gogo
data, _ := os.ReadFile("config.json")
_ = json.Unmarshal(data, &cfg)

Se o ficheiro não existir, data é nil, Unmarshal devolve um erro que ninguém lê e cfg fica com o valor zero. Só descobres mais tarde, quando a aplicação age como se não houvesse configuração nenhuma.

Solução: Trata o erro ou devolve-o a quem chamou a função. Só usa _ quando o erro não pode mesmo ter importância, e deixa um comentário curto a explicar porquê.

10. Deadlocks causados por channels sem buffer

Num channel sem buffer, um envio só se conclui quando um recetor recebe o valor no mesmo instante. Se nada chegar a receber, o envio bloqueia para sempre.

example.gogo
package main

import "fmt"

func main() {
    ch := make(chan int) // unbuffered
    ch <- 42             // deadlock: no goroutine is receiving
    fmt.Println(<-ch)
}

O envio fica à espera de um recetor, mas a receção está na linha seguinte e nunca chega a ser executada. O runtime vê que todas as goroutines estão presas e termina com fatal error: all goroutines are asleep - deadlock!.

Solução: Garante que há uma goroutine pronta a receber antes de enviares, ou usa um channel com buffer quando o produtor e o consumidor trabalham a velocidades diferentes. Para um único resultado, um channel com buffer de 1 é uma escolha segura por omissão.

example.gogo
package main

import "fmt"

func main() {
    ch := make(chan int, 1) // buffered
    ch <- 42
    fmt.Println(<-ch)
}

Continua a avançar

Conhecer estas dez armadilhas de Golang vai poupar-te muita depuração. O resto aprende-se a construir projetos reais e a esbarrar em casos limite com um depurador aberto. Se queres prática estruturada, o percurso de Go do LevelUpGo aborda estes padrões com exercícios que podes executar e feedback automático sobre o teu código.

Escreve Go como um engenheiro sénior

Lições interativas no navegador. As primeiras são grátis.

Experimenta uma lição grátisOu cria uma conta gratuita