Go 1.24에서 개선된 range-over-func로 iterator 패턴 구현하기
배경
프로젝트에서 대용량 CSV 파일을 스트리밍으로 처리하는 로직이 있었다. 기존에는 채널로 구현했지만 고루틴 오버헤드와 메모리 사용량이 부담스러웠다.
Go 1.23에서 실험적으로 도입된 range-over-func이 1.24에서 정식 기능이 되면서, 이를 활용한 iterator 패턴으로 리팩토링을 진행했다.
기존 채널 기반 구현
func ReadCSV(filename string) <-chan []string {
ch := make(chan []string)
go func() {
defer close(ch)
file, _ := os.Open(filename)
defer file.Close()
reader := csv.NewReader(file)
for {
record, err := reader.Read()
if err == io.EOF {
break
}
ch <- record
}
}()
return ch
}
range-over-func로 개선
func ReadCSV(filename string) func(yield func([]string) bool) {
return func(yield func([]string) bool) {
file, err := os.Open(filename)
if err != nil {
return
}
defer file.Close()
reader := csv.NewReader(file)
for {
record, err := reader.Read()
if err == io.EOF {
break
}
if !yield(record) {
break
}
}
}
}
// 사용
for record := range ReadCSV("data.csv") {
process(record)
}
성능 비교
1GB CSV 파일 기준으로 벤치마크를 돌렸다.
- 채널 방식: 2.3초, 메모리 150MB
- range-over-func: 1.8초, 메모리 80MB
고루틴 스케줄링 오버헤드가 없어지면서 약 20% 성능 향상이 있었다. 메모리는 채널 버퍼가 사라지면서 거의 절반으로 줄었다.
결론
range-over-func은 단순히 문법적 편의성뿐 아니라 실질적인 성능 개선도 가져왔다. 기존 채널 기반 iterator를 점진적으로 마이그레이션할 계획이다.
다만 yield 함수의 bool 반환값으로 early termination을 제어하는 패턴이 아직은 낯설어서, 팀 내 코드 리뷰에서 명시적인 주석을 추가하기로 했다.