[Go] cmp
개요
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 체인, 명확한 함수, 헬퍼, 재사용, 테스트, 문서화