[Go] maps package
개요
Go 1.21부터 추가된 maps 패키지는 제네릭 기반의 맵 조작 함수들을 제공합니다.
주요 특징:
- 제네릭 기반: 모든 타입의 맵 지원
- 표준 라이브러리: import “maps”
- 순회: All, Keys, Values로 다양한 방식 순회
- 복사: Clone (얕은 복사), Copy (병합)
- 비교: Equal, EqualFunc로 맵 비교
- 수정: Insert, DeleteFunc로 조작
- 변환: Collect로 Iterator를 맵으로 변환
- 안전성: nil 맵 처리 포함
기본 순회
1. All - 키와 값 모두
import "maps"
func main() {
m := map[string]int{
"apple": 1,
"banana": 2,
"cherry": 3,
}
// 키와 값 모두 순회
for k, v := range maps.All(m) {
fmt.Printf("%s: %d\n", k, v)
}
// apple: 1
// banana: 2
// cherry: 3
}
2. Keys - 키만
func main() {
m := map[string]int{
"apple": 1,
"banana": 2,
"cherry": 3,
}
// 키만 순회
for k := range maps.Keys(m) {
fmt.Println(k)
}
// apple
// banana
// cherry
// 키를 슬라이스로 수집
keys := make([]string, 0)
for k := range maps.Keys(m) {
keys = append(keys, k)
}
fmt.Println(keys) // [apple banana cherry]
}
3. Values - 값만
func main() {
m := map[string]int{
"apple": 1,
"banana": 2,
"cherry": 3,
}
// 값만 순회
for v := range maps.Values(m) {
fmt.Println(v)
}
// 1
// 2
// 3
// 합계 계산
sum := 0
for v := range maps.Values(m) {
sum += v
}
fmt.Println("Sum:", sum) // Sum: 6
}
4. Collect - Iterator를 맵으로
func main() {
// Iterator 생성
seq := maps.All(map[string]int{
"a": 1,
"b": 2,
"c": 3,
})
// 맵으로 변환
result := maps.Collect(seq)
fmt.Println(result) // map[a:1 b:2 c:3]
}
복사 함수
1. Clone - 얕은 복사
func main() {
original := map[string]int{
"a": 1,
"b": 2,
"c": 3,
}
// 얕은 복사
cloned := maps.Clone(original)
fmt.Println(cloned) // map[a:1 b:2 c:3]
// 원본 수정해도 복사본은 영향 없음
original["a"] = 100
fmt.Println(original) // map[a:100 b:2 c:3]
fmt.Println(cloned) // map[a:1 b:2 c:3]
// nil 맵도 안전
var nilMap map[string]int
clonedNil := maps.Clone(nilMap)
fmt.Println(clonedNil == nil) // true
}
2. Clone with 포인터 값
type Person struct {
Name string
Age int
}
func main() {
original := map[string]*Person{
"alice": {Name: "Alice", Age: 30},
"bob": {Name: "Bob", Age: 25},
}
// 포인터는 공유됨 (얕은 복사)
cloned := maps.Clone(original)
// 원본의 포인터가 가리키는 값 수정
original["alice"].Age = 31
fmt.Println(original["alice"].Age) // 31
fmt.Println(cloned["alice"].Age) // 31 (같은 객체)
}
3. Copy - 맵 병합
func main() {
dst := map[string]int{
"a": 1,
"b": 2,
}
src := map[string]int{
"b": 20, // 덮어씀
"c": 3, // 새로 추가
}
// src를 dst로 복사 (병합)
maps.Copy(dst, src)
fmt.Println(dst) // map[a:1 b:20 c:3]
}
4. Copy 활용 - 여러 맵 병합
func main() {
m1 := map[string]int{"a": 1}
m2 := map[string]int{"b": 2}
m3 := map[string]int{"c": 3}
// 모두 병합
result := make(map[string]int)
maps.Copy(result, m1)
maps.Copy(result, m2)
maps.Copy(result, m3)
fmt.Println(result) // map[a:1 b:2 c:3]
}
비교 함수
1. Equal - 동등 비교
func main() {
m1 := map[string]int{"a": 1, "b": 2}
m2 := map[string]int{"a": 1, "b": 2}
m3 := map[string]int{"a": 1, "b": 3}
m4 := map[string]int{"a": 1}
fmt.Println(maps.Equal(m1, m2)) // true
fmt.Println(maps.Equal(m1, m3)) // false (값 다름)
fmt.Println(maps.Equal(m1, m4)) // false (길이 다름)
// nil 맵 비교
var n1, n2 map[string]int
fmt.Println(maps.Equal(n1, n2)) // true (둘 다 nil)
// nil과 빈 맵
empty := make(map[string]int)
fmt.Println(maps.Equal(n1, empty)) // true (둘 다 빈 맵)
}
2. EqualFunc - 커스텀 비교
import "strings"
func main() {
m1 := map[string]string{
"a": "hello",
"b": "world",
}
m2 := map[string]string{
"a": "HELLO",
"b": "WORLD",
}
// 대소문자 무시 비교
equal := maps.EqualFunc(m1, m2, func(v1, v2 string) bool {
return strings.EqualFold(v1, v2)
})
fmt.Println(equal) // true
}
3. EqualFunc - 타입 변환 비교
import "strconv"
func main() {
m1 := map[string]int{
"a": 1,
"b": 2,
}
m2 := map[string]string{
"a": "1",
"b": "2",
}
// int와 string 비교
equal := maps.EqualFunc(m1, m2, func(v1 int, v2 string) bool {
return strconv.Itoa(v1) == v2
})
fmt.Println(equal) // true
}
수정 함수
1. DeleteFunc - 조건부 삭제
func main() {
m := map[string]int{
"a": 1,
"b": 2,
"c": 3,
"d": 4,
"e": 5,
}
// 짝수 값 삭제
maps.DeleteFunc(m, func(k string, v int) bool {
return v%2 == 0
})
fmt.Println(m) // map[a:1 c:3 e:5]
}
2. DeleteFunc - 키 조건
import "strings"
func main() {
m := map[string]int{
"apple": 1,
"banana": 2,
"apricot": 3,
"cherry": 4,
}
// 'a'로 시작하는 키 삭제
maps.DeleteFunc(m, func(k string, v int) bool {
return strings.HasPrefix(k, "a")
})
fmt.Println(m) // map[banana:2 cherry:4]
}
3. Insert - 맵 삽입
func main() {
m := map[string]int{
"a": 1,
"b": 2,
}
// Iterator에서 삽입
toInsert := map[string]int{
"c": 3,
"d": 4,
}
maps.Insert(m, maps.All(toInsert))
fmt.Println(m) // map[a:1 b:2 c:3 d:4]
}
4. Insert - 덮어쓰기
func main() {
m := map[string]int{
"a": 1,
"b": 2,
}
// 기존 키가 있으면 덮어씀
updates := map[string]int{
"a": 100,
"c": 3,
}
maps.Insert(m, maps.All(updates))
fmt.Println(m) // map[a:100 b:2 c:3]
}
실전 예제
1. 맵 필터링
func FilterMap[K comparable, V any](m map[K]V, pred func(K, V) bool) map[K]V {
result := make(map[K]V)
for k, v := range maps.All(m) {
if pred(k, v) {
result[k] = v
}
}
return result
}
func main() {
scores := map[string]int{
"Alice": 85,
"Bob": 92,
"Carol": 78,
"Dave": 95,
}
// 90점 이상만
highScores := FilterMap(scores, func(name string, score int) bool {
return score >= 90
})
fmt.Println(highScores) // map[Bob:92 Dave:95]
}
2. 맵 변환
func MapValues[K comparable, V1, V2 any](m map[K]V1, f func(V1) V2) map[K]V2 {
result := make(map[K]V2, len(m))
for k, v := range maps.All(m) {
result[k] = f(v)
}
return result
}
func main() {
prices := map[string]float64{
"apple": 1.5,
"banana": 0.8,
"cherry": 2.0,
}
// 10% 할인
discounted := MapValues(prices, func(price float64) float64 {
return price * 0.9
})
fmt.Println(discounted)
// map[apple:1.35 banana:0.72 cherry:1.8]
}
3. 맵 역전 (Key ↔ Value)
func InvertMap[K, V comparable](m map[K]V) map[V]K {
result := make(map[V]K, len(m))
for k, v := range maps.All(m) {
result[v] = k
}
return result
}
func main() {
original := map[string]int{
"one": 1,
"two": 2,
"three": 3,
}
inverted := InvertMap(original)
fmt.Println(inverted) // map[1:one 2:two 3:three]
}
4. 맵 그룹화
func GroupBy[K comparable, V any](items []V, keyFunc func(V) K) map[K][]V {
result := make(map[K][]V)
for _, item := range items {
key := keyFunc(item)
result[key] = append(result[key], item)
}
return result
}
type Student struct {
Name string
Grade int
}
func main() {
students := []Student{
{"Alice", 90},
{"Bob", 85},
{"Carol", 90},
{"Dave", 85},
{"Eve", 95},
}
// 성적별 그룹화
byGrade := GroupBy(students, func(s Student) int {
return s.Grade
})
for grade, studs := range byGrade {
fmt.Printf("Grade %d: ", grade)
for _, s := range studs {
fmt.Printf("%s ", s.Name)
}
fmt.Println()
}
// Grade 85: Bob Dave
// Grade 90: Alice Carol
// Grade 95: Eve
}
5. 맵 병합 (충돌 해결)
func MergeWith[K comparable, V any](
m1, m2 map[K]V,
resolve func(V, V) V,
) map[K]V {
result := maps.Clone(m1)
for k, v2 := range maps.All(m2) {
if v1, exists := result[k]; exists {
result[k] = resolve(v1, v2)
} else {
result[k] = v2
}
}
return result
}
func main() {
m1 := map[string]int{"a": 1, "b": 2}
m2 := map[string]int{"b": 3, "c": 4}
// 값 합산
merged := MergeWith(m1, m2, func(v1, v2 int) int {
return v1 + v2
})
fmt.Println(merged) // map[a:1 b:5 c:4]
// 최댓값 선택
m3 := map[string]int{"x": 10, "y": 5}
m4 := map[string]int{"y": 8, "z": 3}
maxMerged := MergeWith(m3, m4, func(v1, v2 int) int {
if v1 > v2 {
return v1
}
return v2
})
fmt.Println(maxMerged) // map[x:10 y:8 z:3]
}
6. 맵 차집합
func Difference[K comparable, V any](m1, m2 map[K]V) map[K]V {
result := make(map[K]V)
for k, v := range maps.All(m1) {
if _, exists := m2[k]; !exists {
result[k] = v
}
}
return result
}
func main() {
m1 := map[string]int{"a": 1, "b": 2, "c": 3}
m2 := map[string]int{"b": 2, "d": 4}
diff := Difference(m1, m2)
fmt.Println(diff) // map[a:1 c:3]
}
7. 맵 교집합
func Intersection[K, V comparable](m1, m2 map[K]V) map[K]V {
result := make(map[K]V)
for k, v1 := range maps.All(m1) {
if v2, exists := m2[k]; exists && v1 == v2 {
result[k] = v1
}
}
return result
}
func main() {
m1 := map[string]int{"a": 1, "b": 2, "c": 3}
m2 := map[string]int{"b": 2, "c": 4, "d": 5}
inter := Intersection(m1, m2)
fmt.Println(inter) // map[b:2] (값도 같아야 함)
}
8. 캐시 구현
import (
"sync"
"time"
)
type CacheItem[V any] struct {
Value V
Expiration time.Time
}
type Cache[K comparable, V any] struct {
items map[K]CacheItem[V]
mu sync.RWMutex
}
func NewCache[K comparable, V any]() *Cache[K, V] {
c := &Cache[K, V]{
items: make(map[K]CacheItem[V]),
}
// 주기적 정리
go c.cleanup()
return c
}
func (c *Cache[K, V]) Set(key K, value V, ttl time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
c.items[key] = CacheItem[V]{
Value: value,
Expiration: time.Now().Add(ttl),
}
}
func (c *Cache[K, V]) Get(key K) (V, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
item, exists := c.items[key]
if !exists {
var zero V
return zero, false
}
if time.Now().After(item.Expiration) {
var zero V
return zero, false
}
return item.Value, true
}
func (c *Cache[K, V]) cleanup() {
ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop()
for range ticker.C {
c.mu.Lock()
now := time.Now()
maps.DeleteFunc(c.items, func(k K, item CacheItem[V]) bool {
return now.After(item.Expiration)
})
c.mu.Unlock()
}
}
func (c *Cache[K, V]) Len() int {
c.mu.RLock()
defer c.mu.RUnlock()
return len(c.items)
}
func main() {
cache := NewCache[string, int]()
cache.Set("a", 1, 5*time.Second)
cache.Set("b", 2, 10*time.Second)
if val, ok := cache.Get("a"); ok {
fmt.Println("a:", val) // a: 1
}
time.Sleep(6 * time.Second)
if _, ok := cache.Get("a"); !ok {
fmt.Println("a expired") // a expired
}
}
일반적인 실수
1. Clone의 얕은 복사 오해
// ❌ 나쁜 예 (깊은 복사 기대)
func main() {
type Data struct {
Value int
}
original := map[string]*Data{
"a": {Value: 1},
}
cloned := maps.Clone(original)
// 같은 객체를 참조
original["a"].Value = 100
fmt.Println(cloned["a"].Value) // 100 (변경됨!)
}
// ✅ 좋은 예 (수동 깊은 복사)
func DeepClone(original map[string]*Data) map[string]*Data {
result := make(map[string]*Data, len(original))
for k, v := range maps.All(original) {
result[k] = &Data{Value: v.Value}
}
return result
}
2. Copy 방향 혼동
// ❌ 나쁜 예 (인자 순서 혼동)
func main() {
src := map[string]int{"a": 1}
dst := map[string]int{"b": 2}
// src로 복사된다고 착각
maps.Copy(src, dst)
fmt.Println(src) // map[a:1 b:2] (src가 수정됨)
}
// ✅ 좋은 예 (명확한 의도)
func main() {
src := map[string]int{"a": 1}
dst := make(map[string]int)
// dst에 src 복사
maps.Copy(dst, src)
fmt.Println(dst) // map[a:1]
}
3. Equal과 참조 비교 혼동
// ❌ 나쁜 예 (같은 맵이어야 한다고 착각)
func main() {
m1 := map[string]int{"a": 1}
m2 := map[string]int{"a": 1}
// 참조 비교
if m1 == m2 { // 컴파일 에러: invalid operation
fmt.Println("Equal")
}
}
// ✅ 좋은 예 (값 비교)
func main() {
m1 := map[string]int{"a": 1}
m2 := map[string]int{"a": 1}
if maps.Equal(m1, m2) {
fmt.Println("Equal") // Equal
}
}
4. DeleteFunc 조건 오류
// ❌ 나쁜 예 (삭제 조건 반대)
func main() {
m := map[string]int{"a": 1, "b": 2, "c": 3}
// "남길" 조건을 잘못 사용
maps.DeleteFunc(m, func(k string, v int) bool {
return v > 1 // 2, 3을 삭제
})
fmt.Println(m) // map[a:1] (의도와 다름)
}
// ✅ 좋은 예 (명확한 조건)
func main() {
m := map[string]int{"a": 1, "b": 2, "c": 3}
// 1보다 큰 값을 "유지"하려면 반대로
maps.DeleteFunc(m, func(k string, v int) bool {
return v <= 1 // 1 이하를 삭제
})
fmt.Println(m) // map[b:2 c:3]
}
5. nil 맵에 Insert
// ❌ 나쁜 예 (nil 맵에 삽입)
func main() {
var m map[string]int
src := map[string]int{"a": 1}
maps.Insert(m, maps.All(src)) // 패닉!
}
// ✅ 좋은 예 (맵 초기화)
func main() {
m := make(map[string]int)
src := map[string]int{"a": 1}
maps.Insert(m, maps.All(src))
fmt.Println(m) // map[a:1]
}
6. EqualFunc 파라미터 순서
// ❌ 나쁜 예 (파라미터 순서 혼동)
func main() {
m1 := map[string]int{"a": 1}
m2 := map[string]string{"a": "1"}
// v1은 int, v2는 string
equal := maps.EqualFunc(m1, m2, func(v1 string, v2 int) bool {
// 타입 에러!
return v1 == strconv.Itoa(v2)
})
}
// ✅ 좋은 예 (올바른 타입 순서)
func main() {
m1 := map[string]int{"a": 1}
m2 := map[string]string{"a": "1"}
equal := maps.EqualFunc(m1, m2, func(v1 int, v2 string) bool {
return strconv.Itoa(v1) == v2
})
fmt.Println(equal) // true
}
7. Keys/Values 순서 기대
// ❌ 나쁜 예 (순서 보장 기대)
func main() {
m := map[string]int{"a": 1, "b": 2, "c": 3}
keys := make([]string, 0)
for k := range maps.Keys(m) {
keys = append(keys, k)
}
// 순서가 매번 다를 수 있음
fmt.Println(keys)
}
// ✅ 좋은 예 (명시적 정렬)
import "sort"
func main() {
m := map[string]int{"a": 1, "b": 2, "c": 3}
keys := make([]string, 0, len(m))
for k := range maps.Keys(m) {
keys = append(keys, k)
}
sort.Strings(keys)
fmt.Println(keys) // [a b c] (정렬됨)
}
베스트 프랙티스
1. 용량 사전 할당
// ✅ 크기를 알 때 사전 할당
func FilterLargeMap(m map[string]int, threshold int) map[string]int {
// 최대 크기로 할당
result := make(map[string]int, len(m))
for k, v := range maps.All(m) {
if v > threshold {
result[k] = v
}
}
return result
}
2. Clone으로 불변성 보장
// ✅ 원본 보호
func ProcessMap(m map[string]int) map[string]int {
// 원본 보호를 위해 복사
working := maps.Clone(m)
// working 수정
maps.DeleteFunc(working, func(k string, v int) bool {
return v < 0
})
return working
}
3. EqualFunc로 커스텀 비교
// ✅ 유연한 비교
type Product struct {
ID int
Price float64
}
func CompareProductMaps(m1, m2 map[string]Product) bool {
return maps.EqualFunc(m1, m2, func(p1, p2 Product) bool {
// ID만 비교 (Price 무시)
return p1.ID == p2.ID
})
}
4. DeleteFunc로 정리
// ✅ 조건부 정리
type Session struct {
UserID string
ExpiresAt time.Time
}
func CleanupSessions(sessions map[string]Session) {
now := time.Now()
maps.DeleteFunc(sessions, func(id string, s Session) bool {
return now.After(s.ExpiresAt)
})
}
5. 제네릭 유틸리티 작성
// ✅ 재사용 가능한 함수
func PickKeys[K comparable, V any](m map[K]V, keys []K) map[K]V {
result := make(map[K]V, len(keys))
for _, k := range keys {
if v, exists := m[k]; exists {
result[k] = v
}
}
return result
}
func main() {
m := map[string]int{"a": 1, "b": 2, "c": 3, "d": 4}
picked := PickKeys(m, []string{"a", "c", "e"})
fmt.Println(picked) // map[a:1 c:3]
}
6. 에러 처리
// ✅ 안전한 접근
func SafeMerge[K comparable, V any](
m1, m2 map[K]V,
) (map[K]V, error) {
if m1 == nil || m2 == nil {
return nil, fmt.Errorf("nil map provided")
}
result := maps.Clone(m1)
maps.Copy(result, m2)
return result, nil
}
7. 문서화
// ✅ 명확한 문서화
// MergeUnique merges two maps, returning an error if any key exists in both.
// Time complexity: O(n+m) where n and m are the sizes of the input maps.
func MergeUnique[K comparable, V any](
m1, m2 map[K]V,
) (map[K]V, error) {
result := maps.Clone(m1)
for k, v := range maps.All(m2) {
if _, exists := result[k]; exists {
return nil, fmt.Errorf("duplicate key: %v", k)
}
result[k] = v
}
return result, nil
}
8. 테스트 작성
func TestFilterMap(t *testing.T) {
tests := []struct {
name string
input map[string]int
pred func(string, int) bool
want map[string]int
}{
{
name: "filter even values",
input: map[string]int{"a": 1, "b": 2, "c": 3, "d": 4},
pred: func(k string, v int) bool {
return v%2 == 0
},
want: map[string]int{"b": 2, "d": 4},
},
{
name: "empty map",
input: map[string]int{},
pred: func(k string, v int) bool {
return true
},
want: map[string]int{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := FilterMap(tt.input, tt.pred)
if !maps.Equal(got, tt.want) {
t.Errorf("got %v, want %v", got, tt.want)
}
})
}
}
정리
- 기본: All, Keys, Values로 다양한 순회, Collect로 변환
- 복사: Clone (얕은 복사), Copy (병합)
- 비교: Equal (동등), EqualFunc (커스텀)
- 수정: DeleteFunc (조건 삭제), Insert (삽입)
- 실전: 필터링, 변환, 역전, 그룹화, 병합, 차집합, 교집합, 캐시
- 실수: Clone 얕은 복사, Copy 방향, Equal vs ==, DeleteFunc 조건, nil 맵, EqualFunc 순서, Keys 순서
- 베스트: 용량 할당, Clone 불변성, EqualFunc 활용, DeleteFunc 정리, 제네릭, 에러 처리, 문서화, 테스트