ข้ามไปยังเนื้อหา

Verification tools

output ทุกอันด้านล่างเป็นของจริง จากการรันแต่ละเครื่องมือกับ pattern ใน AI Bug Catalog จริงๆ ไม่มีเครื่องมือไหน overlap กันสมบูรณ์ และไม่มีตัวไหนใกล้เคียงคำว่าครบถ้วนเลย — ประเด็นของบทนี้คือให้รู้ชัดเจนว่าแต่ละตัวครอบคลุมถึงตรงไหน เพื่อให้รู้ว่าส่วนที่เหลือยังเป็นหน้าที่ของคุณอยู่

go vet มากับ Go toolchain และรันอัตโนมัติก่อน go test มันรายงานเฉพาะสิ่งที่พิสูจน์ได้จาก type system กับชุด analyzer ที่กำหนดตายตัวเท่านั้น — ไม่มีความเห็นเรื่อง style, false positive น้อยมาก และไม่ต้อง config เลย

จับได้แบบไม่มีเงื่อนไข ใน catalog นี้:

copylocks: Inc passes lock by value: main.Counter contains sync.Mutex
waitgroup: WaitGroup.Add called from inside new goroutine
errorsas: second argument to errors.As must be a non-nil pointer to either a type that implements error, or to any interface type
main.go:10:8: using resp before checking for errors

จับได้เฉพาะกับ go.mod directive ที่มันใช้: analyzer loopclosure จับ goroutine ที่ capture loop variable — แต่เฉพาะภายใต้ go directive ที่เก่ากว่า 1.22 เท่านั้น เพราะตัว bug เองก็มีอยู่แค่ในกรณีนั้น

พลาด: ทุกอย่างที่ต้องพิสูจน์เรื่องพฤติกรรมของโปรแกรม ไม่ใช่โครงสร้าง — leak, การเขียน nil map ที่พิสูจน์ไม่ได้ว่า nil เสมอ, ไม่เช็ก status code, ไม่มี Timeout field, err shadowing (analyzer shadow มีอยู่จริงแต่ไม่รันโดย default — เปิดเองด้วย go vet -vettool=$(which shadow) หรือ setting govet.enable: [shadow] ของ golangci-lint)

staticcheck — static analysis ที่ลึกกว่า ไม่มีเรื่องพลาดที่น่าอาย

หัวข้อที่มีชื่อว่า “staticcheck — static analysis ที่ลึกกว่า ไม่มีเรื่องพลาดที่น่าอาย”

staticcheck ครอบคลุม check ของ go vet ทั้งหมดแล้วเพิ่มอีกหลายร้อยตัว จัดเป็น SA* (bug), ST* (style), และ prefix อื่นๆ มันช้ากว่าและบางครั้ง flag สิ่งที่ตั้งใจไว้ แต่มันจับ bug จริงที่ go vet จับไม่ได้เชิงโครงสร้าง

จับได้ ใน catalog นี้:

SA4023: this comparison is always true (typed-nil error, when provable)
SA2000: should call wg.Add(1) before starting the goroutine to avoid a race
SA5000: assignment to nil map (only when provably always-nil)
SA4011: ineffective break statement. Did you mean to break out of the outer loop?
SA1004: sleeping for 5 nanoseconds is probably a bug; be explicit if it isn't
ST1006: receiver name should be a reflection of its identity; don't use generic names such as "this" or "self"

พลาด: หมวดที่ต้องรันจริงเหมือนที่ go vet พลาด บวกกับอะไรก็ตามที่ต้องรู้ทั้ง codebase — ที่เด่นที่สุดคือ interface มี implementation กี่ตัว

golangci-lint v2 — รัน go vet, staticcheck, และอีกหลายสิบตัวในครั้งเดียว

หัวข้อที่มีชื่อว่า “golangci-lint v2 — รัน go vet, staticcheck, และอีกหลายสิบตัวในครั้งเดียว”

golangci-lint เป็น meta-linter: linters.default: standard ใน config รันชุดที่คัดมาแล้วซึ่งมี go vet (ในชื่อ govet) และ staticcheck รวมอยู่ พร้อม errcheck, unused, และอื่นๆ คำสั่งเดียวแทนที่การรันแต่ละเครื่องมือเอง

version: "2"
linters:
default: standard

จับทุกอย่างที่ go vet และ staticcheck จับด้านบน โดยรายงานภายใต้ linter ที่เจอมัน — golangci-lint run บนไฟล์ errors.As ที่ขาด & รายงานว่า:

main.go:19:5: errorsas: second argument to errors.As must be a non-nil pointer to either a type that implements error, or to any interface type (govet)

จับได้ด้วย ผ่าน errcheck (อยู่ใน standard) — ค่า return ที่ไม่ถูกเช็ก ซึ่งเป็นคนละ bug กับตัวที่มันบังเอิญอยู่ข้างๆ ใน catalog นี้:

main.go:10:23: Error return value of `resp.Body.Close` is not checked (errcheck)

ต้อง opt-in เอง แม้ analyzer จะมีอยู่ใน toolbox ของ golangci-lint แล้ว:

  • shadow (check เรื่อง err shadowing) — เปิดด้วย linters.settings.govet.enable: [shadow]
  • modernize (flag interface{} ในจุดที่ any อ่านง่ายกว่า และการ rewrite อื่นๆ) — เปิดด้วย linters.enable: [modernize]

พลาด: จุดบอดเดียวกับ go vet + staticcheck รวมกัน เพราะนั่นคือสิ่งที่มันรัน

-race — เครื่องมือเดียวในลิสต์นี้ที่เฝ้าดูโปรแกรมตอนรันจริง

หัวข้อที่มีชื่อว่า “-race — เครื่องมือเดียวในลิสต์นี้ที่เฝ้าดูโปรแกรมตอนรันจริง”

go run -race และ go test -race ใส่ instrumentation ให้ทุก memory access และตรวจจับการเข้าถึง memory เดียวกันแบบ concurrent ที่ไม่ sync กันจริงๆระหว่างการรันครั้งนั้น — ไม่ใช่ความเป็นไปได้ทางทฤษฎี แต่เป็นสิ่งที่สังเกตเห็นจริง

จับได้ ใน catalog นี้ — pattern wg.Add ข้างใน goroutine รันด้วย -race:

WARNING: DATA RACE
Write at 0x00c00013e000 by goroutine 7:
main.main.func1()
.../main.go:18 +0xf0
Previous read at 0x00c00013e000 by main goroutine:
main.main()
.../main.go:24 +0x23c
Found 1 data race(s)

พลาด: race ไหนก็ตามที่บังเอิญไม่เกิดขึ้นในการรันครั้งที่คุณทดสอบ — race detector พิสูจน์ได้แค่ว่ามี race ที่มันสังเกตเห็น ไม่เคยพิสูจน์ว่าไม่มี race ที่มันไม่เห็น มันก็ไม่มีอะไรจะพูดเกี่ยวกับ goroutine leak, resource leak, หรือ bug ทาง logic แบบ single-threaded ด้วย — พวกนั้นไม่ใช่ race

govulncheck — CVE ที่รู้จักแล้ว กรองเหลือแค่โค้ดที่คุณเรียกใช้จริง

หัวข้อที่มีชื่อว่า “govulncheck — CVE ที่รู้จักแล้ว กรองเหลือแค่โค้ดที่คุณเรียกใช้จริง”

govulncheck เทียบ dependency ของคุณกับ Go vulnerability database แล้วกรองผลลัพธ์ให้เหลือเฉพาะ vulnerability ที่ call graph ของโค้ดคุณเข้าถึงได้จริง — ไม่ใช่แค่ “มี version ที่มีช่องโหว่อยู่ใน go.sum ที่ไหนสักแห่ง”

output จริง module ที่ depend on golang.org/x/[email protected] และเรียก language.Parse:

Vulnerability #1: GO-2021-0113
Out-of-bounds read in golang.org/x/text/language
More info: https://pkg.go.dev/vuln/GO-2021-0113
Module: golang.org/x/text
Found in: golang.org/x/[email protected]
Fixed in: golang.org/x/[email protected]
Example traces found:
#1: main.go:10:26: vulndemo.main calls language.Parse
Your code is affected by 1 vulnerability from 1 module.
This scan also found 1 vulnerability in packages you import and 2
vulnerabilities in modules you require, but your code doesn't appear to call
these vulnerabilities.

พลาด: ทุกอย่างใน catalog นี้ govulncheck ตอบคำถามเดียวเท่านั้น — “มี CVE ที่รู้จักแล้วส่งผลกับโค้ดที่คุณเรียกใช้จริงไหม” — และไม่มีอะไรจะพูดเกี่ยวกับ bug ทาง logic, bug ทาง concurrency, หรือการละเมิด API contract เลย ไม่ว่าจะเป็นของคุณเองหรือของ dependency ที่ยังไม่มีใครค้นพบ

gopls — analysis แบบเดียวกับที่ editor ของคุณรันอยู่แล้ว แต่จาก command line

หัวข้อที่มีชื่อว่า “gopls — analysis แบบเดียวกับที่ editor ของคุณรันอยู่แล้ว แต่จาก command line”

gopls check <file> รัน diagnostic แบบเดียวกับที่ editor ของคุณแสดง inline โดยไม่ต้องเปิดมันขึ้นมา — มีประโยชน์ใน CI หรือผ่าน SSH มันมี analyzer ของ go vet รวมอยู่และเพิ่มของตัวเองอีกนิดหน่อย เช่น nilness analysis

output จริง บนไฟล์ errors.As ที่ขาด & — gopls เจอทั้งการใช้ errors.As ผิดและ bug ที่เป็นผลตามมาอีกตัวในไฟล์เดียวกัน ที่ไม่มีเครื่องมือไหนในลิสต์นี้รายงาน:

main.go:19:5-23: second argument to errors.As must be a non-nil pointer to either a type that implements error, or to any interface type
main.go:20:32: nil dereference in field selection

บรรทัดที่สองคือ nilness analyzer ของ gopls เอง: เนื่องจาก errors.As จะ fail (ตามที่ diagnostic แรกบอก) ve จึงยังเป็น nil และบรรทัดถัดไปที่เข้าถึง ve.Field คือ nil pointer dereference ที่เกิดขึ้นแน่นอน — ผลลัพธ์ที่ go vet และ staticcheck ไม่ได้เชื่อมโยงให้

พลาด: หมวดเดียวกับ go vet เพราะมันสร้างบน analysis framework เดียวกัน บวกกับอะไรก็ตามที่ต้องรู้ทั้ง codebase หรือข้อมูลตอน runtime

go test -cover — บอกว่าอะไรไม่เคยถูกรัน ไม่ใช่บอกว่าอะไรพัง

หัวข้อที่มีชื่อว่า “go test -cover — บอกว่าอะไรไม่เคยถูกรัน ไม่ใช่บอกว่าอะไรพัง”

go test -cover รายงานเปอร์เซ็นต์ของ statement ที่ถูก execute ระหว่างรัน test มันไม่ใช่ bug finder มันคือแผนที่บอกว่าเครื่องมืออื่นๆ — และการอ่านของคุณเอง — ไม่เคยดูตรงไหนเลย

output จริง method Store.Set ที่ test มีแค่ทดสอบ Get:

ok scratch/covdemo 0.474s coverage: 66.7% of statements

33.3% ที่ไม่ถูกทดสอบคือ Set เป๊ะๆ — method ที่ panic บน nil map coverage ไม่ได้เจอ bug นั้น มันแค่เจอว่าไม่มีใครดูตรงนั้นเลย

พลาด: ความถูกต้องทั้งหมด coverage 100% พิสูจน์แค่ว่าทุกบรรทัดถูกรันอย่างน้อยหนึ่งครั้งระหว่าง test บางตัว มันไม่พิสูจน์อะไรเลยว่า assertion ใน test นั้นถูกต้องหรือเปล่า หรือ input ที่ทำให้เกิด bug เคยถูกลองหรือยัง

golangci-lint run ครอบคลุม go vet และ staticcheck พร้อมกันอยู่แล้ว เพิ่ม govulncheck สำหรับ CVE ของ dependency และ go test -race -cover สำหรับ race ตอน runtime กับ coverage แล้วบรรทัดเดียวครอบคลุมทุกอย่างในหน้านี้ ยกเว้น gopls (ซึ่งเป็นแบบ interactive/editor-oriented ไม่ใช่ CI step):

Terminal window
golangci-lint run && govulncheck ./... && go test -race -cover ./...
flowchart TD
  subgraph Static["Static (source only)"]
    Vet["go vet<br/>structural bugs (type-provable):<br/>copylocks, waitgroup, errorsas, loopclosure"]
    SC["staticcheck (SA*, ST*)<br/>deeper static bugs"]
    Vuln["govulncheck<br/>known CVEs reached by your call graph"]
  end
  subgraph Dynamic["Dynamic (must run)"]
    Race["-race<br/>concurrency bugs that actually occur (only what ran)"]
    Cover["go test -cover<br/>what ran vs. didn't"]
  end
  GCL["golangci-lint runs the whole Static box (go vet + staticcheck + more) in one command"] --> Vet
  GCL --> SC
เครื่องมือ static พิสูจน์ได้จาก source อย่างเดียว ส่วนเครื่องมือ dynamic เห็นแค่สิ่งที่รันจริงเท่านั้น
CI pipeline รัน golangci-lint run แล้วผ่านโดยไม่มี issue เลย ข้อไหนต่อไปนี้ยังเป็นไปได้อยู่?
ทำไม staticcheck ถึง fail ด้วย 'package requires newer Go version go1.27' กับ module ที่โค้ดไม่ได้ใช้ syntax เฉพาะของ Go 1.27 เลย?
gopls รายงานทั้งการใช้ errors.As ผิดและ 'nil dereference in field selection' อีกสองบรรทัดถัดมาในไฟล์เดียวกัน diagnostic ตัวที่สองขึ้นอยู่กับอะไร?
ไฟล์หนึ่งมี test coverage 100% นั่นรับประกันอะไร?