
Hai, Habr. Nama saya Sergey Rudachenko, saya seorang ahli teknis di Roistat. Selama dua tahun terakhir, tim kami telah menerjemahkan berbagai bagian proyek ke dalam layanan microser on Go. Mereka dikembangkan oleh beberapa tim, jadi kami perlu menetapkan bar kualitas kode keras. Untuk melakukan ini, kami menggunakan beberapa alat, dalam artikel ini kami akan fokus pada salah satunya - pada analisis statis.
Analisis statis adalah proses pengecekan kode sumber secara otomatis menggunakan utilitas khusus. Artikel ini akan berbicara tentang manfaatnya, menjelaskan secara singkat alat-alat populer dan memberikan instruksi untuk implementasi. Perlu dibaca jika Anda belum menemukan alat yang sama sekali atau menggunakannya secara tidak sistematis.
Dalam artikel tentang topik ini, istilah "linter" sering ditemukan. Bagi kami, ini adalah nama yang tepat untuk alat sederhana untuk analisis statis. Tugas linter adalah mencari kesalahan sederhana dan desain yang salah.
Mengapa linter dibutuhkan?
Saat bekerja dalam tim, Anda kemungkinan besar akan melakukan tinjauan kode. Kesalahan yang dilewati dalam ulasan adalah bug potensial. Terjawab error
tidak tertangani - jangan menerima pesan informatif dan Anda akan mencari masalah secara membabi buta. Keliru dalam tipe casting atau berubah menjadi nihil peta - bahkan lebih buruk, biner akan jatuh dari panik.
Kesalahan yang dijelaskan di atas dapat ditambahkan ke Konvensi Kode , tetapi untuk menemukannya saat membaca permintaan tarik tidak begitu sederhana, karena peninjau harus membaca kode. Jika tidak ada kompiler di kepala Anda, beberapa masalah tetap akan pergi ke pertempuran. Selain itu, pencarian untuk kesalahan kecil mengalihkan perhatian dari memeriksa logika dan arsitektur. Di kejauhan, mendukung kode semacam itu akan menjadi lebih mahal. Kami menulis dalam bahasa yang diketik secara statis, aneh untuk tidak menggunakannya.
Alat populer
Sebagian besar alat untuk analisis statis menggunakan paket go/ast
dan go/parser
. Mereka menyediakan fungsi untuk mem-parsing sintaks file .go. Utas eksekusi standar (misalnya, untuk utilitas golint) adalah sebagai berikut:
- daftar file dari paket yang diperlukan dimuat
parser.ParseFile(...) (*ast.File, error)
dijalankan untuk setiap file- memeriksa aturan yang didukung untuk setiap file atau paket
- verifikasi melewati setiap instruksi, misalnya, seperti ini:
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)
Selain AST, ada Single Static Assignment (SSA). Ini adalah cara parsing kode yang lebih kompleks yang berfungsi dengan utas eksekusi daripada konstruksi sintaks. Pada artikel ini, kami tidak akan mempertimbangkannya secara rinci, Anda dapat membaca dokumentasi dan melihat contoh utilitas stackcheck .
Berikutnya, hanya utilitas populer yang melakukan pemeriksaan yang bermanfaat bagi kami yang akan dipertimbangkan.
gofmt
Ini adalah utilitas standar dari paket go yang memeriksa kecocokan gaya dan secara otomatis dapat memperbaikinya. Kepatuhan terhadap gaya adalah persyaratan wajib bagi kami, oleh karena itu verifikasi gofmt termasuk dalam semua proyek kami.
ketik cek
Typecheck memeriksa pencocokan jenis dalam kode dan mendukung vendor (tidak seperti gotype). Peluncurannya diperlukan untuk memeriksa kompilasi, tetapi tidak memberikan jaminan penuh.
pergi dokter hewan
Utilitas go vet adalah bagian dari paket standar dan direkomendasikan untuk digunakan oleh tim Go. Memeriksa sejumlah kesalahan umum, misalnya:
- penyalahgunaan printf dan fungsi serupa
- tag build salah
- membandingkan fungsi dan nihil
golint
Golint dikembangkan oleh tim Go dan memvalidasi kode berdasarkan pada dokumen Go Efektif dan CodeReviewComments . Sayangnya, tidak ada dokumentasi terperinci, tetapi kode ini menunjukkan bahwa yang berikut ini dicentang:
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()
cek statis
Pengembang sendiri menghadirkan pemeriksaan statis sebagai go go dokter hewan yang ditingkatkan. Ada banyak cek, mereka dibagi menjadi beberapa kelompok:
- penyalahgunaan perpustakaan standar
- masalah multithreading
- masalah dengan tes
- kode tidak berguna
- masalah kinerja
- desain yang meragukan
sederhana
Ia berspesialisasi dalam menemukan struktur yang layak disederhanakan, misalnya:
Sebelumnya ( kode sumber golint )
func (f *file) isMain() bool { if ffName.Name == "main" { return true } return false }
Setelah
func (f *file) isMain() bool { return ffName.Name == "main" }
Dokumentasi ini mirip dengan pemeriksaan statis dan menyertakan contoh terperinci.
errcheck
Kesalahan yang dikembalikan oleh fungsi tidak dapat diabaikan. Alasannya dijelaskan secara rinci dalam dokumen yang mengikat Efektif Go . Errcheck tidak akan melewatkan kode berikut:
json.Unmarshal(text, &val) f, _ := os.OpenFile()
gas
Menemukan kerentanan dalam kode: akses hardcoded, injeksi sql dan penggunaan fungsi hash tidak aman.
Contoh kesalahan:
difitnah
Di Go, urutan bidang dalam struktur memengaruhi konsumsi memori. Memfitnah menemukan penyortiran tidak optimal. Dengan urutan bidang ini:
struct { a bool b string c bool }
Struktur akan menempati 32 bit dalam memori karena penambahan bit kosong setelah bidang a dan c.

Jika kita mengubah penyortiran dan menyatukan dua bidang bool, maka struktur hanya akan mengambil 24 bit:

Gambar asli di stackoverflow
goconst
Variabel magis dalam kode tidak mencerminkan makna dan menyulitkan membaca. Goconst menemukan literal dan angka yang muncul dalam kode 2 kali atau lebih. Harap dicatat, sering kali penggunaan tunggal dapat menjadi kesalahan.
gocyclo
Kami menganggap kompleksitas kode siklomatik sebagai metrik penting. Gocycle menunjukkan kompleksitas untuk setiap fungsi. Hanya fungsi yang melebihi nilai yang ditentukan yang dapat ditampilkan.
gocyclo -over 7 package/name
Kami memilih nilai ambang 7 untuk diri kami sendiri, karena kami tidak menemukan kode dengan kompleksitas lebih tinggi yang tidak memerlukan refactoring.
Kode mati
Ada beberapa utilitas untuk menemukan kode yang tidak digunakan, fungsinya sebagian mungkin tumpang tindih.
- ineffassign: memeriksa tugas yang tidak berguna
func foo() error { var res interface{} log.Println(res) res, err := loadData()
- deadcode: menemukan fungsi yang tidak digunakan
- tidak digunakan: menemukan fungsi yang tidak digunakan, tetapi apakah itu lebih baik daripada deadcode
func unusedFunc() { formallyUsedFunc() } func formallyUsedFunc() { }
Akibatnya, yang tidak terpakai akan mengarah ke kedua fungsi sekaligus, dan deadcode hanya ke unusedFunc. Berkat ini, kode tambahan dihapus dalam satu pass. Juga tidak digunakan menemukan variabel yang tidak digunakan dan bidang struktur.
- varcheck: menemukan variabel yang tidak digunakan
- batalkan konversi: menemukan konversi jenis yang tidak berguna
var res int return int(res)
Jika tidak ada tugas untuk menghemat waktu yang diperlukan untuk meluncurkan cek, lebih baik menjalankan semuanya bersama-sama. Jika optimasi diperlukan, saya sarankan menggunakan yang tidak terpakai dan tidak bertobat.
Betapa nyamannya mengkonfigurasi
Menjalankan alat-alat di atas secara berurutan tidak nyaman: kesalahan dikeluarkan dalam format yang berbeda, eksekusi membutuhkan banyak waktu. Memeriksa salah satu layanan kami dengan ukuran ~ 8000 baris kode membutuhkan waktu lebih dari dua menit. Anda juga harus menginstal utilitas secara terpisah.
Ada utilitas agregasi untuk menyelesaikan masalah ini, misalnya goreporter dan gometalinter . Goreporter membuat laporan dalam html, dan gometalinter menulis ke konsol.
Gometalinter masih digunakan di beberapa proyek besar (mis. Buruh pelabuhan ). Dia tahu cara menginstal semua utilitas dengan satu perintah, menjalankannya secara paralel, dan memformat kesalahan sesuai dengan templat. Waktu eksekusi dalam layanan di atas dikurangi menjadi satu setengah menit.
Agregasi hanya bekerja dengan kebetulan yang tepat dari teks kesalahan, oleh karena itu, kesalahan berulang tidak bisa dihindari pada output.
Pada bulan Mei 2018, proyek golangci-lint muncul di github, yang jauh melampaui kenyamanan pengguna logam:
- waktu pelaksanaan pada proyek yang sama dikurangi menjadi 16 detik (8 kali)
- hampir tidak ada kesalahan rangkap
- hapus yaml config
- output kesalahan yang bagus dengan garis kode dan pointer ke masalah

- tidak perlu menginstal utilitas tambahan
Sekarang peningkatan kecepatan disediakan dengan menggunakan kembali SSA dan loader . Program , di masa depan juga direncanakan untuk menggunakan kembali pohon AST, yang saya tulis di awal bagian Tools.
Pada saat menulis artikel ini di hub.docker.com tidak ada gambar dengan dokumentasi, jadi kami membuat sendiri, disesuaikan sesuai dengan ide-ide kami tentang kenyamanan. Di masa depan, konfigurasi akan berubah, jadi untuk produksi kami sarankan menggantinya dengan milik Anda. Untuk melakukan ini, cukup tambahkan file .golangci.yaml ke direktori root proyek ( contohnya ada di repositori golangci-lint).
PACKAGE=package/name docker run --rm -t \ -v $(GOPATH)/src/$(PACKAGE):/go/src/$(PACKAGE) \ -w /go/src/$(PACKAGE) \ roistat/golangci-lint
Perintah ini dapat menguji seluruh proyek. Misalnya, jika ada di ~/go/src/project
, maka ubah nilai variabel menjadi PACKAGE=project
. Validasi bekerja secara rekursif pada semua paket internal.
Harap dicatat bahwa perintah ini hanya berfungsi dengan benar saat menggunakan vendor.
Implementasi
Semua layanan pengembangan kami menggunakan buruh pelabuhan. Setiap proyek berjalan tanpa lingkungan go diinstal. Untuk menjalankan perintah, gunakan Makefile dan tambahkan perintah lint ke sana:
lint: @docker run --rm -t -v $(GOPATH)/src/$(PACKAGE):/go/src/$(PACKAGE) -w /go/src/$(PACKAGE) roistat/golangci-lint
Sekarang cek dimulai dengan perintah ini:
make lint
Ada cara mudah untuk memblokir kode dengan kesalahan saat memasuki master - buat pre-accept-hook. Sangat cocok jika:
- Anda memiliki proyek kecil dan beberapa dependensi (atau mereka berada di repositori)
- Tidak masalah bagi Anda untuk menunggu perintah
git push
selesai selama beberapa menit
Petunjuk konfigurasi hook : Gitlab , Bitbucket Server , Github Enterprise .
Dalam kasus lain, lebih baik menggunakan CI dan melarang menggabungkan kode yang setidaknya ada satu kesalahan. Kami melakukan hal itu, menambahkan peluncuran linter sebelum tes.
Kesimpulan
Pengenalan ulasan sistematis telah secara signifikan mengurangi periode ulasan. Namun, hal lain yang lebih penting: sekarang kita dapat mendiskusikan gambaran besar dan arsitektur sebagian besar waktu. Ini memungkinkan Anda untuk memikirkan pengembangan proyek alih-alih memasukkan lubang.