Updated:

12 minute read

개요

Go 1.21부터 추가된 cmp 패키지는 비교 가능한 타입들을 위한 유틸리티 함수를 제공합니다.

주요 특징:

  • Ordered 인터페이스: 순서 비교 가능 타입 정의
  • Compare 함수: 삼원 비교 (-1, 0, 1)
  • Less 함수: 작음 비교 (불린 반환)
  • Or 함수: 제로 값 대체
  • 타입 안전: 제네릭 기반
  • 표준 라이브러리: import “cmp”
  • 정렬 지원: slices.SortFunc와 함께 사용

기본 개념

1. Ordered 인터페이스

package main

import "cmp"

// cmp.Ordered 정의
type Ordered interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
    ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
    ~float32 | ~float64 |
    ~string
}

func main() {
    // Ordered 타입들
    var i int = 10
    var f float64 = 3.14
    var s string = "hello"
    
    // 모두 Ordered 인터페이스를 만족
    _ = cmp.Compare(i, 20)
    _ = cmp.Compare(f, 2.71)
    _ = cmp.Compare(s, "world")
}

2. 비교 결과

func main() {
    // Compare 결과:
    // -1: 첫 번째 < 두 번째
    //  0: 첫 번째 == 두 번째
    //  1: 첫 번째 > 두 번째
    
    fmt.Println(cmp.Compare(1, 2))   // -1
    fmt.Println(cmp.Compare(2, 2))   // 0
    fmt.Println(cmp.Compare(3, 2))   // 1
}

Compare 함수

1. 정수 비교

func main() {
    // int
    fmt.Println(cmp.Compare(10, 20))     // -1 (10 < 20)
    fmt.Println(cmp.Compare(20, 10))     // 1  (20 > 10)
    fmt.Println(cmp.Compare(15, 15))     // 0  (15 == 15)
    
    // int64
    var a, b int64 = 100, 200
    fmt.Println(cmp.Compare(a, b))       // -1
    
    // uint
    var x, y uint = 5, 3
    fmt.Println(cmp.Compare(x, y))       // 1
    
    // 음수
    fmt.Println(cmp.Compare(-5, -10))    // 1  (-5 > -10)
    fmt.Println(cmp.Compare(-10, -5))    // -1 (-10 < -5)
}

2. 실수 비교

func main() {
    // float64
    fmt.Println(cmp.Compare(3.14, 2.71))   // 1
    fmt.Println(cmp.Compare(1.5, 1.5))     // 0
    
    // float32
    var f1, f2 float32 = 1.1, 2.2
    fmt.Println(cmp.Compare(f1, f2))       // -1
    
    // 0과 비교
    fmt.Println(cmp.Compare(0.0, -1.0))    // 1
    fmt.Println(cmp.Compare(-0.0, 0.0))    // 0 (양수/음수 0은 같음)
}

3. 문자열 비교

func main() {
    // 사전순 비교
    fmt.Println(cmp.Compare("apple", "banana"))   // -1
    fmt.Println(cmp.Compare("zebra", "aardvark")) // 1
    fmt.Println(cmp.Compare("hello", "hello"))    // 0
    
    // 대소문자 구분
    fmt.Println(cmp.Compare("Apple", "apple"))    // -1 (대문자 < 소문자)
    
    // 길이와 무관 (사전순)
    fmt.Println(cmp.Compare("a", "aaa"))          // -1
    fmt.Println(cmp.Compare("b", "aaa"))          // 1 (b > a)
    
    // 빈 문자열
    fmt.Println(cmp.Compare("", "a"))             // -1
    fmt.Println(cmp.Compare("", ""))              // 0
}

4. 사용자 정의 타입

type MyInt int
type MyString string

func main() {
    // ~ 덕분에 사용 가능
    var a, b MyInt = 10, 20
    fmt.Println(cmp.Compare(a, b))   // -1
    
    var s1, s2 MyString = "hello", "world"
    fmt.Println(cmp.Compare(s1, s2)) // -1
}

Less 함수

1. 기본 사용

func main() {
    // Less: x < y 반환
    fmt.Println(cmp.Less(1, 2))      // true
    fmt.Println(cmp.Less(2, 1))      // false
    fmt.Println(cmp.Less(1, 1))      // false
    
    // Compare를 사용한 구현과 동일
    // cmp.Less(x, y) == (cmp.Compare(x, y) < 0)
}

2. 다양한 타입

func main() {
    // 정수
    fmt.Println(cmp.Less(10, 20))            // true
    fmt.Println(cmp.Less(-5, -10))           // false
    
    // 실수
    fmt.Println(cmp.Less(3.14, 2.71))        // false
    fmt.Println(cmp.Less(1.5, 2.5))          // true
    
    // 문자열
    fmt.Println(cmp.Less("apple", "banana")) // true
    fmt.Println(cmp.Less("zebra", "apple"))  // false
}

3. 정렬에 활용

import "sort"

func main() {
    numbers := []int{5, 2, 8, 1, 9}
    
    // sort.Slice에서 Less 사용
    sort.Slice(numbers, func(i, j int) bool {
        return cmp.Less(numbers[i], numbers[j])
    })
    
    fmt.Println(numbers) // [1 2 5 8 9]
}

Or 함수

1. 기본 사용

func main() {
    // Or: 첫 번째가 제로 값이면 두 번째 반환
    fmt.Println(cmp.Or(0, 10))      // 10 (0은 제로 값)
    fmt.Println(cmp.Or(5, 10))      // 5  (5는 제로 값 아님)
    
    // 문자열
    fmt.Println(cmp.Or("", "default"))   // "default"
    fmt.Println(cmp.Or("value", "default")) // "value"
    
    // 실수
    fmt.Println(cmp.Or(0.0, 1.5))   // 1.5
    fmt.Println(cmp.Or(2.5, 1.5))   // 2.5
}

2. 여러 값 체인

func main() {
    // 첫 번째 비제로 값 반환
    result := cmp.Or(0, 0, 0, 5, 10)
    fmt.Println(result)  // 5
    
    // 모두 제로면 마지막 제로 값 반환
    result = cmp.Or(0, 0, 0)
    fmt.Println(result)  // 0
    
    // 문자열 체인
    name := cmp.Or("", "", "Alice", "Bob")
    fmt.Println(name)  // "Alice"
}

3. 기본값 제공

type Config struct {
    Host string
    Port int
}

func NewConfig(host string, port int) *Config {
    return &Config{
        Host: cmp.Or(host, "localhost"),  // 기본값
        Port: cmp.Or(port, 8080),         // 기본값
    }
}

func main() {
    // 값 제공
    cfg1 := NewConfig("example.com", 9000)
    fmt.Printf("%+v\n", cfg1)  // {Host:example.com Port:9000}
    
    // 기본값 사용
    cfg2 := NewConfig("", 0)
    fmt.Printf("%+v\n", cfg2)  // {Host:localhost Port:8080}
}

4. 환경 변수 폴백

import "os"

func getEnv(key, defaultValue string) string {
    return cmp.Or(os.Getenv(key), defaultValue)
}

func main() {
    // 환경 변수가 없으면 기본값
    host := getEnv("HOST", "localhost")
    port := getEnv("PORT", "8080")
    
    fmt.Printf("Host: %s, Port: %s\n", host, port)
}

정렬과 함께 사용

1. slices.SortFunc

import (
    "cmp"
    "fmt"
    "slices"
)

func main() {
    numbers := []int{5, 2, 8, 1, 9}
    
    // 오름차순
    slices.SortFunc(numbers, cmp.Compare[int])
    fmt.Println(numbers)  // [1 2 5 8 9]
    
    // 내림차순
    slices.SortFunc(numbers, func(a, b int) int {
        return cmp.Compare(b, a)  // 순서 반대
    })
    fmt.Println(numbers)  // [9 8 5 2 1]
}

2. 구조체 정렬

type Person struct {
    Name string
    Age  int
}

func main() {
    people := []Person{
        {Name: "Alice", Age: 30},
        {Name: "Bob", Age: 25},
        {Name: "Carol", Age: 35},
    }
    
    // 나이순 정렬
    slices.SortFunc(people, func(a, b Person) int {
        return cmp.Compare(a.Age, b.Age)
    })
    
    for _, p := range people {
        fmt.Printf("%s: %d\n", p.Name, p.Age)
    }
    // Bob: 25
    // Alice: 30
    // Carol: 35
}

3. 다중 키 정렬

type Product struct {
    Category string
    Price    float64
    Name     string
}

func main() {
    products := []Product{
        {Category: "Electronics", Price: 299.99, Name: "Keyboard"},
        {Category: "Books", Price: 19.99, Name: "Go Programming"},
        {Category: "Electronics", Price: 199.99, Name: "Mouse"},
        {Category: "Books", Price: 29.99, Name: "Clean Code"},
    }
    
    // 카테고리순, 같으면 가격순
    slices.SortFunc(products, func(a, b Product) int {
        if c := cmp.Compare(a.Category, b.Category); c != 0 {
            return c
        }
        return cmp.Compare(a.Price, b.Price)
    })
    
    for _, p := range products {
        fmt.Printf("%s - %s: $%.2f\n", p.Category, p.Name, p.Price)
    }
    // Books - Go Programming: $19.99
    // Books - Clean Code: $29.99
    // Electronics - Mouse: $199.99
    // Electronics - Keyboard: $299.99
}

4. Or을 사용한 정렬

func main() {
    products := []Product{
        {Category: "Electronics", Price: 299.99, Name: "Keyboard"},
        {Category: "Books", Price: 19.99, Name: "Go Programming"},
        {Category: "Electronics", Price: 199.99, Name: "Mouse"},
    }
    
    // Or로 간결하게
    slices.SortFunc(products, func(a, b Product) int {
        return cmp.Or(
            cmp.Compare(a.Category, b.Category),
            cmp.Compare(a.Price, b.Price),
            cmp.Compare(a.Name, b.Name),
        )
    })
    
    // 첫 번째 비제로 비교 결과 반환
}

실전 예제

1. Min/Max 구현

func Min[T cmp.Ordered](a, b T) T {
    if cmp.Less(a, b) {
        return a
    }
    return b
}

func Max[T cmp.Ordered](a, b T) T {
    if cmp.Less(a, b) {
        return b
    }
    return a
}

func main() {
    fmt.Println(Min(10, 20))        // 10
    fmt.Println(Max(10, 20))        // 20
    
    fmt.Println(Min(3.14, 2.71))    // 2.71
    fmt.Println(Max("a", "z"))      // z
}

2. Clamp 함수

func Clamp[T cmp.Ordered](value, min, max T) T {
    if cmp.Less(value, min) {
        return min
    }
    if cmp.Less(max, value) {
        return max
    }
    return value
}

func main() {
    fmt.Println(Clamp(5, 0, 10))    // 5
    fmt.Println(Clamp(-5, 0, 10))   // 0
    fmt.Println(Clamp(15, 0, 10))   // 10
}

3. 이진 검색

func BinarySearch[T cmp.Ordered](slice []T, target T) int {
    left, right := 0, len(slice)-1
    
    for left <= right {
        mid := (left + right) / 2
        
        switch cmp.Compare(slice[mid], target) {
        case -1: // slice[mid] < target
            left = mid + 1
        case 1:  // slice[mid] > target
            right = mid - 1
        case 0:  // slice[mid] == target
            return mid
        }
    }
    
    return -1 // not found
}

func main() {
    numbers := []int{1, 3, 5, 7, 9, 11, 13}
    
    fmt.Println(BinarySearch(numbers, 7))   // 3
    fmt.Println(BinarySearch(numbers, 4))   // -1 (not found)
}

4. 범위 검사

type Range[T cmp.Ordered] struct {
    Min T
    Max T
}

func (r Range[T]) Contains(value T) bool {
    return !cmp.Less(value, r.Min) && !cmp.Less(r.Max, value)
}

func (r Range[T]) Overlaps(other Range[T]) bool {
    return !cmp.Less(r.Max, other.Min) && !cmp.Less(other.Max, r.Min)
}

func main() {
    r := Range[int]{Min: 10, Max: 20}
    
    fmt.Println(r.Contains(15))  // true
    fmt.Println(r.Contains(5))   // false
    fmt.Println(r.Contains(25))  // false
    
    r2 := Range[int]{Min: 15, Max: 25}
    fmt.Println(r.Overlaps(r2))  // true (15-20 겹침)
    
    r3 := Range[int]{Min: 25, Max: 30}
    fmt.Println(r.Overlaps(r3))  // false
}

5. 우선순위 큐

import "container/heap"

type PriorityQueue[T cmp.Ordered] []T

func (pq PriorityQueue[T]) Len() int { return len(pq) }

func (pq PriorityQueue[T]) Less(i, j int) bool {
    return cmp.Less(pq[i], pq[j])
}

func (pq PriorityQueue[T]) Swap(i, j int) {
    pq[i], pq[j] = pq[j], pq[i]
}

func (pq *PriorityQueue[T]) Push(x any) {
    *pq = append(*pq, x.(T))
}

func (pq *PriorityQueue[T]) Pop() any {
    old := *pq
    n := len(old)
    item := old[n-1]
    *pq = old[0 : n-1]
    return item
}

func main() {
    pq := &PriorityQueue[int]{}
    heap.Init(pq)
    
    heap.Push(pq, 5)
    heap.Push(pq, 2)
    heap.Push(pq, 8)
    heap.Push(pq, 1)
    
    for pq.Len() > 0 {
        fmt.Println(heap.Pop(pq))
    }
    // 1
    // 2
    // 5
    // 8
}

6. 버전 비교

type Version struct {
    Major int
    Minor int
    Patch int
}

func CompareVersions(a, b Version) int {
    return cmp.Or(
        cmp.Compare(a.Major, b.Major),
        cmp.Compare(a.Minor, b.Minor),
        cmp.Compare(a.Patch, b.Patch),
    )
}

func main() {
    v1 := Version{Major: 1, Minor: 2, Patch: 3}
    v2 := Version{Major: 1, Minor: 2, Patch: 5}
    v3 := Version{Major: 2, Minor: 0, Patch: 0}
    
    fmt.Println(CompareVersions(v1, v2))  // -1 (v1 < v2)
    fmt.Println(CompareVersions(v1, v3))  // -1 (v1 < v3)
    fmt.Println(CompareVersions(v2, v2))  // 0  (v2 == v2)
    
    versions := []Version{v3, v1, v2}
    slices.SortFunc(versions, CompareVersions)
    
    for _, v := range versions {
        fmt.Printf("v%d.%d.%d\n", v.Major, v.Minor, v.Patch)
    }
    // v1.2.3
    // v1.2.5
    // v2.0.0
}

7. 타임스탬프 비교

import "time"

type Event struct {
    Name      string
    Timestamp time.Time
}

func CompareEvents(a, b Event) int {
    // time.Time은 Ordered가 아니므로 Before/After 사용
    if a.Timestamp.Before(b.Timestamp) {
        return -1
    }
    if a.Timestamp.After(b.Timestamp) {
        return 1
    }
    // 같은 시간이면 이름으로
    return cmp.Compare(a.Name, b.Name)
}

func main() {
    events := []Event{
        {Name: "Event C", Timestamp: time.Now().Add(2 * time.Hour)},
        {Name: "Event A", Timestamp: time.Now()},
        {Name: "Event B", Timestamp: time.Now().Add(1 * time.Hour)},
    }
    
    slices.SortFunc(events, CompareEvents)
    
    for _, e := range events {
        fmt.Printf("%s at %s\n", e.Name, e.Timestamp.Format("15:04"))
    }
}

8. 커스텀 비교 함수

type Student struct {
    Name  string
    Grade int
    Score float64
}

// 성적순, 같으면 점수순
func CompareStudentsByGrade(a, b Student) int {
    return cmp.Or(
        cmp.Compare(b.Grade, a.Grade),  // 내림차순
        cmp.Compare(b.Score, a.Score),  // 내림차순
        cmp.Compare(a.Name, b.Name),    // 오름차순
    )
}

// 이름순
func CompareStudentsByName(a, b Student) int {
    return cmp.Compare(a.Name, b.Name)
}

func main() {
    students := []Student{
        {Name: "Alice", Grade: 90, Score: 85.5},
        {Name: "Bob", Grade: 95, Score: 92.0},
        {Name: "Carol", Grade: 90, Score: 88.0},
    }
    
    // 성적순
    slices.SortFunc(students, CompareStudentsByGrade)
    fmt.Println("By grade:")
    for _, s := range students {
        fmt.Printf("  %s: Grade %d, Score %.1f\n", s.Name, s.Grade, s.Score)
    }
    
    // 이름순
    slices.SortFunc(students, CompareStudentsByName)
    fmt.Println("By name:")
    for _, s := range students {
        fmt.Printf("  %s: Grade %d, Score %.1f\n", s.Name, s.Grade, s.Score)
    }
}

일반적인 실수

1. 타입 불일치

// ❌ 나쁜 예
func main() {
    var a int = 10
    var b int64 = 20
    // result := cmp.Compare(a, b)  // 컴파일 에러
}

// ✅ 좋은 예
func main() {
    var a int = 10
    var b int64 = 20
    result := cmp.Compare(int64(a), b)  // 타입 변환
}

2. NaN 비교

import "math"

// ❌ 예상과 다를 수 있음
func main() {
    nan := math.NaN()
    
    // NaN은 자기 자신과도 같지 않음
    fmt.Println(cmp.Compare(nan, nan))      // 구현 의존적
    fmt.Println(cmp.Less(nan, 1.0))         // 구현 의존적
}

// ✅ NaN 체크
func safeCompare(a, b float64) int {
    if math.IsNaN(a) || math.IsNaN(b) {
        // NaN 처리 로직
        return 0
    }
    return cmp.Compare(a, b)
}

3. Or의 제로 값 오해

// ❌ 잘못된 이해
func main() {
    // false도 제로 값이지만 Or는 Ordered 타입만 지원
    // result := cmp.Or(false, true)  // 컴파일 에러 (bool은 Ordered가 아님)
}

// ✅ 올바른 사용
func main() {
    // 숫자, 문자열만 사용
    result := cmp.Or(0, 10)           // OK
    str := cmp.Or("", "default")      // OK
}

4. 포인터 비교

// ❌ 나쁜 예
func main() {
    a, b := 10, 20
    pa, pb := &a, &b
    
    // 포인터는 Ordered가 아님
    // cmp.Compare(pa, pb)  // 컴파일 에러
}

// ✅ 좋은 예
func main() {
    a, b := 10, 20
    pa, pb := &a, &b
    
    // 값 역참조
    result := cmp.Compare(*pa, *pb)
}

5. 비교 결과 오해

// ❌ 나쁜 예 (부호만 확인)
func main() {
    result := cmp.Compare(10, 20)
    
    // -1, 0, 1만 반환하므로 정확히 비교
    if result > 0 {  // OK
        fmt.Println("Greater")
    }
}

// ✅ 명확한 비교
func main() {
    result := cmp.Compare(10, 20)
    
    switch result {
    case -1:
        fmt.Println("Less")
    case 0:
        fmt.Println("Equal")
    case 1:
        fmt.Println("Greater")
    }
}

6. Less와 LessOrEqual 혼동

// ❌ LessOrEqual 없음
func main() {
    a, b := 10, 10
    
    // cmp.LessOrEqual(a, b)  // 존재하지 않음
}

// ✅ 직접 구현
func LessOrEqual[T cmp.Ordered](a, b T) bool {
    return cmp.Compare(a, b) <= 0
}

func main() {
    fmt.Println(LessOrEqual(10, 10))  // true
    fmt.Println(LessOrEqual(10, 20))  // true
    fmt.Println(LessOrEqual(20, 10))  // false
}

7. 다중 키 정렬 실수

// ❌ 나쁜 예 (모든 비교 수행)
func compareWrong(a, b Person) int {
    c1 := cmp.Compare(a.Age, b.Age)
    c2 := cmp.Compare(a.Name, b.Name)
    
    if c1 != 0 {
        return c1
    }
    return c2
}

// ✅ 좋은 예 (Or 사용)
func compareGood(a, b Person) int {
    return cmp.Or(
        cmp.Compare(a.Age, b.Age),
        cmp.Compare(a.Name, b.Name),
    )
}

베스트 프랙티스

1. 타입 안전성 활용

// ✅ 제네릭으로 타입 안전성 보장
func FindMin[T cmp.Ordered](values []T) (T, bool) {
    if len(values) == 0 {
        var zero T
        return zero, false
    }
    
    min := values[0]
    for _, v := range values[1:] {
        if cmp.Less(v, min) {
            min = v
        }
    }
    return min, true
}

2. Or로 기본값 체인

// ✅ 여러 소스에서 값 가져오기
type Config struct {
    Host string
    Port int
}

func LoadConfig() *Config {
    return &Config{
        Host: cmp.Or(
            os.Getenv("HOST"),
            readConfigFile("host"),
            "localhost",
        ),
        Port: cmp.Or(
            parseEnvInt("PORT"),
            readConfigInt("port"),
            8080,
        ),
    }
}

3. 명확한 비교 함수

// ✅ 비교 로직을 명확하게 문서화
// CompareByPriority sorts items by priority (high to low),
// then by timestamp (old to new), then by name (A to Z).
func CompareByPriority(a, b Item) int {
    return cmp.Or(
        cmp.Compare(b.Priority, a.Priority),  // 높은 우선순위 먼저
        cmp.Compare(a.Timestamp, b.Timestamp), // 오래된 것 먼저
        cmp.Compare(a.Name, b.Name),           // 이름순
    )
}

4. 헬퍼 함수 작성

// ✅ 자주 사용하는 비교 패턴을 함수로
func InRange[T cmp.Ordered](value, min, max T) bool {
    return !cmp.Less(value, min) && !cmp.Less(max, value)
}

func Between[T cmp.Ordered](value, a, b T) bool {
    if cmp.Less(a, b) {
        return InRange(value, a, b)
    }
    return InRange(value, b, a)
}

5. 정렬 함수 재사용

// ✅ 정렬 기준을 함수로 분리
var (
    ByAge  = func(a, b Person) int { return cmp.Compare(a.Age, b.Age) }
    ByName = func(a, b Person) int { return cmp.Compare(a.Name, b.Name) }
)

func main() {
    people := []Person{...}
    
    slices.SortFunc(people, ByAge)
    // 사용
    
    slices.SortFunc(people, ByName)
    // 사용
}

6. 테스트 작성

func TestCompare(t *testing.T) {
    tests := []struct {
        name string
        a, b int
        want int
    }{
        {"less", 1, 2, -1},
        {"equal", 2, 2, 0},
        {"greater", 3, 2, 1},
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := cmp.Compare(tt.a, tt.b)
            if got != tt.want {
                t.Errorf("Compare(%d, %d) = %d; want %d",
                    tt.a, tt.b, got, tt.want)
            }
        })
    }
}

7. 인터페이스 정의

// ✅ 비교 가능한 타입을 인터페이스로
type Comparable[T any] interface {
    Compare(T) int
}

func Sort[T Comparable[T]](items []T) {
    slices.SortFunc(items, func(a, b T) int {
        return a.Compare(b)
    })
}

8. 문서화

// Compare returns:
//   -1 if a < b
//    0 if a == b
//    1 if a > b
//
// Example:
//
//	result := CompareProducts(p1, p2)
//	if result < 0 {
//	    fmt.Println("p1 is cheaper")
//	}
func CompareProducts(a, b Product) int {
    return cmp.Or(
        cmp.Compare(a.Price, b.Price),
        cmp.Compare(a.Name, b.Name),
    )
}

정리

  • 기본: Ordered 인터페이스 (정수, 실수, 문자열)
  • Compare: 삼원 비교 (-1, 0, 1)
  • Less: 작음 비교 (불린)
  • Or: 첫 번째 비제로 값 반환, 기본값 제공
  • 정렬: slices.SortFunc와 함께 사용
  • 실전: Min/Max, Clamp, 이진 검색, 범위, 우선순위 큐, 버전
  • 실수: 타입 불일치, NaN, Or 제로 값, 포인터, 결과 오해
  • 베스트: 타입 안전, Or 체인, 명확한 함수, 헬퍼, 재사용, 테스트, 문서화