포매터와 린터
이 챕터에서 다루는 것
Go 코드의 형식과 품질을 자동으로 지키는 도구들을 한 줄로 세운다. 포매터(gofmt,
goimports, gofumpt), 정적 분석기(go vet, staticcheck), 그리고 Go 1.26에서 완전히
다시 쓰인 **go fix**다. 마지막에 저장 시 자동 포맷과 CI 연동을 붙인다.
문제: 스타일 논쟁은 아무것도 만들지 않는다
탭이냐 스페이스냐, 중괄호를 어디에 두느냐, import를 어떻게 정렬하느냐. 어느 팀에서든 이 논쟁에 시간이 녹는다. 논쟁 자체가 문제가 아니라, 결론이 나도 강제할 방법이 없다는 게 문제다. 스타일 가이드 문서는 아무도 안 읽고, 코드 리뷰는 "여기 공백 하나"로 채워진다.
Go의 답은 단순하다. 포매터를 언어와 함께 배포하고, 설정을 주지 않는다.
gofmt: 옵션이 없는 포매터
gofmt는 Go 설치에 딸려 온다($GOROOT/bin/gofmt). 그리고 스타일 옵션이 없다.
들여쓰기 폭도, 줄 길이 제한도, 중괄호 위치도 바꿀 수 없다.
이건 기능 부족이 아니라 핵심 설계다. 옵션이 하나라도 있으면 그 옵션을 두고 논쟁이 시작되고, 프로젝트마다 다른 설정이 생기고, 결국 "이 코드는 왜 이렇게 생겼지"를 매번 생각해야 한다. 옵션이 없으면 모든 Go 코드가 똑같이 생긴다. 표준 라이브러리도, 남의 오픈소스도, 내 코드도.
Rob Pike의 말대로다. "gofmt의 스타일은 아무도 좋아하지 않지만, gofmt는 모두가 좋아한다."
써 보기
이런 파일이 있다고 하자.
package main
import "fmt"
type point struct{ X int
Y int }
func main(){
p:=point{1,2}
if p.X>0 {
fmt.Println( "x is positive", p )
}
}
무엇이 바뀔지 먼저 본다. -d는 diff, -l은 "고칠 게 있는 파일 목록만".
gofmt -l .
messy.go
gofmt -d messy.go
diff messy.go.orig messy.go
--- messy.go.orig
+++ messy.go
@@ -1,10 +1,15 @@
package main
+
import "fmt"
-type point struct{ X int
-Y int }
-func main(){
- p:=point{1,2}
- if p.X>0 {
- fmt.Println( "x is positive", p )
- }
+
+type point struct {
+ X int
+ Y int
+}
+
+func main() {
+ p := point{1, 2}
+ if p.X > 0 {
+ fmt.Println("x is positive", p)
+ }
}
실제로 고치려면 -w(write)를 붙이거나, go fmt를 쓴다.
go fmt ./...
messy.go
go fmt는 gofmt -l -w를 패키지 단위로 돌리는 얇은 래퍼다. 고친 파일 이름을 출력한다.
package main
import "fmt"
type point struct {
X int
Y int
}
func main() {
p := point{1, 2}
if p.X > 0 {
fmt.Println("x is positive", p)
}
}
:::info 들여쓰기는 탭이다 gofmt는 탭으로 들여쓰고, 정렬(구조체 필드 등)에는 스페이스를 쓴다. 이것도 옵션이 아니다. 탭이면 각자 편집기에서 원하는 폭으로 볼 수 있기 때문이다. :::
gofmt -s
-s(simplify)는 중복 표현을 줄여 준다. 예를 들어 []Point{Point{1,2}}를
[]Point{{1,2}}로. 기본으로 켜져 있지는 않지만 켜 두면 좋다. 뒤에 나올 golangci-lint
설정에서 활성화한다.
goimports: import를 알아서 맞춘다
gofmt가 못 하는 게 하나 있다. import 목록을 고치는 것이다. goimports가 그걸 한다.
go install golang.org/x/tools/cmd/goimports@latest
package main
import (
"os"
"fmt"
)
func run() {
fmt.Fprintln(os.Stderr, "start")
log.Println("done")
}
log를 import하지 않았고, os와 fmt의 순서도 틀렸다.
goimports -d imp.go
diff -u imp.go.orig imp.go
--- imp.go.orig 2026-08-08 13:07:40
+++ imp.go 2026-08-08 13:07:40
@@ -1,8 +1,9 @@
package main
import (
- "os"
"fmt"
+ "log"
+ "os"
)
func run() {
필요한 import를 추가하고, 안 쓰는 import를 지우고, 알파벳 순으로 정렬한다. goimports는 gofmt의 상위 집합이라 gofmt가 하는 일도 전부 한다.
편집기를 쓴다면 gopls가 이 기능을 내장하고 있어서(source.organizeImports 코드 액션)
CLI로 직접 쓸 일은 많지 않다. 하지만 CI에서는 유용하다.
:::note import 그룹 나누기 Go 관례상 표준 라이브러리와 외부 패키지를 빈 줄로 나눈다.
import (
"fmt"
"os"
"github.com/gin-gonic/gin"
)
goimports는 이미 나뉜 그룹은 존중하지만, 붙어 있는 걸 알아서 나눠 주지는 않는다.
-local github.com/myorg 플래그를 주면 자사 패키지를 세 번째 그룹으로 뺀다.
:::
gofumpt: 더 엄격한 gofmt
gofumpt는 gofmt가 "이것까지는 안 건드리겠다"고 남겨 둔 부분을 추가로 정리한다.
gofmt와 호환된다 — gofumpt를 통과한 코드는 gofmt도 통과한다.
go install mvdan.cc/gofumpt@latest
package main
import (
"fmt"
"os"
)
func save(path string) error {
err := os.WriteFile(path, []byte("hi"), 0644)
if err != nil {
return fmt.Errorf("save: %w", err)
}
return nil
}
gofumpt -d fum.go
diff fum.go.orig fum.go
--- fum.go.orig
+++ fum.go
@@ -6,7 +6,6 @@
)
func save(path string) error {
-
err := os.WriteFile(path, []byte("hi"), 0644)
if err != nil {
return fmt.Errorf("save: %w", err)
@@ -13,5 +12,4 @@
}
return nil
-
}
함수 본문 처음과 끝의 빈 줄을 지웠다. 이 외에도 0644 → 0o644, 짧은 if err != nil
블록 정리 등 수십 개의 규칙이 있다.
쓸지 말지는 팀 결정이다. gofmt는 표준이고 gofumpt는 아니다. 다만 한 번 켜면 논쟁이
더 줄어드는 건 사실이라, 새 프로젝트라면 켜 두는 쪽을 권한다. gopls 설정에서
"formatting.gofumpt": true로 켠다(1-5).
go vet: 컴파일되지만 틀린 코드
포매터는 모양만 본다. go vet은 의미를 본다. 컴파일은 되지만 거의 확실히 버그인
패턴을 찾는다. 설치할 것도, 설정할 것도 없다. 툴체인에 들어 있다.
이런 프로그램이 있다고 하자. 컴파일은 완벽하게 된다.
package main
import (
"fmt"
"net"
"sync"
)
func dial(host string, port int) (net.Conn, error) {
addr := fmt.Sprintf("%s:%d", host, port)
return net.Dial("tcp", addr)
}
func main() {
var wg sync.WaitGroup
go func() {
wg.Add(1)
defer wg.Done()
fmt.Println("작업 중")
}()
wg.Wait()
conn, err := dial("::1", 8080)
if err != nil {
fmt.Println("연결 실패:", err)
return
}
defer conn.Close()
}
go vet ./06-vet-demo
06-vet-demo/main.go:10:22: address format "%s:%d" does not work with IPv6 (passed to net.Dial at L11)
06-vet-demo/main.go:17:9: WaitGroup.Add called from inside new goroutine
두 개 다 잡았다. 각각이 왜 버그인지 보자.
hostport — "%s:%d"로 주소를 만들면 IPv6가 깨진다
host가 "::1"(IPv6 루프백)이면 fmt.Sprintf("%s:%d", "::1", 8080)은 "::1:8080"이
된다. 어디까지가 주소이고 어디부터가 포트인지 구분되지 않는다. 올바른 형태는
"[::1]:8080"이다.
정답은 net.JoinHostPort다. 대괄호를 알아서 붙여 준다.
addr := net.JoinHostPort(host, strconv.Itoa(port))
IPv4만 쓰는 환경에서는 몇 년이고 멀쩡히 돌다가, IPv6가 켜진 순간 터진다. 정확히 이런 게 정적 분석이 필요한 이유다.
waitgroup — Add를 고루틴 안에서 부르면 안 된다
wg.Add(1)이 새 고루틴 안에 있다. wg.Wait()가 그 고루틴이 스케줄되기 전에 실행되면
카운터가 아직 0이라 즉시 통과한다. 고루틴이 끝나기를 기다리지 못한다. 실행할 때마다
결과가 달라지는, 가장 잡기 싫은 종류의 버그다.
Go 1.25부터는 wg.Go(f) 하나로 끝난다. Add와 Done을 알아서 짝지어 준다.
var wg sync.WaitGroup
wg.Go(func() {
fmt.Println("작업 중")
})
wg.Wait()
Part 7-5에서 자세히 다룬다.
go vet이 가진 분석기 전체
go tool vet help
appends check for missing values after append
asmdecl report mismatches between assembly files and Go declarations
assign check for useless assignments
atomic check for common mistakes using the sync/atomic package
bools check for common mistakes involving boolean operators
buildtag check //go:build and // +build directives
cgocall detect some violations of the cgo pointer passing rules
composites check for unkeyed composite literals
copylocks check for locks erroneously passed by value
defers report common mistakes in defer statements
directive check Go toolchain directives such as //go:debug
errorsas report passing non-pointer or non-error values to errors.As
framepointer report assembly that clobbers the frame pointer before saving it
hostport check format of addresses passed to net.Dial
httpresponse check for mistakes using HTTP responses
ifaceassert detect impossible interface-to-interface type assertions
loopclosure check references to loop variables from within nested functions
lostcancel check cancel func returned by context.WithCancel is called
nilfunc check for useless comparisons between functions and nil
printf check consistency of Printf format strings and arguments
shift check for shifts that equal or exceed the width of the integer
sigchanyzer check for unbuffered channel of os.Signal
slog check for invalid structured logging calls
stdmethods check signature of methods of well-known interfaces
stdversion report uses of too-new standard library symbols
stringintconv check for string(int) conversions
structtag check that struct field tags conform to reflect.StructTag.Get
testinggoroutine report calls to (*testing.T).Fatal from goroutines started by a test
tests check for common mistaken usages of tests and examples
timeformat check for calls of (time.Time).Format or time.Parse with 2006-02-01
unmarshal report passing non-pointer or non-interface values to unmarshal
unreachable check for unreachable code
unsafeptr check for invalid conversions of uintptr to unsafe.Pointer
unusedresult check for unused results of calls to some functions
waitgroup check for misuses of sync.WaitGroup
이 목록을 한 번 훑어 두면 좋다. 여기 있는 항목 중 상당수가 이 강의의 챕터 주제와 겹친다 —
copylocks(7-5), lostcancel(7-7), printf(2-3), structtag(5-5), errorsas(4-7).
:::tip go test는 vet을 자동으로 돌린다
go test를 실행하면 그 전에 go vet의 일부 분석기(atomic, bool, buildtags,
errorsas, ifaceassert, nilfunc, printf, stringintconv)가 먼저 돈다.
"테스트를 돌렸는데 컴파일도 안 되고 이상한 에러가 난다" 싶으면 vet 결과일 수 있다.
:::
go fix: 이제는 모더나이저다
Go 1.26에서 go fix가 완전히 다시 쓰였다. 예전의 go fix는 Go 1.0 이전 코드를
1.0으로 옮기는 유물이었고, 십수 년 동안 쓸 일이 없었다. 지금은 다르다.
The venerable
go fixcommand has been completely revamped and is now the home of Go's modernizers. — Go 1.26 릴리스 노트
go vet과 같은 분석 프레임워크 위에 만들어졌다. 차이는 목적이다. go vet은 "이건
버그다"를 보고하고, go fix는 "이건 낡은 방식이다"를 고친다.
무엇을 고쳐 주나
go tool fix help
Registered analyzers:
any replace interface{} with any
buildtag check //go:build and // +build directives
fmtappendf replace []byte(fmt.Sprintf) with fmt.Appendf
forvar remove redundant re-declaration of loop variables
hostport check format of addresses passed to net.Dial
inline apply fixes based on 'go:fix inline' comment directives
mapsloop replace explicit loops over maps with calls to maps package
minmax replace if/else statements with calls to min or max
newexpr simplify code by using go1.26's new(expr)
omitzero suggest replacing omitempty with omitzero for struct fields
plusbuild remove obsolete //+build comments
rangeint replace 3-clause for loops with for-range over integers
reflecttypefor replace reflect.TypeOf(x) with TypeFor[T]()
slicescontains replace loops with slices.Contains or slices.ContainsFunc
slicessort replace sort.Slice with slices.Sort for basic types
stditerators use iterators instead of Len/At-style APIs
stringsbuilder replace += with strings.Builder
stringscut replace strings.Index etc. with strings.Cut
stringscutprefix replace HasPrefix/TrimPrefix with CutPrefix
stringsseq replace ranging over Split/Fields with SplitSeq/FieldsSeq
testingcontext replace context.WithCancel with t.Context in tests
waitgroup replace wg.Add(1)/go/wg.Done() with wg.Go
이 목록이 사실상 "지금 Go에서 무엇이 최신 관용구인가"의 요약본이다. interface{} 대신
any, sort.Slice 대신 slices.Sort, 3절 for 대신 for range 정수, 그리고 앞서 본
wg.Add/wg.Done 대신 wg.Go까지.
forvar가 특히 눈길을 끈다. Go 1.22 이전에는 클로저의 루프 변수 캡처를 피하려고
v := v를 써야 했는데, 이제 불필요하다. go fix가 그 흔적을 지워 준다(2-6에서 다룬다).
실제로 돌려 보기
package main
import (
"fmt"
"sort"
)
func describe(v interface{}) string {
return fmt.Sprintf("%T", v)
}
func main() {
nums := []int{5, 2, 9, 1}
sort.Slice(nums, func(i, j int) bool { return nums[i] < nums[j] })
total := 0
for i := 0; i < len(nums); i++ {
total += nums[i]
}
fmt.Println(nums, total, describe(nums))
}
먼저 -diff로 무엇이 바뀔지 확인한다. gofmt -d와 같은 습관이다.
go fix -diff ./06-modernize-demo
--- .../06-modernize-demo/main.go (old)
+++ .../06-modernize-demo/main.go (new)
@@ -2,20 +2,20 @@
import (
"fmt"
- "sort"
+ "slices"
)
-func describe(v interface{}) string {
+func describe(v any) string {
return fmt.Sprintf("%T", v)
}
func main() {
nums := []int{5, 2, 9, 1}
- sort.Slice(nums, func(i, j int) bool { return nums[i] < nums[j] })
+ slices.Sort(nums)
total := 0
- for i := 0; i < len(nums); i++ {
+ for i := range nums {
total += nums[i]
}
세 가지를 동시에 고쳤다. interface{} → any, sort.Slice → slices.Sort,
3절 for → for range. import까지 정리했다.
-diff 없이 실행하면 파일에 그대로 적용된다.
go fix ./...
동작이 바뀌지 않았는지 확인해 보자.
go run .
[1 2 5 9] 17 []int
원본과 같다.
:::warning go fix는 커밋된 상태에서 돌리자
파일을 직접 고치므로, 되돌릴 수 있어야 한다. 항상 깨끗한 작업 트리에서 실행하고
git diff로 결과를 검토하자. Go 팀은 "동작을 바꾸지 않아야 한다"고 명시하고 있고 그런
문제는 버그로 신고받지만, 검토 없이 통째로 커밋할 일은 아니다.
:::
:::tip 언제 돌리나 Go를 새 버전으로 올린 직후가 가장 좋은 타이밍이다. 새 버전에서 추가된 관용구가 fixer로 따라 들어오기 때문이다. 평소에는 gopls가 같은 제안을 코드 액션으로 하나씩 띄워 준다. :::
staticcheck: go vet보다 훨씬 멀리 간다
go vet은 오탐이 거의 없는 것만 고른다. staticcheck는 범위를 넓힌다.
go install honnef.co/go/tools/cmd/staticcheck@latest
staticcheck --version
staticcheck 2026.1 (v0.7.0)
package main
import (
"fmt"
"strings"
)
func hasPrefix(s string) bool {
if strings.HasPrefix(s, "go-") {
return true
}
return false
}
func main() {
fmt.Println(strings.Title("hello"), hasPrefix("go-kit"))
}
go vet ./...은 아무것도 잡지 못한다. 출력이 한 줄도 없이 끝난다. staticcheck는 두 개를
잡는다.
staticcheck ./...
main.go:9:2: should use 'return strings.HasPrefix(s, "go-")' instead of 'if strings.HasPrefix(s, "go-") { return true }; return false' (S1008)
main.go:16:14: strings.Title has been deprecated since Go 1.18 and an alternative has been available since Go 1.0: The rule Title uses for word boundaries does not handle Unicode punctuation properly. Use golang.org/x/text/cases instead. (SA1019)
검사 코드에 접두어가 붙어 있다. 이것만 알면 메시지를 분류할 수 있다.
| 접두어 | 뜻 |
|---|---|
SA | 정적 분석 — 버그 가능성 (SA1019는 폐기된 API 사용) |
S1 | 단순화 제안 |
ST1 | 스타일 (기본 비활성) |
QF1 | 빠른 수정(quickfix) 제안 |
U1 | 미사용 코드 |
SA1019(deprecated API)만으로도 도입할 값어치가 있다. io/ioutil처럼 폐기된 API를 쓰는
코드를 자동으로 찾아낸다.
편집기에서는 gopls 설정 "ui.diagnostic.staticcheck": true로 켜면 별도 실행 없이 진단에
섞여 나온다.
golangci-lint: 여러 린터를 하나로
린터를 하나씩 설치하고 하나씩 실행하는 건 CI에서 금방 지저분해진다. golangci-lint는
수십 개 린터를 한 번에 돌리고, 결과를 합치고, 병렬 실행과 캐싱까지 해 준다.
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
golangci-lint --version
golangci-lint has version 2.12.2 built with go1.26.5 ...
:::warning v1과 v2는 설정 파일 형식이 다르다
golangci-lint는 v2에서 설정 스키마가 크게 바뀌었다. version: "2" 줄이 없는 설정 파일을
인터넷에서 복사해 오면 동작하지 않는다. 아래는 v2 형식이다. 모듈 경로에 /v2가 들어가는
것도 잊지 말자.
:::
설정 파일
프로젝트 루트에 .golangci.yml을 둔다.
version: "2"
linters:
default: standard
enable:
- errcheck
- govet
- ineffassign
- staticcheck
- unused
exclusions:
rules:
- path: _test\.go
linters:
- errcheck
formatters:
enable:
- gofumpt
- goimports
default: standard— 기본 린터 묶음에서 시작한다.none으로 두고 필요한 것만 켜는 방식도 있다.errcheck— 처리하지 않은 에러를 잡는다. Go에서 가장 중요한 린터다. 4-6에서 다룰 "에러를_로 버리지 말 것"을 기계적으로 강제해 준다.ineffassign— 대입했지만 읽히지 않는 값.unused— 쓰이지 않는 비공개 함수·타입·필드. 컴파일러는 지역 변수만 잡아 준다.exclusions— 테스트 파일에서는errcheck를 끈다. 테스트 헬퍼에서 에러 검사를 생략하는 게 실용적일 때가 있다.formatters— v2에서 포매터는 린터와 분리된 섹션이다.
설정이 유효한지 먼저 확인한다.
golangci-lint config verify
출력이 없으면 통과다.
golangci-lint run ./...
main.go:9:2: S1008: should use 'return strings.HasPrefix(s, "go-")' instead of 'if strings.HasPrefix(s, "go-") { return true }; return false' (staticcheck)
if strings.HasPrefix(s, "go-") {
^
main.go:16:14: SA1019: strings.Title has been deprecated since Go 1.18 and an alternative has been available since Go 1.0: The rule Title uses for word boundaries does not handle Unicode punctuation properly. Use golang.org/x/text/cases instead. (staticcheck)
fmt.Println(strings.Title("hello"), hasPrefix("go-kit"))
^
2 issues:
* staticcheck: 2
문제가 난 줄을 함께 보여 준다는 게 staticcheck 단독 실행과의 차이다.
:::tip 린터는 적게 시작해서 늘린다
golangci-lint에는 100개 넘는 린터가 있다. 전부 켜면(default: all) 기존 프로젝트에서
수천 개의 경고가 쏟아지고, 결국 아무도 안 본다. 위 다섯 개로 시작해서, 팀이 실제로
겪은 버그 유형에 맞춰 하나씩 추가하는 편이 낫다.
:::
저장할 때 자동으로 포맷하기
수동으로 gofmt를 돌리는 사람은 없다. 편집기에 맡긴다.
1-5에서 본 설정을 다시 보자.
{
"[go]": {
"editor.defaultFormatter": "golang.go",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
}
}
}
저장할 때마다 포매팅되고 import가 정리된다. 이 설정이 켜져 있으면 gofmt를 직접 실행할 일이 사실상 없어진다.
CI에 붙이기
포매팅과 린트는 사람이 리뷰에서 지적할 일이 아니라 기계가 막을 일이다.
name: CI
on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: 포맷 확인
run: |
test -z "$(gofmt -l .)" || { gofmt -l .; exit 1; }
- name: go vet
run: go vet ./...
- name: 모더나이저 확인
run: go fix -diff ./...
- name: 린트
uses: golangci/golangci-lint-action@v8
- name: 테스트
run: go test -race ./...
몇 가지 짚어 둔다.
go-version-file: go.mod— Go 버전을 워크플로에 하드코딩하지 않고go.mod에서 읽는다. 두 곳을 따로 관리하면 반드시 어긋난다.- 포맷 확인은
gofmt -l의 출력이 비어 있는지로 한다. CI에서-w로 고치면 안 된다. 고칠 사람은 개발자다. go fix -diff는 고칠 게 있으면 0이 아닌 종료 코드를 낸다. 그래서 그 자체로 CI 체크가 된다.go test -race— 경합 탐지기를 켜고 테스트한다. Part 7-6에서 다룬다.
:::note 이 워크플로는 실행해 보지 않았다
위 YAML은 GitHub Actions에서 도는 것이므로 이 기계에서 검증할 수 없다. 다만 각 run
줄의 명령 자체(gofmt -l, go vet ./..., go fix -diff ./..., go test -race ./...)는
로컬에서 그대로 실행해 확인할 수 있다. CI에 넣기 전에 로컬에서 먼저 돌려 보자.
:::
도구 정리
| 도구 | 설치 | 무엇을 보나 | 고쳐 주나 |
|---|---|---|---|
gofmt | 내장 | 형식 | O (-w) |
goimports | go install | 형식 + import | O (-w) |
gofumpt | go install | 형식(더 엄격) | O (-w) |
go vet | 내장 | 버그 가능성 | X |
go fix | 내장 | 낡은 관용구 | O |
staticcheck | go install | 버그 + 단순화 + 폐기 API | X |
golangci-lint | go install | 위 여러 개를 한 번에 | 일부 |
흔히 하는 실수
- gofmt를 설정하려고 한다. 옵션이 없다. 그게 요점이다.
- CI에서 포맷을 자동으로 고쳐 커밋한다. 봇 커밋이 히스토리를 어지럽히고, 개발자는 자기 코드가 왜 바뀌었는지 모른다. 확인만 하고 실패시키자.
go fix를 옛날 도구로 안다. Go 1.26 이전 자료를 읽고 있는 것이다. 지금은 업그레이드할 때마다 돌릴 가치가 있는 도구다.- 린터를 전부 켠다. 경고가 수천 개면 아무도 안 본다.
go vet을 안 돌린다. 무료이고, 오탐이 거의 없고, 설정도 필요 없다. 안 돌릴 이유가 없다.golangci-lintv1 설정을 v2에 쓴다.version: "2"줄부터 확인하자.
정리
- gofmt에는 옵션이 없다. 그래서 모든 Go 코드가 똑같이 생겼고, 스타일 논쟁이 사라졌다.
goimports는 import까지,gofumpt는 그보다 더 엄격하게 정리한다.go vet은 버그를 보고한다.hostport와waitgroup은 특히 실전에서 아프게 물리는 것들이다.go fix는 Go 1.26에서 다시 태어났다. 낡은 관용구를 최신 형태로 바꿔 준다.-diff로 먼저 확인하는 습관을 들이자.staticcheck는 폐기 API와 단순화 지점을 잡는다. gopls에서 켜 두면 편하다.golangci-lint(v2)는 이 전부를 CI에서 한 번에 돌리는 방법이다. 적게 시작해서 늘린다.- 저장 시 자동 포맷 + CI 검사. 사람이 리뷰에서 지적할 일이 아니다.
연습문제
-
아무 Go 파일이나 골라 일부러 들여쓰기를 망가뜨리고
gofmt -d로 diff를 확인해 보자. 그다음gofmt -w로 고치고, 원래 파일과 완전히 같아졌는지git diff로 확인해 보자. gofmt가 원래 코드를 복원한 것인지, 우연히 같아진 것인지 어떻게 확신할 수 있는가? -
06-vet-demo/main.go의 두 문제를 직접 고쳐 보자.net.JoinHostPort와wg.Go를 쓰면 된다. 고친 뒤go vet ./...이 조용해지는지 확인하자. 힌트:strconv.Itoa로 포트를 문자열로 바꿔야 한다. -
go fix -diff ./...를 이 강의 저장소의examples/전체에 돌려 보자. 아직 제안이 나오는 곳이 있는가?go tool fix help의 목록 중 자신이 지금 쓰는 다른 언어에도 비슷한 도구가 있는지 생각해 보자.