Análisis estático en Go: cómo ahorramos tiempo al verificar el código


Hola Habr Mi nombre es Sergey Rudachenko, soy un experto técnico en Roistat. En los últimos dos años, nuestro equipo ha estado traduciendo varias partes del proyecto en microservicios Go. Son desarrollados por varios equipos, por lo que necesitábamos establecer una barra de calidad de código duro. Para hacer esto, utilizamos varias herramientas, en este artículo nos centraremos en una de ellas: el análisis estático.


El análisis estático es el proceso de verificar automáticamente el código fuente utilizando utilidades especiales. Este artículo hablará sobre sus beneficios, describirá brevemente las herramientas populares y dará instrucciones para la implementación. Vale la pena leerlo si no se ha encontrado con herramientas similares o no las ha utilizado de manera sistemática.


En los artículos sobre este tema, a menudo se encuentra el término "linter". Para nosotros, este es un nombre conveniente para herramientas simples para el análisis estático. La tarea de linter es buscar errores simples y un diseño incorrecto.


¿Por qué se necesitan linters?


Cuando trabajas en equipo, lo más probable es que realices revisiones de código. Los errores omitidos en la revisión son errores potenciales. Se perdió un error no controlado: no reciba un mensaje informativo y buscará el problema a ciegas. Confundido en el tipo de conversión o convertido en mapa nulo, peor aún, el binario caerá del pánico.


Los errores descritos anteriormente se pueden agregar a las Convenciones de código , pero encontrarlos al leer la solicitud de extracción no es tan simple, ya que el revisor tendrá que leer el código. Si no hay un compilador en su cabeza, algunos de los problemas irán a la batalla de todos modos. Además, la búsqueda de errores menores distrae la verificación de la lógica y la arquitectura. A distancia, admitir dicho código será más costoso. Escribimos en un lenguaje escrito estáticamente, es extraño no usarlo.


Herramientas populares


La mayoría de las herramientas para el análisis estático utilizan los paquetes go/ast y go/parser . Proporcionan funciones para analizar la sintaxis de los archivos .go. El hilo de ejecución estándar (por ejemplo, para la utilidad golint) es el siguiente:


  • se carga la lista de archivos de los paquetes requeridos
  • parser.ParseFile(...) (*ast.File, error) se ejecuta para cada archivo
  • busca reglas compatibles para cada archivo o paquete
  • la verificación pasa por cada instrucción, por ejemplo, así:

 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) 

Además de AST, hay una asignación estática única (SSA). Esta es una forma más compleja de analizar el código que funciona con un hilo de ejecución en lugar de construcciones de sintaxis. En este artículo, no lo consideraremos en detalle, puede leer la documentación y ver el ejemplo de la utilidad stackcheck .


A continuación, solo se considerarán las utilidades populares que realizan comprobaciones útiles para nosotros.


gofmt


Esta es la utilidad estándar del paquete go que comprueba la coincidencia de estilos y puede solucionarlo automáticamente. El cumplimiento del estilo es un requisito obligatorio para nosotros, por lo tanto, la verificación gofmt se incluye en todos nuestros proyectos.


chequeo


Typecheck verifica la coincidencia de tipos en el código y es compatible con el proveedor (a diferencia de gotype). Se requiere su lanzamiento para verificar la compilación, pero no ofrece garantías completas.


ir veterinario


La utilidad go vet es parte del paquete estándar y el equipo Go la recomienda para su uso. Comprueba una serie de errores comunes, por ejemplo:


  • mal uso de printf y funciones similares
  • etiquetas de compilación incorrectas
  • función de comparación y nula

Golint


Golint es desarrollado por el equipo de Go y valida el código basado en los documentos de Effective Go y CodeReviewComments . Desafortunadamente, no hay documentación detallada, pero el código muestra que se verifica lo siguiente:


 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() 

comprobación estática


Los propios desarrolladores presentan la comprobación estática como un veterano mejorado. Hay muchos controles, se dividen en grupos:


  • mal uso de bibliotecas estándar
  • problemas de subprocesos múltiples
  • problemas con las pruebas
  • código inútil
  • problemas de rendimiento
  • diseños dudosos

gosimple


Se especializa en encontrar estructuras que valgan la pena simplificar, por ejemplo:


Antes ( código fuente golint )


 func (f *file) isMain() bool { if ffName.Name == "main" { return true } return false } 

Despues


 func (f *file) isMain() bool { return ffName.Name == "main" } 

La documentación es similar a staticcheck e incluye ejemplos detallados.


errcheck


Los errores devueltos por las funciones no pueden ser ignorados. Los motivos se describen en detalle en el documento vinculante Effective Go . Errcheck no omitirá el siguiente código:


 json.Unmarshal(text, &val) f, _ := os.OpenFile(/* ... */) 

gas


Encuentra vulnerabilidades en el código: accesos codificados, inyecciones sql y uso de funciones hash inseguras.


Ejemplos de errores:


 //    IP  l, err := net.Listen("tcp", ":2000") //  sql  q := fmt.Sprintf("SELECT * FROM foo where name = '%s'", name) q := "SELECT * FROM foo where name = " + name //     import "crypto/md5" 

difamado


En Go, el orden de los campos en las estructuras afecta el consumo de memoria. Malignado encuentra ordenación no óptima. Con este orden de campos:


 struct { a bool b string c bool } 

La estructura ocupará 32 bits en la memoria debido a la adición de bits vacíos después de los campos ayc.


imagen


Si cambiamos la clasificación y juntamos dos campos bool, la estructura tomará solo 24 bits:


imagen


Imagen original en stackoverflow


goconst


Las variables mágicas en el código no reflejan el significado y complican la lectura. Goconst encuentra literales y números que aparecen en el código 2 veces o más. Tenga en cuenta que, a menudo, incluso un solo uso puede ser un error.


gocyclo


Consideramos que la complejidad ciclomática del código es una métrica importante. Gocycle muestra complejidad para cada función. Solo se pueden mostrar las funciones que exceden el valor especificado.


 gocyclo -over 7 package/name 

Elegimos un valor umbral de 7 para nosotros, porque no encontramos un código con una mayor complejidad que no requiriera refactorización.


Código muerto


Existen varias utilidades para encontrar código no utilizado; su funcionalidad puede superponerse parcialmente.


  • ineffassign: verifica tareas inútiles

 func foo() error { var res interface{} log.Println(res) res, err := loadData() //  res    return err } 

  • código muerto: encuentra funciones no utilizadas
  • no utilizado: encuentra funciones no utilizadas, pero lo hace mejor que el código muerto

 func unusedFunc() { formallyUsedFunc() } func formallyUsedFunc() { } 

Como resultado, sin usar apuntará a ambas funciones a la vez, y el código muerto solo a no utilizadoFunc. Gracias a esto, el código adicional se elimina en una sola pasada. También no utilizado encuentra variables no utilizadas y campos de estructura.


  • varcheck: encuentra variables no utilizadas
  • desconvertir: encuentra conversiones de tipo inútiles

 var res int return int(res) // unconvert error 

Si no hay una tarea que ahorrar en el tiempo que lleva iniciar las comprobaciones, es mejor ejecutarlas todas juntas. Si se necesita optimización, recomiendo usar sin usar y sin convertir.


Qué conveniente es configurar


Ejecutar las herramientas anteriores en secuencia es inconveniente: los errores se emiten en un formato diferente, la ejecución lleva mucho tiempo. Verificar uno de nuestros servicios con un tamaño de ~ 8000 líneas de código tomó más de dos minutos. También deberá instalar las utilidades por separado.


Hay utilidades de agregación para resolver este problema, por ejemplo, goreporter y gometalinter . Goreporter representa el informe en html y gometalinter escribe en la consola.


Gometalinter todavía se usa en algunos proyectos grandes (por ejemplo, docker ). Él sabe cómo instalar todas las utilidades con un solo comando, ejecutarlas en paralelo y formatear los errores de acuerdo con la plantilla. El tiempo de ejecución en el servicio anterior se redujo a un minuto y medio.


La agregación funciona solo por la coincidencia exacta del texto del error, por lo tanto, los errores repetidos son inevitables en la salida.


En mayo de 2018, el proyecto golangci-lint apareció en el github, que supera en gran medida la comodidad de gometalinter:


  • El tiempo de ejecución del mismo proyecto se redujo a 16 segundos (8 veces)
  • casi no hay errores duplicados
  • clear yaml config
  • salida de error agradable con una línea de código y un puntero a un problema
  • no es necesario instalar utilidades adicionales

Ahora, el aumento de la velocidad se obtiene reutilizando SSA y el cargador . Programa , en el futuro también se planea reutilizar el árbol AST, sobre el que escribí al comienzo de la sección Herramientas.


Al momento de escribir este artículo en hub.docker.com no había imagen con documentación, por lo que hicimos la nuestra, personalizada de acuerdo con nuestras ideas sobre conveniencia. En el futuro, la configuración cambiará, por lo que para la producción recomendamos reemplazarla por la suya. Para hacer esto, simplemente agregue el archivo .golangci.yaml al directorio raíz del proyecto ( un ejemplo está en el repositorio 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 puede probar todo el proyecto. Por ejemplo, si está en ~/go/src/project , cambie el valor de la variable a PACKAGE=project . La validación funciona de forma recursiva en todos los paquetes internos.


Tenga en cuenta que este comando solo funciona correctamente cuando se utiliza el proveedor.


Implementación


Todos nuestros servicios de desarrollo usan docker. Cualquier proyecto se ejecuta sin el entorno go instalado. Para ejecutar los comandos, use el Makefile y agréguele el comando lint:


 lint: @docker run --rm -t -v $(GOPATH)/src/$(PACKAGE):/go/src/$(PACKAGE) -w /go/src/$(PACKAGE) roistat/golangci-lint 

Ahora la comprobación se inicia con este comando:


 make lint 

Hay una manera fácil de bloquear el código con errores para que no entren al maestro: crear un enlace previo a la recepción. Es adecuado si:


  1. Tiene un proyecto pequeño y pocas dependencias (o están en el repositorio)
  2. No es un problema que espere a que se complete el comando git push durante unos minutos.

Instrucciones de configuración de gancho : Gitlab , Bitbucket Server , Github Enterprise .


En otros casos, es mejor usar CI y prohibir el código de fusión en el que hay al menos un error. Hacemos exactamente eso, agregando el lanzamiento de la interfaz antes de las pruebas.


Conclusión


La introducción de revisiones sistemáticas ha reducido significativamente el período de revisión. Sin embargo, otra cosa es más importante: ahora podemos discutir el panorama general y la arquitectura la mayor parte del tiempo. Esto le permite pensar en el desarrollo del proyecto en lugar de tapar agujeros.

Source: https://habr.com/ru/post/es413175/


All Articles