The Go Toolchain
คำสั่ง go คือมีดพับสวิส
หัวข้อที่มีชื่อว่า “คำสั่ง go คือมีดพับสวิส”binary go รวมทุกเครื่องมือที่นักพัฒนา Go ต้องการไว้ใน executable เดียว ไม่มี build system แยกต่างหาก ไม่ต้องใช้ Makefile สำหรับ workflow พื้นฐาน และไม่มี plugin ecosystem ที่ต้องตั้งค่า คำสั่งต่อไปนี้ครอบคลุม 90% ของการพัฒนา Go ประจำวัน
go run — compile และ execute ในขั้นตอนเดียว
หัวข้อที่มีชื่อว่า “go run — compile และ execute ในขั้นตอนเดียว”go run compile ไฟล์ที่ระบุและรัน binary ที่ได้ทันที เหมาะสำหรับ scripts และการทดสอบรวดเร็ว — ไม่มีไฟล์ output เหลือบน disk
go run main.gogo run . # run the package in the current directorygo run ./cmd/server # run a specific cmd sub-packagego build — สร้าง binary
หัวข้อที่มีชื่อว่า “go build — สร้าง binary”go build compile package และเขียน binary ลง disk โดยไม่มี flag -o binary จะตั้งชื่อตาม element สุดท้ายของ package path (./cmd/server → server); ที่ module root ก็คือ element สุดท้ายของ module (go build . ใน github.com/yourname/myapp เขียน myapp)
go build . # build current package, output: ./myappgo build -o bin/server ./cmd/server # build to a specific pathgo fmt — จัดรูปแบบ canonical
หัวข้อที่มีชื่อว่า “go fmt — จัดรูปแบบ canonical”go fmt รัน gofmt กับไฟล์ .go ทั้งหมดใน package และเขียนผลลัพธ์กลับลง disk มีวิธีเดียวเท่านั้นในการจัดรูปแบบโค้ด Go
go fmt ./... # format every package in the modulegofmt -w -l ./ # equivalent: write changes, list modified filesใน CI ตรวจสอบว่าโค้ดถูกจัดรูปแบบแล้ว:
gofmt -l . | grep -q . && echo "unformatted files found" && exit 1 || echo "all formatted"go vet — analyzer ที่จับ bug ที่ AI เขียน
หัวข้อที่มีชื่อว่า “go vet — analyzer ที่จับ bug ที่ AI เขียน”go vet ไม่ใช่การตรวจแค่แบบเดียว — มันคือชุด analyzer อิสระหลายตัว แต่ละตัวมีชื่อของตัวเอง ที่คำสั่ง go รันรวมกัน go doc cmd/vet แสดงรายการทั้งหมด; หกตัวด้านล่างจับ pattern ที่พบบ่อยในโค้ด generated แต่ละตัวยืนยันด้วย bug จริงและข้อความจริงของ go vet:
copylocks — struct ที่มี sync.Mutex (หรือคล้ายกัน) ถูก copy ทำให้สำเนามี lock state เป็นอิสระที่ไม่มีประโยชน์:
type Safe struct { mu sync.Mutex count int}
s := Safe{}s2 := s // copies the mutexmain.go:12:8: assignment copies lock value to s2: example.com/vetdemo/copylocks.Safe contains sync.Mutexlostcancel — cancel function จาก context.WithCancel ถูกทิ้งไป ทำให้ context leak จนกว่า parent จะจบ:
ctx, _ := context.WithCancel(context.Background())main.go:6:7: the cancel function returned by context.WithCancel should be called, not discarded, to avoid a context leakunusedresult — function ที่ return value สำคัญ (standard library mark ไว้บางตัว เช่น errors.New และ fmt.Errorf) ถูกเรียกแล้วทิ้งผลลัพธ์:
errors.New("boom")main.go:9:2: result of errors.New call not usedhttpresponse — response จาก http.Get ถูกใช้ (ที่นี่คือ defer resp.Body.Close()) ก่อนเช็ก error ทำให้ request ที่ fail dereference *http.Response ที่เป็น nil:
resp, err := http.Get("http://example.com")defer resp.Body.Close()if err != nil { fmt.Println(err) return}main.go:10:8: using resp before checking for errorswaitgroup (Go 1.25) — wg.Add(1) ถูกเรียกจากข้างใน goroutine ที่มันควรจะ track ซึ่ง race กับ wg.Wait() ใน parent — Add ต้องเกิดก่อน go statement:
go func() { wg.Add(1) defer wg.Done()}()wg.Wait()main.go:8:9: WaitGroup.Add called from inside new goroutineprintf — format verb ไม่ตรงกับ type ของ argument:
fmt.Printf("%d\n", "hello")main.go:6:14: fmt.Printf format %d has arg "hello" of wrong type stringgo vet ./...go vet exit ด้วย non-zero status หากพบปัญหา ทำให้ fail CI pipeline ได้ง่าย มันยังรัน analyzer กลุ่มเล็ก ๆ ที่ตายตัวเหล่านี้อัตโนมัติก่อน go test และ go build ทุกครั้ง — hostport (Go 1.25) แนะนำ net.JoinHostPort แทน fmt.Sprintf("%s:%d", host, port) ซึ่งพังกับ IPv6 และ stdversion (Go 1.27) รายงานการเรียก API ของ standard library ที่ใหม่กว่าบรรทัด go ของ module
staticcheck และ golangci-lint v2
หัวข้อที่มีชื่อว่า “staticcheck และ golangci-lint v2”go vet ระมัดระวังโดยตั้งใจ — มันรายงานเฉพาะปัญหาที่พิสูจน์ได้ว่าน่าจะเป็น bug จริงเกือบแน่นอน staticcheck และ golangci-lint รัน analyzer มากกว่ามาก รวมถึงหลายตัวที่มีความเสี่ยง false-positive ที่ go vet หลีกเลี่ยง
staticcheck (honnef.co/go/tools/cmd/staticcheck) รันแบบ standalone และรายงานโค้ดของตัวเองที่ขึ้นต้นด้วย SA กับ bug printf และ unusedresult ตัวเดิมด้านบน ยืนยันแล้วจริง (staticcheck 2026.2.1):
main.go:6:13: Printf format %d has arg #1 of wrong type string (SA5009)main.go:9:2: New doesn't have side effects and its return value is ignored (SA4017)golangci-lint รวม linter หลายสิบตัว — รวมถึง go vet เอง, staticcheck และอีกมากมาย — ไว้หลังคำสั่งเดียวและ config file เดียว รูปแบบ config ของ version 2 (ยืนยันแล้วจริงกับ golangci-lint 2.13.2) เริ่มจาก:
version: "2"linters: default: standarddefault: standard เปิดชุด baseline ที่คัดสรรมาแล้ว (govet, staticcheck, unused และอื่น ๆ) แทนที่จะเปิดทุกตัว — ชุด default ของ golangci-lint v1 หายไปใน v2 และ default เป็นตัวเลือกที่ระบุชัดเจนแล้ว การรันกับ bug copylocks ด้านบน ยืนยันแล้วจริง ห่อ finding ของ go vet ด้วยชื่อ linter ที่รายงานและเพิ่ม issue จริงที่ไม่เกี่ยวข้องในไฟล์เดียวกัน:
main.go:12:8: copylocks: assignment copies lock value to s2: ...Safe contains sync.Mutex (govet)main.go:13:6: copylocks: assignment copies lock value to _: ...Safe contains sync.Mutex (govet)main.go:6:2: field mu is unused (unused)main.go:7:2: field count is unused (unused)suffix (govet) และ (unused) บอกว่า linter ตัวไหนอยู่เบื้องหลังแต่ละบรรทัด มีประโยชน์เมื่อต้องตัดสินใจว่าจะ suppress อะไรและแก้อะไร
govulncheck
หัวข้อที่มีชื่อว่า “govulncheck”govulncheck เทียบ dependency ของคุณ (แม่นยำกว่านั้นคือ function ที่โค้ดคุณเรียกจริง ๆ) กับฐานข้อมูล vulnerability ของ Go
govulncheck ./...ยืนยันแล้วจริงบน module ที่สะอาดไม่มี dependency ภายนอก:
No vulnerabilities found.
Your code is affected by 0 vulnerabilities.This scan also found 0 vulnerabilities in packages you import and 8vulnerabilities in modules you require, but your code doesn't appear to callthese vulnerabilities.ความแตกต่างในบรรทัดสุดท้ายนั้นคือประเด็นทั้งหมดของเครื่องมือนี้: การมี vulnerable function อยู่ที่ไหนสักแห่งใน dependency tree ไม่เหมือนกับโปรแกรมของคุณเรียกมัน govulncheck ทำ call-graph analysis เพื่อแยกสองอย่างนี้ออกจากกัน ซึ่งเป็นเหตุผลที่มันรายงานผลแบบ “ต้องแก้เดี๋ยวนี้” น้อยกว่า tool ที่แค่ “scan go.sum เทียบ CVE list” แบบไร้เดียงสามาก
go fix — modernizer (Go 1.26+)
หัวข้อที่มีชื่อว่า “go fix — modernizer (Go 1.26+)”go fix ถูกสร้างใหม่ใน Go 1.26 บน analysis framework เดียวกับ go vet แทน fixer ในอดีตไม่กี่ตัว ตอนนี้มี modernizer เป็นสิบ ๆ ตัว: analyzer ที่เขียน idiom แบบเก่าใหม่ให้เป็น standard library ปัจจุบัน
go fix ./... # apply every modernizer to the modulego fix -diff ./... # preview the rewrite without touching filesการเขียนใหม่ที่พบบ่อย: for loop ที่หาค่าต่ำสุดด้วยมือกลายเป็น min(...); sort.Slice กลายเป็น slices.Sort; for i := 0; i < n; i++ กลายเป็น for i := range n; สามบรรทัด wg.Add(1) / go / defer wg.Done() กลายเป็น wg.Go(...) (waitgroupgo); การ copy ตัวแปร loop แบบ x := x ถูกลบ Go 1.27 เพิ่ม atomictypes, embedlit, slicesbackward และ unsafefuncs ผู้เขียน library ยัง mark wrapper ด้วย //go:fix inline ได้ เพื่อให้ go fix inline การเรียกในทุกจุดใช้งาน — กลไกเดียวกับที่ standard library ใช้ย้าย caller ออกจาก helper ที่ deprecated
Build tags
หัวข้อที่มีชื่อว่า “Build tags”บรรทัด //go:build ที่ด้านบนของไฟล์ (ก่อน clause package ตามด้วยบรรทัดว่าง) บอก toolchain ให้รวมหรือตัดไฟล์นั้นตาม boolean expression ของ tags ยืนยันแล้วจริงกับสองไฟล์ที่ต่างกันแค่ build tag:
//go:build dev
package main
func banner() string { return "DEV BUILD" }//go:build !dev
package main
func banner() string { return "production" }go run . # productiongo run -tags dev . # DEV BUILDการใช้งานที่พบบ่อย: สลับ implementation จริงเป็น fake ตอน test, ตัดไฟล์เฉพาะ platform ออก (Go เอา tag จาก suffix ชื่อไฟล์แบบ _linux.go / _windows.go ให้อัตโนมัติด้วย) หรือ gate โค้ดที่ต้องการ Go version ใหม่กว่า floor ของ module
go generate
หัวข้อที่มีชื่อว่า “go generate”comment //go:generate mark คำสั่งให้รันเมื่อสั่ง — ไม่รันอัตโนมัติเด็ดขาด ยืนยันแล้วจริง:
//go:generate echo "generation would run here"go generate ./...# generation would run herego build และ go test ไม่เรียก go generate ให้คุณเลย มันต้องเป็นขั้นตอนที่ตั้งใจใน workflow หรือ CI ของคุณ ปกติใช้กับ String() method ที่ generate โดย stringer, โค้ด protobuf, หรือการ generate mock
-ldflags -X และ -trimpath
หัวข้อที่มีชื่อว่า “-ldflags -X และ -trimpath”-ldflags "-X pkg.var=value" ตั้งค่าตัวแปร string ระดับ package ตอน link time — วิธีมาตรฐานในการฝัง version string หรือ commit hash ลง binary โดยไม่ต้อง compile source ใหม่:
var version = "dev"
func main() { fmt.Println("version:", version) }ยืนยันแล้วจริง:
go build -o app . # version: devgo build -ldflags "-X main.version=1.2.3" -o app . # version: 1.2.3ตัวแปรต้องเป็น string ที่ไม่ initialize ตอน runtime (ไม่ใช่ const, ไม่ใช่ค่า default ที่คำนวณ) และ flag ต้องการ path เต็ม (main.version หรือ example.com/mod/pkg.Var สำหรับ package ที่ไม่ใช่ main)
-trimpath ลบ filesystem path ของเครื่อง local ออกจาก binary ที่ compile แล้ว (ไม่งั้นมันจะรั่วเข้าไปใน panic stack trace และ debug info) — ยืนยันแล้วจริงว่า build ผ่านโดยไม่มี error และเป็น practice มาตรฐานสำหรับ binary ที่คุณแจกจ่าย:
go build -trimpath -o app .Cross-compilation
หัวข้อที่มีชื่อว่า “Cross-compilation”การตั้ง GOOS และ GOARCH cross-compile สำหรับ platform อื่นได้เลย — ไม่ต้อง download toolchain แยก เพราะ Go compiler เป็น cross-compiler ตัวเดียวสำหรับทุก target ที่รองรับ ยืนยันแล้วจริง สร้าง binary Linux/arm64 จาก macOS/arm64:
GOOS=linux GOARCH=arm64 go build -o app-linux-arm64 .file app-linux-arm64# app-linux-arm64: ELF 64-bit LSB executable, ARM aarch64 ..., statically linked# Cross-compile a stripped, static Linux/amd64 binaryCGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o bin/server ./cmd/server-ldflags="-s -w" ลบ symbol table และ DWARF debug info ออก ลดขนาด binary CGO_ENABLED=0 รับประกัน binary ที่ static อย่างสมบูรณ์ — ไม่มี dynamic libc dependency และจำเป็นสำหรับ cross-compilation target ส่วนใหญ่ เพราะ cgo ต้องการ C cross-toolchain ที่ go build เฉย ๆ ไม่มี
go env อ่านและเขียน configuration ถาวรของ toolchain (เก็บนอก module ของคุณ ใน config file ต่อผู้ใช้) ตัวแปรที่สำคัญที่สุดในชีวิตประจำวัน:
| ตัวแปร | หน้าที่ |
|---|---|
GOPROXY | go get/go mod download ดึง module จากไหน (default https://proxy.golang.org,direct) |
GOPRIVATE | glob pattern ของ module path ที่จะดึงตรง ข้าม proxy และ checksum database — สำหรับ repository ภายใน/private |
GOSUMDB | checksum database ที่ใช้ verify การ download (default sum.golang.org); ตั้งเป็น off เฉพาะเมื่อคุณเชื่อ GOPROXY เต็มที่ |
GOFLAGS | flag ที่ต่อเข้ากับทุกคำสั่ง go โดยปริยาย เช่น GOFLAGS=-mod=mod |
GOTOOLCHAIN | ควบคุมการสลับ toolchain อัตโนมัติ (default auto) — ดู directive toolchain ในบท Modules |
go env GOPROXY GOPRIVATE GOSUMDB# https://proxy.golang.org,direct## sum.golang.orggo env -w GOFLAGS=-mod=mod เขียนค่าถาวร; go env GOPROXY เฉย ๆ แค่อ่านค่าปัจจุบัน
gopls และ goimports
หัวข้อที่มีชื่อว่า “gopls และ goimports”gopls คือ official Go language server — Go support ของ editor หลักทุกตัว (VS Code, Neovim, protocol เบื้องหลังของ GoLand) คุยกับ gopls สำหรับ autocomplete, go-to-definition, inline diagnostics และ refactoring มันรัน check สไตล์ go vet ต่อเนื่องขณะคุณพิมพ์ นี่คือเหตุผลที่ bug หลายตัวในบทนี้เห็นเป็นเส้นหยักสีแดงก่อนที่คุณจะรันคำสั่งใด ๆ ด้วยซ้ำ goimports คือ gofmt บวกการจัดการ import อัตโนมัติ: มันเพิ่ม import ที่ขาดและลบที่ไม่ใช้ editor ส่วนใหญ่รันมันแทน gofmt เฉย ๆ ตอน save
Profile-guided optimization (PGO)
หัวข้อที่มีชื่อว่า “Profile-guided optimization (PGO)”ตั้งแต่ Go 1.21 การวาง CPU profile ชื่อ default.pgo ไว้ใน directory ของ package main ทำให้ go build ใช้มันอัตโนมัติ: compiler inline มากขึ้นตาม hot path ที่ profile ระบุ ปกติเพิ่ม throughput ไม่กี่ percent โดยไม่ต้องแก้ source เลย generate profile จาก production workload ตัวแทน (-cpuprofile กลไกเดียวกับบท Benchmarks) commit default.pgo คู่กับ source แล้ว go build จะหยิบมันมาใช้โดยไม่ต้องใส่ flag เพิ่ม
go doc — เรียกดู documentation
หัวข้อที่มีชื่อว่า “go doc — เรียกดู documentation”go doc แสดง documentation สำหรับ package, type, function, หรือ method ใดก็ได้ — โดยไม่ต้องออกจาก terminal
go doc fmt # package overviewgo doc fmt.Println # specific functiongo doc net/http.Request # type in a sub-packagego doc -all net/http # every exported symbol in the packagego doc -ex strings.Cut # print the runnable examples (Go 1.27)สำหรับมุมมองแบบ browser รัน go doc -http (Go 1.25+) หรือเรียกดูที่ pkg.go.dev
รวมทุกอย่าง — CI pipeline ทั่วไป
หัวข้อที่มีชื่อว่า “รวมทุกอย่าง — CI pipeline ทั่วไป”test -z "$(gofmt -l .)" && \go vet ./... && \govulncheck ./... && \go test -race -coverprofile=coverage.out -covermode=atomic ./... && \go build ./...pipeline นี้ตรวจจับ formatting drift, static bugs (รวมถึง go vet analyzer หกตัวด้านบน), known vulnerability, data races และ compilation errors ก่อนที่ code จะถูก merge
Quality-gate pipeline
หัวข้อที่มีชื่อว่า “Quality-gate pipeline”flowchart TD A["source code"] --> B["gofmt -l (formatting check)"] B --> C["go vet (copylocks, lostcancel, printf, ...)"] C --> D["staticcheck / golangci-lint (broader analysis)"] D --> E["govulncheck (known CVEs actually called)"] E --> F["go test -race (behavior + data races)"] F --> G["go build (compiles to a binary)"]
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| สิ่งที่ได้ | ประโยชน์ | ต้นทุน |
|---|---|---|
go vet | ตรวจหา common mistakes ก่อน runtime, built-in, ไม่ต้อง config | ระมัดระวังโดยตั้งใจ — ใช้ staticcheck เพิ่มเพื่อความครอบคลุมมากขึ้น |
golangci-lint | linter หลายสิบตัวหลังคำสั่งเดียวและ config file เดียว | ต้องมี config file เพื่อปรับ (default: standard ของ v2 เป็นจุดเริ่มต้นที่สมเหตุสมผล); ช้ากว่า go vet เดี่ยว ๆ |
govulncheck | รู้ call-graph — flag เฉพาะสิ่งที่คุณเรียกจริง ไม่ใช่แค่สิ่งที่อยู่ใน go.sum | ทันสมัยเท่าที่ vulnerability database มี — CVE ใหม่ต้องใช้เวลากว่าจะปรากฏ |
go build -race | ตรวจหา race condition ใน binary | binary ช้าลง 2-10x — ไม่เหมาะ production build |
PGO (default.pgo) | throughput เพิ่มขึ้นไม่กี่ percent โดยไม่ต้องแก้ source | profile อาจล้าสมัยเมื่อ codebase เปลี่ยน — ต้อง regenerate เป็นระยะ |
gofmt (ผ่าน go fmt) | ไม่มีถกเถียงเรื่อง formatting, consistent ทั้ง ecosystem | ไม่มี config — ต้องยอมรับ canonical style |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”go fmtเป็นแค่ style เท่านั้น — gofmt เป็น de facto standard — project ส่วนใหญ่บังคับ gofmt ใน CIgo vetตรวจสอบครบทุก bug —go vetตรวจ subset ของ common mistakes ที่มันพิสูจน์ได้ด้วยความมั่นใจสูง — ใช้ staticcheck หรือ golangci-lint เพิ่มเพื่อความครอบคลุมมากขึ้นgo generateทำงานอัตโนมัติตอน build —go generateต้องรันเอง ไม่ได้รันตอนgo build— ต้อง explicit ใน Makefile หรือ CI- GOPATH ยังจำเป็น — Go modules (
go.mod) แทน GOPATH อย่างสมบูรณ์ตั้งแต่ Go 1.16 govulncheckรายงาน dependency ที่มี vulnerability แปลว่าต้อง patch ทันที — มันแยกรายงาน vulnerability ใน module ที่ require กับ module ที่ call-graph ของโค้ดคุณเข้าถึงจริง — เฉพาะอย่างหลังคือความเสี่ยงที่ active-ldflags -Xตั้งค่าตัวแปรอะไรก็ได้ รวมถึง constant — มันตั้งได้เฉพาะตัวแปรstringที่ไม่ initialize เท่านั้น;const, type ที่ไม่ใช่ string, หรือตัวแปรที่มี initializer แบบคำนวณ ล้วนไม่ได้รับผลกระทบแบบเงียบ ๆ
สิ่งที่เปลี่ยนใน toolchain, Go 1.25–1.27
go mod initเขียนบรรทัดgoเป็นเวอร์ชันจริงของ toolchain ที่รันมัน — ไม่ใช่ต่ำกว่าหนึ่ง minor version ยืนยันในเครื่องแล้ว: Go 1.26.5 เขียนgo 1.26.5; รันด้วย Go 1.27.0 จะเขียนgo 1.27.0ยกหรือตรึงบรรทัดนี้เองด้วยgo get [email protected]เมื่อต้องการ floor เฉพาะgo doc example.com/[email protected](Go 1.27) แสดง doc ของ module version ที่ระบุ และgo doc -exแสดงรายการ example ที่รันได้go doc -http(Go 1.25) เสิร์ฟ documentation ในเบราว์เซอร์ ไม่ต้องใช้pkgsiteสำหรับงานในเครื่องอีกแล้วgo tool pprof -http(Go 1.26) เปิดมาที่หน้า flame graph;go tool trace -http=:6060(Go 1.27) bind เฉพาะ localhost เว้นแต่ระบุ address เต็มgo version -m -json(Go 1.24) พิมพ์ build info เป็น JSON;go build -asan(Go 1.25) รายงาน memory leak ตอนจบโปรแกรมgo fix(Go 1.26) ถูกสร้างใหม่บน analysis framework ของgo vetพร้อม modernizer หลายสิบตัว; Go 1.27 เพิ่มatomictypes,embedlit,slicesbackwardและunsafefuncs- analyzer
waitgroupและhostportของgo vet(Go 1.25) และstdversionที่go testรันด้วยแล้ว (Go 1.27)
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”โค้ด generated compile ผ่าน ดังนั้นความเงียบของ compiler ไม่พิสูจน์อะไรเลย รัน pipeline ด้านบนก่อนเชื่อมัน และเช็กเรื่องพวกนี้เป็นพิเศษ:
- struct ที่มี
sync.Mutex,sync.WaitGroupหรือคล้ายกัน ถูก copy บ้างไหม — assign, ส่งแบบ value, return แบบ value, หรือวนใน slice element?copylocksของgo vetจับสิ่งนี้ได้ แต่เฉพาะถ้าคุณรันมันจริง wg.Add(1)ถูกเรียกจากข้างใน goroutine ที่มันควร track แทนที่จะเรียกก่อนgostatement หรือเปล่า? นี่คือ race จริงที่waitgroupจับได้ — และ pattern ที่โค้ด generated สร้างขึ้นบ่อยมากเพราะมันอ่านเรียงจากซ้ายไปขวาแบบธรรมชาติ- การเรียก
context.WithCancel/WithTimeout/WithDeadlineแต่ละครั้ง functioncancelที่ return มาถูกเรียก (ปกติผ่านdefer) หรือถูกทิ้งด้วย_? http.Response(หรือ.Bodyของมัน) ถูกอ่านหรือปิดก่อนเช็กerrorที่มาคู่กันหรือเปล่า?- config file อ้างว่า
version: "2"สำหรับgolangci-lintแต่มี option เฉพาะ v1 หรือกลับกัน — config ที่ no-op แบบเงียบ ๆ แทนที่จะ fail หรือเปล่า? govulncheckถูกรันจริงหรือเปล่า หรือ CI config ที่ generate มามีแค่go vetแล้วสมมติว่าเพียงพอแล้ว?
🐛 หา bug ในโค้ดนี้
func fetchAndLog(url string) { resp, err := http.Get(url) defer resp.Body.Close() if err != nil { log.Println("fetch failed:", err) return } log.Println("status:", resp.StatusCode)}เฉลย
defer resp.Body.Close() รันก่อนเช็ก err != nil แต่เมื่อ http.Get fail resp จะเป็น nil — ดังนั้น resp.Body จึง panic ด้วย nil pointer dereference บน error path ที่โค้ดนี้พยายามจะจัดการอย่างสวยงามเป๊ะ ๆ analyzer httpresponse ของ go vet จับสิ่งนี้ได้ตรง ๆ: using resp before checking for errors วิธีแก้คือเช็ก error ก่อน แล้วค่อย defer การปิดเมื่อรู้แล้วว่า resp ไม่ใช่ nil: ย้าย if err != nil { ...; return } ขึ้นไปอยู่เหนือ defer resp.Body.Close()