
Oi Habr. Meu nome é Sergey Rudachenko, sou especialista técnico na Roistat. Nos últimos dois anos, nossa equipe traduziu várias partes do projeto em microsserviços on Go. Eles são desenvolvidos por várias equipes, portanto, precisamos definir uma barra de qualidade de código rígido. Para fazer isso, usamos várias ferramentas; neste artigo, focaremos em uma delas - na análise estática.
A análise estática é o processo de verificar automaticamente o código-fonte usando utilitários especiais. Este artigo abordará seus benefícios, descreverá brevemente as ferramentas populares e fornecerá instruções para implementação. Vale a pena ler se você não encontrou ferramentas semelhantes ou as usou sistematicamente.
Em artigos sobre esse tópico, o termo "linter" é frequentemente encontrado. Para nós, esse é um nome conveniente para ferramentas simples para análise estática. A tarefa do linter é procurar erros simples e design incorreto.
Por que os linters são necessários?
Ao trabalhar em equipe, você provavelmente está realizando análises de código. Erros ignorados na revisão são possíveis erros. Perdeu um error
não tratado - não receba uma mensagem informativa e você procurará o problema cegamente. Erro no lançamento do tipo ou virou para o mapa nulo - pior ainda, o binário cairá de pânico.
Os erros descritos acima podem ser adicionados às convenções de código , mas encontrá-los ao ler a solicitação pull não é tão simples, porque o revisor precisará ler o código. Se não houver um compilador em sua cabeça, alguns dos problemas irão para a batalha de qualquer maneira. Além disso, a busca por pequenos erros distrai a verificação da lógica e da arquitetura. À distância, o suporte a esse código se tornará mais caro. Escrevemos em uma linguagem estaticamente tipada, é estranho não usá-la.
Ferramentas populares
A maioria das ferramentas para análise estática usa os pacotes go/ast
e go/parser
. Eles fornecem funções para analisar a sintaxe dos arquivos .go. O encadeamento de execução padrão (por exemplo, para o utilitário golint) é este:
- a lista de arquivos dos pacotes necessários é carregada
parser.ParseFile(...) (*ast.File, error)
é executado para cada arquivo- verifica as regras suportadas para cada arquivo ou pacote
- a verificação passa por cada instrução, por exemplo, assim:
f, err := parser.ParseFile() ast.Walk(func (n *ast.Node) { switch v := node.(type) { case *ast.FuncDecl: if strings.Contains(v.Name, "_") { panic("wrong function naming") } } }, f)
Além do AST, há uma atribuição estática única (SSA). Essa é uma maneira mais complexa de analisar código que funciona com um encadeamento de execução, em vez de construções de sintaxe. Neste artigo, não o consideraremos em detalhes, você pode ler a documentação e dar uma olhada no exemplo do utilitário stackcheck .
Em seguida, apenas os utilitários populares que executam verificações úteis para nós serão considerados.
gofmt
Este é o utilitário padrão do pacote go, que verifica a correspondência de estilos e pode corrigi-lo automaticamente. A conformidade com o estilo é um requisito obrigatório para nós, portanto, a verificação do gofmt está incluída em todos os nossos projetos.
typecheck
O Typecheck verifica a correspondência de tipos no código e suporta o fornecedor (diferente do gotype). Seu lançamento é necessário para verificar a compilação, mas não oferece garantias completas.
vá ao veterinário
O utilitário go vet faz parte do pacote padrão e é recomendado para uso pela equipe Go. Verifica vários erros comuns, por exemplo:
- uso indevido de printf e funções similares
- tags de compilação incorretas
- função de comparação e nada
golint
O Golint é desenvolvido pela equipe Go e valida o código com base nos documentos Effective Go e CodeReviewComments . Infelizmente, não há documentação detalhada, mas o código mostra que o seguinte está verificado:
f.lintPackageComment() f.lintImports() f.lintBlankImports() f.lintExported() f.lintNames() f.lintVarDecls() f.lintElses() f.lintRanges() f.lintErrorf() f.lintErrors() f.lintErrorStrings() f.lintReceiverNames() f.lintIncDec() f.lintErrorReturn() f.lintUnexportedReturn() f.lintTimeNames() f.lintContextKeyTypes() f.lintContextArgs()
staticcheck
Os próprios desenvolvedores apresentam o staticcheck como um veterinário aprimorado. Existem muitas verificações, elas são divididas em grupos:
- uso indevido de bibliotecas padrão
- problemas de multithreading
- problemas com testes
- código inútil
- problemas de desempenho
- projetos duvidosos
gosimple
É especialista em encontrar estruturas que valem a pena simplificar, por exemplo:
Antes ( código fonte golint )
func (f *file) isMain() bool { if ffName.Name == "main" { return true } return false }
Depois
func (f *file) isMain() bool { return ffName.Name == "main" }
A documentação é semelhante à staticcheck e inclui exemplos detalhados.
cheque
Erros retornados por funções não podem ser ignorados. Os motivos estão descritos em detalhes no documento vinculativo Effective Go . Errcheck não ignora o seguinte código:
json.Unmarshal(text, &val) f, _ := os.OpenFile()
gás
Localiza vulnerabilidades no código: acessos codificados, injeções de sql e uso de funções de hash inseguras.
Exemplos de erros:
difamado
No Go, a ordem dos campos nas estruturas afeta o consumo de memória. Maligno encontra classificação não ideal. Com esta ordem de campos:
struct { a bool b string c bool }
A estrutura ocupará 32 bits na memória devido à adição de bits vazios após os campos a e c.

Se alterarmos a classificação e colocarmos dois campos bool juntos, a estrutura terá apenas 24 bits:

Imagem original no stackoverflow
goconst
Variáveis mágicas no código não refletem o significado e complicam a leitura. O Goconst encontra literais e números que aparecem no código 2 vezes ou mais. Observe que muitas vezes até um único uso pode ser um erro.
gocyclo
Consideramos a complexidade ciclomática do código uma métrica importante. Gocycle mostra complexidade para cada função. Somente funções que excedem o valor especificado podem ser exibidas.
gocyclo -over 7 package/name
Escolhemos um valor limite de 7 para nós mesmos, porque não encontramos um código com uma complexidade mais alta que não exigisse refatoração.
Código morto
Existem vários utilitários para encontrar código não utilizado; sua funcionalidade pode se sobrepor parcialmente.
- ineffassign: verifica atribuições inúteis
func foo() error { var res interface{} log.Println(res) res, err := loadData()
- deadcode: encontra funções não utilizadas
- unused: localiza funções não utilizadas, mas é melhor que deadcode
func unusedFunc() { formallyUsedFunc() } func formallyUsedFunc() { }
Como resultado, o não utilizado apontará para as duas funções ao mesmo tempo e o código morto apenas para o não utilizadoFunc. Graças a isso, o código extra é excluído em uma passagem. Também não utilizado encontra variáveis não utilizadas e campos de estrutura.
- varcheck: encontra variáveis não utilizadas
- unconvert: encontra conversões de tipo inúteis
var res int return int(res)
Se não houver tarefa para economizar no tempo necessário para iniciar as verificações, é melhor executá-las todas juntas. Se for necessária otimização, recomendo o uso não utilizado e a conversão.
Quão conveniente é configurar
A execução das ferramentas acima em sequência é inconveniente: os erros são emitidos em um formato diferente, a execução leva muito tempo. A verificação de um de nossos serviços com um tamanho de ~ 8000 linhas de código levou mais de dois minutos. Você também precisará instalar os utilitários separadamente.
Existem utilitários de agregação para resolver esse problema, por exemplo, goreporter e gometalinter . O Goreporter renderiza o relatório em html e o gometalinter grava no console.
O Gometalinter ainda é usado em alguns projetos grandes (por exemplo, janela de encaixe ). Ele sabe como instalar todos os utilitários com um único comando, executá-los em paralelo e formatar erros de acordo com o modelo. O tempo de execução no serviço acima foi reduzido para um minuto e meio.
A agregação funciona apenas pela coincidência exata do texto do erro; portanto, erros repetidos são inevitáveis na saída.
Em maio de 2018, o projeto golangci-lint apareceu no github, o que supera muito o gometalinter por conveniência:
- o tempo de execução no mesmo projeto foi reduzido para 16 segundos (8 vezes)
- quase nenhum erro duplicado
- limpar configuração yaml
- boa saída de erro com uma linha de código e um ponteiro para um problema

- não é necessário instalar utilitários adicionais
Agora, o aumento na velocidade é fornecido com a reutilização do SSA e do loader.Program , no futuro, também está planejado reutilizar a árvore AST, sobre a qual escrevi no início da seção Ferramentas.
No momento em que escrevemos este artigo em hub.docker.com, não havia imagem com documentação; portanto, criamos nossos próprios, personalizados de acordo com nossas idéias sobre conveniência. No futuro, a configuração será alterada, portanto, para a produção, recomendamos substituí-la pela sua. Para fazer isso, basta adicionar o arquivo .golangci.yaml ao diretório raiz do projeto ( um exemplo está no repositório golangci-lint).
PACKAGE=package/name docker run --rm -t \ -v $(GOPATH)/src/$(PACKAGE):/go/src/$(PACKAGE) \ -w /go/src/$(PACKAGE) \ roistat/golangci-lint
Este comando pode testar o projeto inteiro. Por exemplo, se estiver em ~/go/src/project
, altere o valor da variável para PACKAGE=project
. A validação funciona recursivamente em todos os pacotes internos.
Observe que este comando só funciona corretamente ao usar o fornecedor.
Implementação
Todos os nossos serviços de desenvolvimento usam docker. Qualquer projeto é executado sem o ambiente go instalado. Para executar os comandos, use o Makefile e adicione o comando lint a ele:
lint: @docker run --rm -t -v $(GOPATH)/src/$(PACKAGE):/go/src/$(PACKAGE) -w /go/src/$(PACKAGE) roistat/golangci-lint
Agora a verificação é iniciada com este comando:
make lint
Existe uma maneira fácil de bloquear o código com erros ao entrar no mestre - crie um gancho de pré-recebimento. É adequado se:
- Você tem um projeto pequeno e poucas dependências (ou elas estão no repositório)
- Não é um problema para você esperar o comando
git push
ser concluído por alguns minutos
Instruções de configuração do gancho : Gitlab , Bitbucket Server , Github Enterprise .
Em outros casos, é melhor usar o IC e proibir o código de mesclagem no qual há pelo menos um erro. Fazemos exatamente isso, adicionando o lançamento do linter antes dos testes.
Conclusão
A introdução de revisões sistemáticas reduziu significativamente o período de revisão. No entanto, outra coisa é mais importante: agora podemos discutir o cenário geral e a arquitetura na maioria das vezes. Isso permite que você pense sobre o desenvolvimento do projeto em vez de fazer buracos.