Menu

Golang Set: map[T]struct{}, 집합 연산, 제네릭 Set

Go에는 내장 집합 타입이 없습니다. 표준 관용구는 빈 구조체를 값으로 쓰는 맵입니다. 추가, 확인, 삭제와 합집합, 교집합, 차집합, 그리고 작은 제네릭 Set을 만드는 법을 알아봅니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

관용구: 값이 빈 맵

Go에는 set 키워드도 없고 표준 라이브러리에 집합도 없습니다. 원소를 키로 삼고 값에는 아무것도 담지 않는 맵으로 충분합니다.

struct{}는 빈 구조체 타입이고 struct{}{}는 그 유일한 값입니다. 0바이트를 차지하므로 맵은 키만 저장합니다.

맵의 규칙이 모두 적용됩니다. 키는 비교 가능해야 하고, 순회 순서는 무작위이며, nil 맵에 쓰면 패닉이 나고, 동시에 쓰려면 잠금이 필요합니다. 각각은 페이지에서 다룹니다.

map[T]struct{}와 map[T]bool

다른 흔한 표기는 map[T]bool입니다. 없는 키가 false를 반환하므로 더 잘 읽힙니다:

seen := map[string]bool{}
seen["a"] = true
if seen["a"] { ... }

장단점은 다음과 같습니다:

map[T]struct{}map[T]bool
값 크기0바이트1바이트(정렬 여백 별도)
포함 여부 확인_, ok := s[k]s[k]
모호함없음s[k] = false라는 세 번째 상태

많은 코드베이스가 빈 구조체를 선호하는 진짜 이유는 세 번째 행입니다. bool을 쓰면 언젠가 누군가 s[k] = false라고 쓰고, 그때부터 len(s)는 원소 개수가 아니게 됩니다. 작은 집합에서는 메모리 차이가 문제되지 않습니다.

슬라이스에서 중복 제거하기

집합의 가장 흔한 용도는 중복 제거입니다. 다음 코드는 각 값이 처음 나온 것을 남기고 순서를 유지합니다:

출력:

[b a c]
[3 1 2]
[1 2 3]

합집합, 교집합, 차집합

집합 연산은 반복문 몇 개면 됩니다. 조회는 평균적으로 상수 시간이므로, 다른 집합에 포함되는지 확인할 때는 작은 집합을 순회하세요.

sorted는 출력을 안정적으로 만들기 위해서만 있습니다. 집합을 range로 순회해 출력하면 실행할 때마다 순서가 달라집니다. maps.Keysslices.Sorted는 Go 1.23이 필요합니다.

type set map[string]struct{} 같은 이름 있는 타입도 여전히 맵입니다. 인덱싱, range, delete를 똑같이 쓸 수 있고, 메서드도 붙일 수 있습니다.

작은 제네릭 Set

제네릭(Go 1.18)을 쓰면 타입 하나로 비교 가능한 모든 원소 타입을 다룰 수 있습니다. 대부분의 프로그램에는 이 정도면 충분합니다:

맵을 구조체로 감싸면 struct{}{}라는 잡음이 가려지고, 생성자가 맵을 반드시 만들어 주므로 nil 맵 패닉도 사라집니다. Sorted는 메서드가 될 수 없습니다. 메서드는 자기만의 타입 매개변수를 선언하거나 타입의 comparable 제약을 좁힐 수 없는데, 정렬에는 cmp.Ordered가 필요하기 때문입니다. 제약은 제네릭 페이지에서 설명합니다.

기능이 풍부한 집합(스레드 안전 변형, 다양한 연산)이 필요하다면 github.com/deckarep/golang-set 같은 서드파티 패키지가 있습니다. 대부분의 코드에서 Go 개발자들은 맵 관용구나 이 정도의 30줄짜리 타입을 씁니다.

구조체 집합

비교 가능한 타입이라면 무엇이든 원소가 될 수 있고, 비교 가능한 필드로 된 구조체도 마찬가지입니다. 그래서 "이 쌍을 본 적이 있나" 같은 확인을 바로 할 수 있습니다:

type edge struct{ from, to string }
visited := map[edge]struct{}{}
visited[edge{"a", "b"}] = struct{}{}

슬라이스와 맵은 집합의 원소가 될 수 없습니다. 고유한 슬라이스를 추적하려면 먼저 각 슬라이스를 비교 가능한 키로 바꾸세요. 예를 들어 고정 크기 배열이나 fmt.Sprint로 만든 문자열을 쓸 수 있습니다.

흔한 실수

  • 초기화를 잊음. var s map[string]struct{}는 nil이라서 처음 추가할 때 패닉이 납니다.
  • 집합을 출력하면서 안정적인 출력을 기대함. 원소를 먼저 정렬하세요.
  • map[T]bool을 쓰면서 false를 저장함. 그러면 len이 더 이상 원소 개수를 세지 않습니다. 제거할 때는 delete를 쓰세요.

자주 묻는 질문

Go에 집합(set) 타입이 있나요?

없습니다. 표준 라이브러리에도 집합이 없습니다. 관용구는 값에 아무 정보도 담지 않는 맵입니다: map[string]struct{}. 추가는 s[k] = struct{}{}, 포함 여부는 _, ok := s[k], 삭제는 delete(s, k), 크기는 len(s)입니다.

Go에서 집합으로 map[T]bool과 map[T]struct{} 중 무엇을 써야 하나요?

map[T]struct{}는 의도가 분명하고 값이 0바이트를 차지합니다. map[T]bool은 없는 키가 false를 반환하므로 더 자연스럽게 읽힙니다(if seen[x]). 둘 다 올바르며, 메모리 차이는 아주 큰 집합에서만 의미가 있습니다. 하나를 골라 일관되게 쓰세요.

Go에서 슬라이스의 중복을 제거하려면 어떻게 하나요?

처음 나온 값을 순서대로 남기려면, 반복하면서 본 값을 map[T]struct{}에 기록하고 처음 보는 값만 append합니다. 순서가 상관없다면 정렬한 뒤 인접한 중복을 버립니다: slices.Sort(s); s = slices.Compact(s).

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기