Go 1.22 range over function 실험적 사용기
배경
팀 내부 배치 처리 서비스를 Go로 마이그레이션하는 중이었다. 대용량 데이터를 청크 단위로 처리하는 로직이 많았는데, 기존 코드가 channel과 goroutine으로 복잡하게 얽혀있었다.
Go 1.22에서 실험적으로 추가된 range over function을 보고 이걸로 정리할 수 있겠다 싶었다.
기존 코드의 문제
func ProcessRecords(db *sql.DB) error {
rows, err := db.Query("SELECT * FROM large_table")
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var record Record
if err := rows.Scan(&record); err != nil {
return err
}
// 처리 로직
}
return rows.Err()
}
에러 핸들링이 산재되어 있고, 청크 단위 처리를 추가하려면 코드가 더 복잡해졌다.
range over function 적용
GOEXPERIMENT=rangefunc로 활성화 후 이터레이터 함수를 작성했다.
func QueryRecords(db *sql.DB, query string) func(yield func(Record) bool) {
return func(yield func(Record) bool) {
rows, err := db.Query(query)
if err != nil {
return
}
defer rows.Close()
for rows.Next() {
var record Record
if err := rows.Scan(&record); err != nil {
return
}
if !yield(record) {
return
}
}
}
}
// 사용
for record := range QueryRecords(db, "SELECT * FROM large_table") {
process(record)
}
리소스 관리가 이터레이터 내부로 캡슐화되면서 사용하는 쪽 코드가 깔끔해졌다.
청크 처리 구현
func Chunk[T any](size int) func(func([]T) bool) func(func(T) bool) {
return func(yield func([]T) bool) func(func(T) bool) {
return func(yieldItem func(T) bool) {
chunk := make([]T, 0, size)
for item := range yieldItem {
chunk = append(chunk, item)
if len(chunk) >= size {
if !yield(chunk) {
return
}
chunk = chunk[:0]
}
}
if len(chunk) > 0 {
yield(chunk)
}
}
}
}
함수 시그니처가 복잡해 보이지만, 조합 가능한 이터레이터를 만들 수 있다는 게 핵심이었다.
한계
아직 실험 단계라 프로덕션 적용은 보류했다. 에러 핸들링이 명확하지 않고, 타입 추론도 완벽하지 않았다. 하지만 방향성은 좋아 보였다.
Go 1.23에서 정식 기능이 되면 본격적으로 도입할 계획이다.