Quick Flow
go test -race ./...
go test -race ./internal/service
go run -race ./cmd/apiGo data race 점검은 공유 상태가 있는 동시성 코드에서 -race를 켜고 실제 실행 경로를 태우는 것부터 시작합니다.
기본 흐름
data race는 동시에 접근한 공유 메모리 문제다
var count int
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
count++
}()
go func() {
defer wg.Done()
count++
}()
wg.Wait()둘 이상의 goroutine이 같은 변수에 동시에 접근하고, 그중 하나 이상이 write이며 접근 사이에 동기화 순서가 없다면 data race입니다. 결과가 가끔 맞아 보여도 안전한 코드는 아닙니다. channel send와 해당 receive, 같은 mutex의 unlock과 다음 lock처럼 happens-before 관계를 만들면 접근 순서를 보장할 수 있습니다.
race detector는 실행된 경로에서 찾는다
go test -race ./...-race는 테스트나 실행 중 실제로 지나간 경로에서 data race를 찾습니다. 테스트가 해당 동시성 경로를 실행하지 않으면 race도 드러나지 않을 수 있습니다.
리포트는 접근 위치와 goroutine 생성 위치를 같이 본다
race detector가 출력하는 리포트에는 충돌한 read/write 위치와 goroutine이 어디서 만들어졌는지가 나옵니다. 수정할 때는 에러가 난 줄 하나보다 공유 상태의 소유권을 먼저 봐야 합니다.
보호 방법
공유 상태는 mutex로 보호한다
var mu sync.Mutex
count := 0
mu.Lock()
count++
mu.Unlock()여러 goroutine이 같은 map이나 struct를 수정하면 sync.Mutex로 보호하는 흐름이 가장 직접적입니다. 읽기와 쓰기 모두 같은 mutex 규약을 따라야 하며, lock을 건너뛴 읽기 하나도 race를 다시 만들 수 있습니다.
작은 counter는 atomic도 가능하다
var count atomic.Int64
count.Add(1)단순 counter, flag처럼 매우 작은 상태는 sync/atomic이 맞을 수 있습니다. atomic 연산 하나는 동기화되지만 여러 필드를 함께 일관되게 바꿔 주지는 않습니다. atomic.Int64, atomic.Bool 같은 typed atomic 값은 처음 사용한 뒤 복사하지 않습니다. 여러 필드의 불변식을 지켜야 하면 mutex가 더 읽기 쉽습니다.
소유권 이동은 channel이 맞을 수 있다
updates := make(chan Update)상태를 여러 goroutine이 직접 만지는 대신, 한 goroutine이 소유하고 channel로 요청만 받게 만들 수도 있습니다. send와 receive는 순서를 만들지만, send와 close를 서로 다른 goroutine이 수행하면 별도 동기화 없이 안전하다고 가정하지 않습니다.
선택 기준
| 상황 | 먼저 떠올릴 선택 |
|---|---|
| 전체 테스트 race 점검 | go test -race ./... |
| 서버 실행 경로 점검 | go run -race ./cmd/app |
| 공유 map/struct 보호 | sync.Mutex |
| 단순 counter/flag | sync/atomic 검토 |
| 상태 소유권을 한 곳으로 모음 | channel |
주의할 점
race detector는 강력하지만 정적 증명 도구는 아닙니다. 실행되지 않은 경로의 race는 찾지 못하므로 실제 부하에 가까운 binary도 -race로 실행합니다. -race는 cgo가 활성화되어야 하며, non-Darwin target은 설치된 C compiler도 필요합니다. 지원 platform과 실행 비용을 확인해 보통 로컬 검증이나 CI의 별도 job에서 사용합니다. race가 보이면 sleep을 넣어 타이밍을 피하기보다 공유 상태의 소유권과 보호 방식을 고쳐야 합니다.
참고 링크
3 sources