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
- 2. Não reatribuir o resultado de append
- 3. Valores zero que escondem chaves em falta num map
- 4. Usar %v em vez de %w ao encapsular erros
- 5. Recetores por valor que descartam alterações em silêncio
- 6. Variáveis sombreadas que devolvem erros nil
- 7. Campos não exportados de uma struct que desaparecem no JSON
- 8. Usar defer dentro de ciclos
- 9. Ignorar erros com o identificador em branco
- 10. Deadlocks causados por channels sem buffer
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.gogopackage 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.gogopackage 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.gogopackage 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.gogopackage 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.gogopackage 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.gogofunc 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.gogopackage 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.gogofunc 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.gogofunc 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.gogodata, _ := 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.gogopackage 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.gogopackage 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.
