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을 제어하는 패턴이 아직은 낯설어서, 팀 내 코드 리뷰에서 명시적인 주석을 추가하기로 했다.