Ver3.5 ๐ Go ์ธ์ด ์ฝ๋ ํ์ง ๊ด๋ฆฌ์ golangci-lint๋ฅผ ํ์ฉํ ์ ์ ๋ถ์ ์ค์ ๊ฐ์ด๋

๐ Go ์ธ์ด ์ฝ๋ ํ์ง ๊ด๋ฆฌ์ golangci-lint๋ฅผ ํ์ฉํ ์ ์ ๋ถ์ ์ค์ ๊ฐ์ด๋
์ฝ๋ ํ๋ฆฌํฐ ์ฑ๊ธฐ๋ ๊ฑฐ ๊ท์ฐฎ๋ค๊ณ ? ์ด๊ฑฐ ์ฝ๊ณ ๋๋ฉด ์๊ฐ ๋ฐ๋๊ฑธ์ ใ ใ ์ง์ง๋ก์
์์งํ ๋งํด์, ์ฒ์ Go ๋ฐฐ์ธ ๋ "๊ทธ๋ฅ ๋์๊ฐ๋ฉด ๋๋ ๊ฑฐ ์๋?" ํ๋ ์๊ฐ ํ ๋ฒ์ฏค์ ํด๋ดค์ ๊ฑฐ์์ ใ ใ ๊ทผ๋ฐ ์ค๋ฌด ๋ค์ด๊ฐ๋ฉด ์๊ธฐ๊ฐ ์์ ๋ฌ๋ผ์ง๊ฑฐ๋ ์. ํ์์ด ๋ด ์ฝ๋ ๋ณด๊ณ "์ด๊ฒ ๋ญ์ผ..." ํ๋ ํ์ ์ง๋ ๊ฑฐ ์์ํด๋ด์. ์ง์ง ์๋ฆ ๋์์์.
์ฝ๋ ํ์ง(Code Quality)์ด๋ ๋จ์ํ "๋ฒ๊ทธ ์์ด ์คํ๋๋ค"๋ ์์ค์ ๋์ด์, ๊ฐ๋ ์ฑยท์ ์ง๋ณด์์ฑยท์ฑ๋ฅยท๋ณด์๊น์ง ์์ฐ๋ฅด๋ ๊ฐ๋ ์ด์์. Go ์ธ์ด๋ ํ์์ ์ผ๋ก ์ฌํํ๊ณ ๋ช ํํ ์ฝ๋๋ฅผ ์งํฅํ๋ ์ธ์ด์ธ๋ฐ, ๊ทธ ์ฒ ํ์ ์ ๋๋ก ์ด๋ฆฌ๋ ค๋ฉด ์ ์ ๋ถ์ ๋๊ตฌ๊ฐ ํ์์์.
Go ์ํ๊ณ์์ ์ ์ ๋ถ์ ๋๊ตฌ๋ ๊ต์ฅํ ํ๋ถํ๋ฐ์, ๋ฌธ์ ๋ ๋๊ตฌ๊ฐ ๋๋ฌด ๋ง๋ค๋ ๊ฒ์ด์์ ใ
ใ
golint, staticcheck, errcheck, govet, ineffassign... ์ด๊ฑธ ๋ค ๋ฐ๋ก๋ฐ๋ก ์ค์นํ๊ณ ์คํํ๋ฉด ์ง์ง ๋จธ๋ฆฌ ์ํ์. ๊ทธ๋์ ๋ฑ์ฅํ ๊ฒ ๋ฐ๋ก golangci-lint์์!
golangci-lint๋ Go ์ธ์ด๋ฅผ ์ํ ํตํฉ ๋ฆฐํฐ ๋ฌ๋(Aggregated Linter Runner)์์. ์ฝ๊ฒ ๋งํ๋ฉด ์์ญ ๊ฐ์ lint ๋๊ตฌ๋ฅผ ํ๋๋ก ๋ฌถ์ด์ ๋น ๋ฅด๊ฒ ์คํํด์ฃผ๋ ๋ฉํ ๋๊ตฌ์์. 2018๋ ์ ์ฒ์ ๋ฑ์ฅํ๊ณ , ์ง๊ธ์ Go ์ํ๊ณ์์ ์ฌ์ค์ ํ์ค ์ ์ ๋ถ์ ๋๊ตฌ๋ก ์๋ฆฌ์ก์์ด์.
๋ณ๋ ฌ ์คํ + ์บ์ฑ์ผ๋ก ๊ธฐ์กด ๋๊ตฌ ๋๋น 5~10๋ฐฐ ๋น ๋ฆ. ๋ํ ํ๋ก์ ํธ๋ ๊ฑฐ๋ฌํด์.
50๊ฐ ์ด์์ linter๋ฅผ ๋จ์ผ ๋ช ๋ น์ด๋ก ์คํ. ์ค์ ํ์ผ ํ๋๋ก ์ ๋ถ ๊ด๋ฆฌํด์.
YAML ์ค์ ์ผ๋ก linter๋ณ ์ธ๋ฐํ ์ปค์คํฐ๋ง์ด์ง ๊ฐ๋ฅ. ํ๋ก์ ํธ ํน์ฑ์ ๋ง๊ฒ ์กฐ์ ํด์.
| Linter ์ด๋ฆ | ์ญํ | ๊ธฐ๋ณธ ํ์ฑํ |
|---|---|---|
errcheck |
์๋ฌ ๋ฐํ๊ฐ ๋ฌด์ ์ฌ๋ถ ๊ฒ์ฌ | โ ๊ธฐ๋ณธ ํ์ฑ |
gosimple |
์ฝ๋ ๋จ์ํ ๊ฐ๋ฅ ๋ถ๋ถ ์ ์ | โ ๊ธฐ๋ณธ ํ์ฑ |
govet |
Go ๊ณต์ vet ๋๊ตฌ (์์ฌ์ค๋ฌ์ด ์ฝ๋ ํจํด) | โ ๊ธฐ๋ณธ ํ์ฑ |
ineffassign |
์ฌ์ฉ๋์ง ์๋ ๋ณ์ ํ ๋น ๊ฐ์ง | โ ๊ธฐ๋ณธ ํ์ฑ |
staticcheck |
๊ณ ๊ธ ์ ์ ๋ถ์ (deprecated API, ๋ฒ๊ทธ ํจํด ๋ฑ) | โ ๊ธฐ๋ณธ ํ์ฑ |
unused |
์ฌ์ฉ๋์ง ์๋ ์ฝ๋ ๊ฐ์ง | โ ๊ธฐ๋ณธ ํ์ฑ |
gofmt |
Go ๊ณต์ ํฌ๋งทํ ๊ท์น ์ค์ ์ฌ๋ถ | โ ์ ํ ํ์ฑ |
goimports |
import ์ ๋ ฌ ๋ฐ ๋ฏธ์ฌ์ฉ import ๊ฒ์ฌ | โ ์ ํ ํ์ฑ |
gosec |
๋ณด์ ์ทจ์ฝ์ ํจํด ๊ฒ์ฌ | โ ์ ํ ํ์ฑ |
cyclop |
์ํ ๋ณต์ก๋(Cyclomatic Complexity) ์ธก์ | โ ์ ํ ํ์ฑ |
dupl |
์ค๋ณต ์ฝ๋ ๊ฐ์ง | โ ์ ํ ํ์ฑ |
funlen |
ํจ์ ๊ธธ์ด ์ ํ ๊ฒ์ฌ | โ ์ ํ ํ์ฑ |
golangci-lint ์ค์น ๋ฐฉ๋ฒ์ ์ฌ๋ฌ ๊ฐ์ง์ธ๋ฐ, ๊ณต์ ๊ถ์ฅ ๋ฐฉ๋ฒ์ ๋ฐ์ด๋๋ฆฌ ์ง์ ์ค์น์์. go install๋ก ์ค์นํ๋ ๊ฑด ๊ณต์์ ์ผ๋ก ๋น๊ถ์ฅ์ด์์ (๋ฒ์ ๋ถ์ผ์น ๋ฌธ์ ๋๋ฌธ).
macOS (Homebrew)
brew install golangci-lint
brew upgrade golangci-lint
Linux / macOS (๊ณต์ ์คํฌ๋ฆฝํธ)
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh \
| sh -s -- -b $(go env GOPATH)/bin v1.57.2
Windows (Scoop)
scoop install golangci-lint
Docker
docker run --rm -v $(pwd):/app -w /app \
golangci/golangci-lint:v1.57.2 golangci-lint run
์ค์น ์๋ฃ ํ ํ๋ก์ ํธ ๋ฃจํธ์์ ๊ทธ๋ฅ ์ด๋ ๊ฒ ์น๋ฉด ๋ผ์:
golangci-lint run
๊ธฐ๋ณธ ์ค์ ์ผ๋ก ํ์ฌ ๋๋ ํ ๋ฆฌ์ ๋ชจ๋ Go ํ์ผ์ ๋ถ์ํด์. ํน์ ๋๋ ํ ๋ฆฌ๋ง ๋ถ์ํ๊ณ ์ถ์ผ๋ฉด:
# ํน์ ํจํค์ง๋ง ๋ถ์
golangci-lint run ./internal/...
# ํน์ ํ์ผ๋ง ๋ถ์
golangci-lint run ./cmd/main.go
# ํ์์์ ์ค์ (๋ํ ํ๋ก์ ํธ์ฉ)
golangci-lint run --timeout 5m ./...
--new-from-rev ์ต์
์ผ๋ก ์๋ก ์ถ๊ฐ๋ ์ฝ๋๋ง ๊ฒ์ฌํ๋ ๊ฒ๋ ์ข์ ์ ๋ต์ด์์.
golangci-lint์ ์ง์ง ํ์๋ ์ค์ ํ์ผ์์ ๋์์. ํ๋ก์ ํธ ๋ฃจํธ์ .golangci.yml ํ์ผ์ ๋ง๋ค๋ฉด ํ ์ ์ฒด๊ฐ ๋์ผํ lint ๊ท์น์ ๊ณต์ ํ ์ ์์ด์. ์ด๊ฒ ์ง์ง ์ค์ํ ๊ฑฐ์์ - ๊ฐ์ธ ์ทจํฅ์ด ์๋๋ผ ํ ์ปจ๋ฒค์
์ ์ฝ๋๋ก ๊ฐ์ ํ๋ ๊ฑฐ๊ฑฐ๋ ์.
run:
timeout: 5m
go: '1.21'
tests: true
skip-dirs:
- vendor
- third_party
linters:
enable-all: false
enable:
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- unused
- gofmt
- goimports
- gosec
- cyclop
- funlen
- gocognit
- misspell
- revive
disable:
- deadcode # unused๋ก ๋์ฒด๋จ
linters-settings:
cyclop:
max-complexity: 10
funlen:
lines: 80
statements: 50
gocognit:
min-complexity: 15
gosec:
excludes:
- G104 # ํน์ ๊ท์น ์ ์ธ
revive:
rules:
- name: exported
severity: warning
misspell:
locale: US
issues:
exclude-rules:
- path: _test\.go
linters:
- funlen
- gosec
max-issues-per-linter: 50
max-same-issues: 10
new: false
์ ์ฒด ์คํ ํ๊ฒฝ์ ์ค์ ํด์.
timeout์ ๋ถ์ ์ ํ ์๊ฐ, go๋ Go ๋ฒ์ ์ง์ , skip-dirs๋ ๋ถ์ ์ ์ธ ๋๋ ํ ๋ฆฌ์์. vendor ๋๋ ํ ๋ฆฌ๋ ๊ฑฐ์ ํญ์ ์ ์ธํ๋ ๊ฒ ๊ตญ๋ฃฐ์ด์์ ใ
ใ
์ด๋ค linter๋ฅผ ํ์ฑํ/๋นํ์ฑํํ ์ง ๊ฒฐ์ ํด์.
enable-all: true๋ก ์ ๋ถ ์ผ๊ณ ์ผ๋ถ๋ง ๋๋ ๋ฐฉ์๋ ์์ง๋ง, ์ฒ์์ ํ์ํ ๊ฒ๋ง ์ผ๋ ๊ฒ ์ ์ ๊ฑด๊ฐ์ ์ข์์ ใ
ใ
๊ฐ linter์ ์ธ๋ถ ํ๋ผ๋ฏธํฐ๋ฅผ ์กฐ์ ํด์. ์๋ฅผ ๋ค์ด
funlen์ ์ต๋ ์ค ์, cyclop์ ์ต๋ ๋ณต์ก๋ ๋ฑ์ ํ๋ก์ ํธ ํน์ฑ์ ๋ง๊ฒ ์ค์ ํ ์ ์์ด์.
ํน์ ํ์ผ์ด๋ ํจํด์์ ํน์ linter๋ฅผ ์ ์ธํ๋ ๊ท์น์ ์ ์ํด์. ํ ์คํธ ํ์ผ์์
funlen์ ๋๋ ๊ฑด ๊ฑฐ์ ๋ชจ๋ ํ์ด ํ๋ ์ค์ ์ด์์.
enable-all: true๋ ์ ์คํ๊ฒ ์จ์ผ ํด์. ์ ๋ฒ์ ์์ linter๊ฐ ์ถ๊ฐ๋๋ฉด ๊ฐ์๊ธฐ CI๊ฐ ๊นจ์ง ์ ์๊ฑฐ๋ ์. ๋ช
์์ ์ผ๋ก ํ์ํ linter๋ง enableํ๋ ๊ฒ ๋ ์์ ์ ์ด์์.
์ด๋ก ๋ง ๋ณด๋ฉด ์ง๋ฃจํ์์์ ใ ใ ์ค์ ์ฝ๋ ์์ ๋ก ๊ฐ linter๊ฐ ๋ญ ์ก์๋ด๋์ง ๋ด์!
Go์์ ๊ฐ์ฅ ํํ ์ค์ ์ค ํ๋๊ฐ ์๋ฌ ๋ฐํ๊ฐ์ ๋ฌด์ํ๋ ๊ฑฐ์์.
package main
import (
"os"
"fmt"
)
func main() {
// โ ์๋ฌ ๋ฌด์! errcheck๊ฐ ๊ฒฝ๊ณ ๋ฐ์
os.Remove("/tmp/test.txt")
f, _ := os.Open("config.json") // โ ์๋ฌ๋ฅผ _๋ก ๋ฌด์
fmt.Println(f)
}
package main
import (
"fmt"
"log"
"os"
)
func main() {
// โ
์๋ฌ๋ฅผ ๋ช
์์ ์ผ๋ก ์ฒ๋ฆฌ
if err := os.Remove("/tmp/test.txt"); err != nil {
log.Printf("ํ์ผ ์ญ์ ์คํจ: %v", err)
}
f, err := os.Open("config.json")
if err != nil {
log.Fatalf("ํ์ผ ์ด๊ธฐ ์คํจ: %v", err)
}
defer f.Close()
fmt.Println(f.Name())
}
package main
import "sync"
func badExample() {
var mu sync.Mutex
// โ SA2001: sync.Mutex๋ฅผ ๊ฐ์ผ๋ก ๋ณต์ฌ (Lock ์ํ ๋ณต์ฌ ์ํ)
mu2 := mu
_ = mu2
// โ S1039: ๋ถํ์ํ fmt.Sprintf ์ฌ์ฉ
// s := fmt.Sprintf("%s", "hello") // ๊ทธ๋ฅ s := "hello" ์ฐ๋ฉด ๋จ
// โ SA4006: ๋ณ์ ํ ๋น ํ ์ฌ์ฉ ์ ํจ
x := 42
x = 100 // ์ด์ 42๋ ์ฌ์ฉ๋ ์ ์์
_ = x
}
package main
import (
"fmt"
"os/exec"
"net/http"
)
func vulnerableCode(userInput string) {
// โ G204: ์ฌ์ฉ์ ์
๋ ฅ์ผ๋ก ๋ช
๋ น์ด ์คํ (Command Injection ์ํ)
cmd := exec.Command("sh", "-c", userInput)
cmd.Run()
// โ G107: ๋ณ์๋ก URL ๊ตฌ์ฑ (SSRF ์ํ)
resp, _ := http.Get(fmt.Sprintf("http://%s/api", userInput))
_ = resp
}
์ํ ๋ณต์ก๋(Cyclomatic Complexity)๋ ์ฝ๋์ ๋ถ๊ธฐ ๊ฒฝ๋ก ์๋ฅผ ๋ํ๋ด๋ ์งํ์์. ๋์์๋ก ํ ์คํธํ๊ธฐ ์ด๋ ต๊ณ ๋ฒ๊ทธ๊ฐ ์๊ธฐ๊ธฐ ์ฌ์์.
// โ ๋ณต์ก๋ 12 - cyclop ๊ฒฝ๊ณ ๋ฐ์ (๊ธฐ๋ณธ ์๊ณ๊ฐ 10 ์ด๊ณผ)
func processOrder(order Order) error {
if order.ID == "" {
return errors.New("ID ์์")
}
if order.Amount <= 0 {
return errors.New("๊ธ์ก ์ค๋ฅ")
}
if order.UserID == "" {
return errors.New("์ฌ์ฉ์ ์์")
}
switch order.Status {
case "pending":
if order.Amount > 1000000 {
if order.VIPUser {
// ์ฒ๋ฆฌ ๋ก์ง...
} else {
// ๋ค๋ฅธ ์ฒ๋ฆฌ...
}
}
case "processing":
// ...
case "completed":
// ...
default:
return errors.New("์ ์ ์๋ ์ํ")
}
return nil
}
// โ
ํจ์ ๋ถ๋ฆฌ๋ก ๋ณต์ก๋ ๋ฎ์ถ๊ธฐ
func validateOrder(order Order) error {
if order.ID == "" { return errors.New("ID ์์") }
if order.Amount <= 0 { return errors.New("๊ธ์ก ์ค๋ฅ") }
if order.UserID == "" { return errors.New("์ฌ์ฉ์ ์์") }
return nil
}
func processOrderStatus(order Order) error {
// ์ํ๋ณ ์ฒ๋ฆฌ ๋ก์ง ๋ถ๋ฆฌ
return nil
}
revive๋ deprecated๋ golint๋ฅผ ๋์ฒดํ๋ linter์์. Go ์ฝ๋ฉ ์ปจ๋ฒค์
์ค์ ์ฌ๋ถ๋ฅผ ๊ฒ์ฌํด์.
// โ exported ํจ์์ ์ฃผ์ ์์
func ProcessData(data []byte) error {
return nil
}
// โ ์๋ฌ ํ์
์ด๋ฆ์ด Error๋ก ๋๋์ง ์์
type MyErr struct {
msg string
}
// โ
์ฌ๋ฐ๋ฅธ ๋ฐฉ์
// ProcessData๋ ๋ฐ์ดํธ ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํ๊ณ ์๋ฌ๋ฅผ ๋ฐํํฉ๋๋ค.
func ProcessData(data []byte) error {
return nil
}
// โ
์๋ฌ ํ์
๋ช
๋ช
๊ท์น ์ค์
type ProcessError struct {
msg string
}
func (e *ProcessError) Error() string {
return e.msg
}
golangci-lint๋ฅผ ๋ก์ปฌ์์๋ง ์ฐ๋ฉด ๋ฐ์ชฝ์ง๋ฆฌ์์. ์ง์ง ํ์๋ CI/CD ํ์ดํ๋ผ์ธ์ ํตํฉํ์ ๋ ๋์์. ํ์ ๋๊ตฐ๊ฐ๊ฐ lint ๊ฒ์ฌ ์ ํ๊ณ PR ์ฌ๋ ค๋ ์๋์ผ๋ก ๋ง์์ฃผ๊ฑฐ๋ ์ ใ ใ ์ด๊ฒ ์ง์ง ํ ์ฝ๋ ํ์ง ๊ด๋ฆฌ์ ํต์ฌ์ด์์.
name: golangci-lint
on:
push:
branches:
- main
- develop
pull_request:
branches:
- main
- develop
permissions:
contents: read
pull-requests: read
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.21'
cache: false
- name: golangci-lint ์คํ
uses: golangci/golangci-lint-action@v4
with:
version: v1.57.2
args: --timeout=5m
# ์ค์ ํ์ผ ๊ฒฝ๋ก (๊ธฐ๋ณธ๊ฐ: .golangci.yml)
# config: .golangci.yml
lint:
image: golangci/golangci-lint:v1.57.2
stage: test
script:
- golangci-lint run --timeout 5m ./...
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "main"
pipelines:
pull-requests:
'**':
- step:
name: Go Lint
image: golangci/golangci-lint:v1.57.2
script:
- golangci-lint run --timeout 5m ./...
golangci-lint-action์ ์ฐ๋ฉด ์๋์ผ๋ก ์บ์ฑ์ด ์ ์ฉ๋ผ์. ๋ ๋ฒ์งธ ์คํ๋ถํฐ๋ ํจ์ฌ ๋นจ๋ผ์ ธ์. ์บ์ ํค๋ Go ๋ฒ์ ๊ณผ ์ค์ ํ์ผ ํด์๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์๋ ์์ฑ๋ผ์.
์ด๋ก ์ ์๊ฒ ๋๋ฐ ์ค์ ๋ก ํ์ ๋์ ํ๋ ค๋ฉด ์ด๋ป๊ฒ ํด์ผ ํ ๊น์? ๊ฒฝํ ๊ธฐ๋ฐ์ผ๋ก ๋จ๊ณ๋ณ ์ ๋ต์ ์๋ ค๋๋ฆด๊ฒ์.
๊ธฐ์กด ํ๋ก์ ํธ์ ์ฒ์ ์ ์ฉํ๋ฉด ์๋ฐฑ ๊ฐ์ ๊ฒฝ๊ณ ๊ฐ ์์์ง ์ ์์ด์. ๋จผ์
golangci-lint run --out-format json | jq '.Issues | group_by(.FromLinter) | map({linter: .[0].FromLinter, count: length})'๋ก linter๋ณ ์ด์ ์๋ฅผ ํ์
ํด์.
์ฒ์๋ถํฐ ๋ชจ๋ linter๋ฅผ ์ผ๋ฉด ํ์๋ค์ด ๋ฐ๋ฐํ ์ ์์ด์ ใ ใ ๊ธฐ๋ณธ linter(errcheck, govet, staticcheck)๋ถํฐ ์์ํด์ 2์ฃผ๋ง๋ค ํ๋์ฉ ์ถ๊ฐํ๋ ๋ฐฉ์์ด ํ์ค์ ์ด์์.
--new-from-rev=HEAD~1 ์ต์
์ผ๋ก ์๋ก ์ถ๊ฐ๋ ์ฝ๋๋ง ๊ฒ์ฌํ๊ฑฐ๋, //nolint:lintername ์ฃผ์์ผ๋ก ํน์ ๋ผ์ธ์ ์ผ์์ ์ผ๋ก ์ ์ธํ ์ ์์ด์. ๋จ, nolint ๋จ์ฉ์ ๊ธ๋ฌผ!
CI์์๋ง ๊ฒ์ฌํ๋ฉด ๋ฆ์ด์. ๋ก์ปฌ์์ ์ปค๋ฐ ์ ์ ์๋์ผ๋ก lint๋ฅผ ์คํํ๋ฉด ํจ์ฌ ๋น ๋ฅธ ํผ๋๋ฐฑ ๋ฃจํ๋ฅผ ๋ง๋ค ์ ์์ด์.
repos:
- repo: https://github.com/dnephin/pre-commit-golang
rev: v0.5.1
hooks:
- id: golangci-lint
๋๋ ์ง์ git hook์ผ๋ก ์ค์ :
#!/bin/sh
echo "๐ golangci-lint ์คํ ์ค..."
golangci-lint run ./...
if [ $? -ne 0 ]; then
echo "โ lint ์ค๋ฅ๊ฐ ์์ด์. ์์ ํ ๋ค์ ์ปค๋ฐํด์ฃผ์ธ์."
exit 1
fi
echo "โ
lint ํต๊ณผ!"
์ด์ฉ ์ ์์ด lint ๊ท์น์ ๋ฌด์ํด์ผ ํ ๋๋ //nolint ์ฃผ์์ ์จ์. ๊ทผ๋ฐ ์ด๊ฑฐ ๋จ์ฉํ๋ฉด lint ์ฐ๋ ์๋ฏธ๊ฐ ์์ด์ง๋๊น ์ฃผ์ํด์!
// โ
ํน์ linter๋ง ์ ์ธ (์ด์ ๋ช
์ ํ์!)
func legacyFunction() error {
result, _ := riskyOperation() //nolint:errcheck // ๋ ๊ฑฐ์ ์ฝ๋, ์ถํ ์์ ์์ (#123)
return result
}
// โ
์ฌ๋ฌ linter ๋์ ์ ์ธ
var globalVar = "test" //nolint:gochecknoglobals,revive // ์ค์ ๊ฐ์ผ๋ก ์ฌ์ฉ
// โ ์ด์ ์๋ nolint (๋์ ์)
result, _ := riskyOperation() //nolint
//nolint ์ฃผ์์๋ ๋ฐ๋์ ์ด๋ค linter๋ฅผ ์ ์ธํ๋์ง์ ์ด์ ๋ฅผ ๋ช
์ํ๋ ๊ฒ ํ ์ปจ๋ฒค์
์ผ๋ก ์ ํ๋ ๊ฒ ์ข์์. ์ด์ ์๋ nolint๋ ์ฝ๋ ๋ฆฌ๋ทฐ์์ ๋ฐ๋์ ์ง์ ๋ฐ์์ผ ํด์.
golangci-lint๊ฐ ์ก์๋ด๋ ์ด์๋ค์ ์นดํ ๊ณ ๋ฆฌ๋ณ๋ก ์ดํดํ๋ฉด ๋ ํจ๊ณผ์ ์ผ๋ก ํ์ฉํ ์ ์์ด์.
| ์ด์ ์ ํ | ๋ฐ์ ์์ธ | ํด๊ฒฐ ๋ฐฉ๋ฒ |
|---|---|---|
SA1006 |
Printf์ ํฌ๋งท ์์ด ๋ณ์ ์ง์ ์ ๋ฌ | fmt.Println(x) ๋๋ fmt.Printf("%v", x) ์ฌ์ฉ |
G401 |
MD5/SHA1 ๊ฐ์ ์ฝํ ํด์ ์ฌ์ฉ | SHA-256 ์ด์ ์ฌ์ฉ, ๋น๋ฐ๋ฒํธ๋ bcrypt ์ฌ์ฉ |
bodyclose |
HTTP ์๋ต Body๋ฅผ Closeํ์ง ์์ | defer resp.Body.Close() ์ถ๊ฐ |
contextcheck |
context.Background() ๋จ์ฉ | ์์ ์ปจํ ์คํธ๋ฅผ ์ ํํด์ ์ฌ์ฉ |
wrapcheck |
์ธ๋ถ ํจํค์ง ์๋ฌ๋ฅผ wrapํ์ง ์์ | fmt.Errorf("...: %w", err) ์ฌ์ฉ |
๊ธฐ๋ณธ ์ ๊ณต linter๋ก ๋ถ์กฑํ๋ค๋ฉด? ์ง์ ๋ง๋ค ์๋ ์์ด์! golangci-lint๋ ํ๋ฌ๊ทธ์ธ ์์คํ ์ ์ง์ํด์. ํ์ฌ ๋ด๋ถ ์ฝ๋ฉ ๊ท์น์ linter๋ก ๋ง๋ค์ด์ ๊ฐ์ ํ ์ ์์ด์.
package main
import (
"go/ast"
"golang.org/x/tools/go/analysis"
)
// ์ปค์คํ
๋ถ์๊ธฐ ์ ์
var Analyzer = &analysis.Analyzer{
Name: "mycompanyrule",
Doc: "ํ์ฌ ๋ด๋ถ ๊ท์น: ํน์ ํจ์ ์ฌ์ฉ ๊ธ์ง",
Run: run,
}
func run(pass *analysis.Pass) (interface{}, error) {
for _, file := range pass.Files {
ast.Inspect(file, func(n ast.Node) bool {
// ํน์ ํจ์ ํธ์ถ ํจํด ๊ฐ์ง
call, ok := n.(*ast.CallExpr)
if !ok {
return true
}
sel, ok := call.Fun.(*ast.SelectorExpr)
if !ok {
return true
}
// "deprecated.OldFunction" ํธ์ถ ๊ฐ์ง
if sel.Sel.Name == "OldFunction" {
pass.Reportf(call.Pos(),
"OldFunction์ deprecated์
๋๋ค. NewFunction์ ์ฌ์ฉํ์ธ์.")
}
return true
})
}
return nil, nil
}
go/analysis ํจํค์ง๋ฅผ ์ดํดํด์ผ ํด์. ์ฒ์์ ์ง์
์ฅ๋ฒฝ์ด ์์ง๋ง, ํ ๋ฒ ์ตํ๋ฉด ํ ์ ์ฒด์ ์ฝ๋ ํ์ง์ ํ๊ธฐ์ ์ผ๋ก ๋์ผ ์ ์์ด์. ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ Go ๊ฐ๋ฐ ์ ๋ฌธ๊ฐ๋ฅผ ์ฐพ์ ๋์๋ฐ๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์์!
๋ณด์์ ๋์ค์ ์๊ฐํ๋ฉด ๋ฆ์ด์. gosec์ OWASP ๊ฐ์ด๋๋ผ์ธ ๊ธฐ๋ฐ์ผ๋ก Go ์ฝ๋์ ๋ณด์ ์ทจ์ฝ์ ์ ์ฐพ์์ค์. ์ค๋ฌด์์ ์ง์ง ์ค์ํ linter์์.
| ๊ท์น ID | ์ค๋ช | ์ํ๋ |
|---|---|---|
G101 |
ํ๋์ฝ๋ฉ๋ ์๊ฒฉ์ฆ๋ช ๊ฐ์ง | ๐ด ๋์ |
G201 |
SQL ์ฟผ๋ฆฌ ํฌ๋งท ๋ฌธ์์ด ์ฌ์ฉ (SQL Injection) | ๐ด ๋์ |
G204 |
์ฌ์ฉ์ ์ ๋ ฅ์ผ๋ก ๋ช ๋ น์ด ์คํ (Command Injection) | ๐ด ๋์ |
G304 |
ํ์ผ ๊ฒฝ๋ก์ ์ฌ์ฉ์ ์ ๋ ฅ ์ฌ์ฉ (Path Traversal) | ๐ ์ค๊ฐ |
G401 |
์ฝํ ์ํธํ ์๊ณ ๋ฆฌ์ฆ ์ฌ์ฉ (MD5, SHA1) | ๐ ์ค๊ฐ |
G501 |
์์ ํ์ง ์์ ํด์ ํจ์ import | ๐ ์ค๊ฐ |
G601 |
๋ฃจํ ๋ณ์ ์ฃผ์ ์ฐธ์กฐ (Go 1.22 ์ด์ ) | ๐ก ๋ฎ์ |
package main
import (
"crypto/md5" // G501: ์์ ํ์ง ์์ ํด์
"database/sql"
"fmt"
"os"
)
const apiKey = "sk-1234567890abcdef" // G101: ํ๋์ฝ๋ฉ๋ ์๊ฒฉ์ฆ๋ช
!
func getUserData(db *sql.DB, userID string) {
// G201: SQL Injection ์ทจ์ฝ์ !
query := fmt.Sprintf("SELECT * FROM users WHERE id = '%s'", userID)
db.Query(query)
}
func readUserFile(filename string) {
// G304: Path Traversal ์ทจ์ฝ์ !
f, _ := os.Open("/data/" + filename)
_ = f
}
func hashPassword(password string) []byte {
// G401: MD5๋ ๋ณด์์ ์ทจ์ฝํ ํด์ ์๊ณ ๋ฆฌ์ฆ
h := md5.New()
h.Write([]byte(password))
return h.Sum(nil)
}
G401์ ์ ์ธํ ์ ์์ด์. ํ์ง๋ง ์ ์ธํ ๋๋ ๋ฐ๋์ ํ ๋ฆฌ๋ทฐ๋ฅผ ๊ฑฐ์ณ์ผ ํด์.
๊ธฐ๋ณธ ํ ์คํธ ์ถ๋ ฅ ์ธ์๋ ๋ค์ํ ํ์์ผ๋ก ๊ฒฐ๊ณผ๋ฅผ ๋ฐ์ ์ ์์ด์. ํนํ CI ํ๊ฒฝ์ด๋ IDE ํตํฉ ์ ์ ์ฉํด์.
# ๊ธฐ๋ณธ ํ
์คํธ ์ถ๋ ฅ
golangci-lint run
# JSON ํ์ (ํ์ฑ ์ฉ์ด)
golangci-lint run --out-format json
# GitHub Actions ์ด๋
ธํ
์ด์
ํ์
golangci-lint run --out-format github-actions
# JUnit XML (Jenkins ๋ฑ CI ๋๊ตฌ ์ฐ๋)
golangci-lint run --out-format junit-xml > lint-report.xml
# ์ฌ๋ฌ ํ์ ๋์ ์ถ๋ ฅ
golangci-lint run \
--out-format "github-actions,json:lint-results.json"
# ์์ ์๋ ์ถ๋ ฅ (๋ก๊ทธ ํ์ผ ์ ์ฅ์ฉ)
golangci-lint run --no-config --color never
VSCode์์ golangci-lint๋ฅผ ๋ฐ๋ก ์ฌ์ฉํ๋ ค๋ฉด Go ํ์ฅ(golang.go)์ ์ค์ ์์:
{
"go.lintTool": "golangci-lint",
"go.lintFlags": [
"--fast",
"--config=${workspaceFolder}/.golangci.yml"
],
"go.lintOnSave": "package"
}
GoLand(JetBrains)์์๋ Settings โ Go โ Go Linter์์ golangci-lint๋ฅผ ์ ํํ๋ฉด ๋ผ์. ์ ์ฅํ ๋๋ง๋ค ์๋์ผ๋ก lint ๊ฒฐ๊ณผ๋ฅผ ๋ณด์ฌ์ค์ ใ ใ ์ง์ง ํธํด์.
์ง๊ธ๊น์ง ๋ฐฐ์ด ๋ด์ฉ์ ์ข ํฉํด์ ์ค์ ํ๋ก๋์ ํ๋ก์ ํธ์ ์ธ ์ ์๋ ์์ฑํ ์ค์ ์ ๋ณด์ฌ๋๋ฆด๊ฒ์. ์ฌ๋ฅ๋ท์ฒ๋ผ ์ค์ ์๋น์ค๋ฅผ ์ด์ํ๋ ํ์์ ์ธ ์ ์๋ ์์ค์ด์์!
run:
timeout: 10m
go: '1.21'
tests: true
build-tags:
- integration
skip-dirs:
- vendor
- third_party
- generated
skip-files:
- ".*\\.pb\\.go$" # protobuf ์์ฑ ํ์ผ ์ ์ธ
- ".*\\.gen\\.go$" # ์๋ ์์ฑ ํ์ผ ์ ์ธ
output:
format: colored-line-number
print-issued-lines: true
print-linter-name: true
sort-results: true
linters:
enable:
# ๋ฒ๊ทธ ๊ฐ์ง
- errcheck
- govet
- staticcheck
- ineffassign
- unused
# ์ฝ๋ ์คํ์ผ
- gofmt
- goimports
- revive
- misspell
# ๋ณด์
- gosec
- bodyclose
- sqlclosecheck
# ๋ณต์ก๋
- cyclop
- funlen
- gocognit
# ์ฑ๋ฅ
- prealloc
# ์๋ฌ ์ฒ๋ฆฌ
- wrapcheck
- errorlint
# ๊ธฐํ
- godot
- whitespace
linters-settings:
errcheck:
check-type-assertions: true
check-blank: true
govet:
enable-all: true
cyclop:
max-complexity: 10
package-average: 0.0
skip-tests: true
funlen:
lines: 100
statements: 60
gocognit:
min-complexity: 20
gosec:
severity: medium
confidence: medium
excludes:
- G115 # ํน์ ํ๊ฒฝ์์ ๋ถํ์ํ ๊ท์น
revive:
min-confidence: 0.8
rules:
- name: exported
severity: warning
- name: var-naming
severity: warning
- name: package-comments
severity: warning
wrapcheck:
ignoreSigs:
- .Errorf(
- errors.New(
- errors.Unwrap(
misspell:
locale: US
ignore-words:
- someword
goimports:
local-prefixes: github.com/mycompany/myproject
issues:
exclude-rules:
# ํ
์คํธ ํ์ผ ์์ธ
- path: _test\.go
linters:
- funlen
- gocognit
- cyclop
- gosec
- wrapcheck
# main ํจํค์ง ์์ธ
- path: cmd/
linters:
- wrapcheck
# ํน์ ํจํด ์ ์ธ
- text: "weak cryptographic primitive"
linters:
- gosec
path: internal/checksum/
exclude-use-default: false
max-issues-per-linter: 100
max-same-issues: 20
new: false
fix: false
max-issues-per-linter๋ฅผ ๋๊ฒ ์ค์ ํ๊ณ , ์ ์ง์ ์ผ๋ก ์ด์๋ฅผ ์ค์ฌ๋๊ฐ๋ ๋ฐฉ์์ ์ถ์ฒํด์.
golangci-lint๋ ๋๊ตฌ์ผ ๋ฟ์ด์์. ์ง์ง ์ค์ํ ๊ฑด ์ฝ๋ ํ์ง์ ์ง์์ ์ผ๋ก ๊ฐ์ ํ๋ ค๋ ํ ๋ฌธํ์์. lint ๊ฒฝ๊ณ ๋ฅผ ๋ฌด์ํ๊ฑฐ๋ nolint๋ก ๋๋ฐฐํ๋ฉด ์๋ฌด ์๋ฏธ ์๊ฑฐ๋ ์ ใ
ใ
์ฒ์์ lint ๊ฒฝ๊ณ ๊ณ ์น๋ ๊ฒ ๊ท์ฐฎ๊ณ ์๊ฐ ๋ญ๋น์ฒ๋ผ ๋๊ปด์ง ์ ์์ด์. ๊ทผ๋ฐ 6๊ฐ์, 1๋ ์ง๋๊ณ ๋๋ฉด ํ์คํ ๋๊ปด์ ธ์. ๋ฒ๊ทธ๊ฐ ์ค๊ณ , ์ฝ๋ ๋ฆฌ๋ทฐ๊ฐ ๋นจ๋ผ์ง๊ณ , ์ ํ์์ด ์ฝ๋ ์ดํดํ๋ ์๊ฐ์ด ์ค์ด๋ค์ด์. ์ด๊ฒ ์ง์ง ์์ฐ์ฑ ํฅ์์ด์์.
1. golangci-lint ์ค์น
2. ํ๋ก์ ํธ์ ๊ธฐ๋ณธ ์ค์ ํ์ผ ์์ฑ
3. ๊ธฐ๋ณธ linter๋ก ํํฉ ํ์
4. ๊ฐ์ฅ ์ฌ๊ฐํ ์ด์๋ถํฐ ์์
1. CI/CD ํ์ดํ๋ผ์ธ ํตํฉ
2. pre-commit hook ์ค์
3. ํ ์ปจ๋ฒค์ ๋ฌธ์ํ
4. IDE ํตํฉ ์ค์
1. ๋ณด์ linter ์ถ๊ฐ
2. ๋ณต์ก๋ ์งํ ๋ชจ๋ํฐ๋ง
3. ํ ๋ฆฌ๋ทฐ ํ๋ก์ธ์ค ๊ฐ์
4. ์ปค์คํ linter ๊ฒํ
Go ์ธ์ด์ ์ฒ ํ์ "๋ช ํํ๊ณ ๋จ์ํ ์ฝ๋"์์. golangci-lint๋ ๊ทธ ์ฒ ํ์ ์ค์ฒํ๋ ๊ฐ์ฅ ํจ๊ณผ์ ์ธ ๋๊ตฌ์์. ์ง๊ธ ๋ฐ๋ก ์์ํด๋ด์! ์ฒ์์ ๊ฒฝ๊ณ ํญํ ๋ง๊ฒ ์ง๋ง ใ ใ ๊ทธ๊ฒ ์ฑ์ฅ์ ์์์ด์์.
๊ด๋ จ ํค์๋
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.

๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.