Go 1.22 generics로 슬라이스 유틸리티 함수 리팩토링

문제 상황

프로젝트 곳곳에서 슬라이스를 변환하거나 필터링하는 코드가 중복됐다. Go 1.18 이전에는 제네릭이 없어서 interface{}를 쓰거나 타입별로 함수를 만들어야 했는데, 이제는 제네릭을 써볼 만한 시점이라고 판단했다.

기존 코드

func FilterStrings(items []string, fn func(string) bool) []string {
    result := []string{}
    for _, item := range items {
        if fn(item) {
            result = append(result, item)
        }
    }
    return result
}

func FilterInts(items []int, fn func(int) bool) []int {
    // 동일한 로직 반복
}

타입마다 함수를 만들어야 해서 유지보수가 번거로웠다.

제네릭 적용

func Filter[T any](items []T, fn func(T) bool) []T {
    result := make([]T, 0, len(items))
    for _, item := range items {
        if fn(item) {
            result = append(result, item)
        }
    }
    return result
}

func Map[T any, U any](items []T, fn func(T) U) []U {
    result := make([]U, len(items))
    for i, item := range items {
        result[i] = fn(item)
    }
    return result
}

사용 예시:

ids := []int{1, 2, 3, 4, 5}
even := Filter(ids, func(n int) bool { return n%2 == 0 })

users := []User{{ID: 1}, {ID: 2}}
userIDs := Map(users, func(u User) int { return u.ID })

성능 체크

벤치마크 결과 제네릭 버전이 interface{} 버전보다 약 30% 빠르게 나왔다. 타입 어서션 오버헤드가 없어진 덕분이다.

주의사항

  • 컴파일 타임은 소폭 증가했다
  • 너무 복잡한 제네릭 제약은 오히려 가독성을 해친다
  • 간단한 로직은 그냥 인라인으로 쓰는 게 나을 수 있다

결과적으로 코드 중복은 줄고 타입 안정성은 높아졌다. 앞으로 유사한 유틸리티는 제네릭을 우선 고려할 예정이다.