Go 맵
일반적으로 맵 데이터 구조 구현은 주로 두 가지 방식이 있습니다. 해시 테이블 (hash table) 과 검색 트리 (search tree) 입니다. 차이는 전자는 무순서이고 후자는 순서가 있다는 점입니다. Go 에서 map 의 구현은 해시 버킷 (해시 테이블의 일종) 을 기반으로 하므로 무순서입니다. 본 절에서는 구현 원리에 대해 자세히 다루지 않으며, 이는 기초 범위를 벗어나고 이후에 심층 분석할 예정입니다.
TIP
맵의 원리에 대해 알고 싶다면 맵 구현 을 참고하세요.
초기화
Go 에서 맵의 키 타입은 반드시 비교 가능해야 합니다. 예를 들어 string, int 는 비교 가능하지만 []int 는 비교 가능하지 않으므로 맵의 키로 사용할 수 없습니다. 맵을 초기화하는 방법은 두 가지가 있습니다. 첫 번째는 리터럴로, 형식은 다음과 같습니다.
map[keyType]valueType{}예를 들어 몇 가지 들겠습니다.
mp := map[int]string{
0: "a",
1: "a",
2: "a",
3: "a",
4: "a",
}
mp := map[string]int{
"a": 0,
"b": 22,
"c": 33,
}두 번째 방법은 내장 함수 make 를 사용하는 것입니다. 맵의 경우 두 개의 매개변수를 받으며, 각각 타입과 초기 용량입니다. 예시는 다음과 같습니다.
mp := make(map[string]int, 8)
mp := make(map[string][]int, 10)맵은 참조 타입이며, 영값이거나 초기화되지 않은 맵은 액세스할 수 있지만 요소를 저장할 수 없으므로 반드시 메모리를 할당해야 합니다.
func main() {
var mp map[string]int
mp["a"] = 1
fmt.Println(mp)
}panic: assignment to entry in nil mapTIP
맵을 초기화할 때는 합리적인 용량을 할당하는 것이 좋으며, 이는 확장 횟수를 줄일 수 있습니다.
액세스
맵에 액세스하는 방식은 인덱스로 배열에 액세스하는 것과 유사합니다.
func main() {
mp := map[string]int{
"a": 0,
"b": 1,
"c": 2,
"d": 3,
}
fmt.Println(mp["a"])
fmt.Println(mp["b"])
fmt.Println(mp["d"])
fmt.Println(mp["f"])
}0
1
3
0코드를 통해 관찰할 수 있듯이, 맵에 "f" 라는 키 - 값 쌍이 없더라도 반환값이 있습니다. 맵은 존재하지 않는 키에 대해 해당 타입의 영값을 반환하며, 맵을 액세스할 때는 실제로 두 개의 반환값이 있습니다. 첫 번째 반환값은 해당 타입의 값이고, 두 번째 반환값은 불리언으로 키가 존재하는지 여부를 나타냅니다. 예를 들어:
func main() {
mp := map[string]int{
"a": 0,
"b": 1,
"c": 2,
"d": 3,
}
if val, exist := mp["f"]; exist {
fmt.Println(val)
} else {
fmt.Println("키가 존재하지 않습니다")
}
}맵의 길이 구하기
func main() {
mp := map[string]int{
"a": 0,
"b": 1,
"c": 2,
"d": 3,
}
fmt.Println(len(mp))
}값 저장
맵에 값을 저장하는 방식도 배열에 값을 저장하는 것과 유사합니다. 예를 들어:
func main() {
mp := make(map[string]int, 10)
mp["a"] = 1
mp["b"] = 2
fmt.Println(mp)
}값을 저장할 때 이미 존재하는 키를 사용하면 기존 값이 덮어씌워집니다.
func main() {
mp := make(map[string]int, 10)
mp["a"] = 1
mp["b"] = 2
if _, exist := mp["b"]; exist {
mp["b"] = 3
}
fmt.Println(mp)
}하지만 특별한 경우도 있습니다. 바로 키가 math.NaN() 일 때입니다.
func main() {
mp := make(map[float64]string, 10)
mp[math.NaN()] = "a"
mp[math.NaN()] = "b"
mp[math.NaN()] = "c"
_, exist := mp[math.NaN()]
fmt.Println(exist)
fmt.Println(mp)
}false
map[NaN:c NaN:a NaN:b]결과를 통해 관찰할 수 있듯이, 동일한 키 값이 덮어씌워지지 않았을 뿐만 아니라 여러 개 존재할 수 있으며, 존재하는지 여부도 판단할 수 없어 정상적으로 값을 가져올 수 없습니다. NaN 이 IEE754 표준에 정의된 것이며, 그 구현은 하단 어셈블리 명령어 UCOMISD 로 완료되기 때문입니다. 이는 무순서 배정밀도 부동소수점 비교 명령어로, 이 명령어는 NaN 상황을 고려합니다. 따라서 어떤 숫자도 NaN 과 같지 않으며, NaN 은 자기 자신과도 같지 않습니다. 이로 인해 매번 해시값이 달라집니다. 이에 대해 커뮤니티에서도 치열한 논의가 있었지만, 공식은 수정할 필요가 없다고 판단했으므로 NaN 을 맵의 키로 사용하는 것은 최대한 피해야 합니다.
삭제
func delete(m map[Type]Type1, key Type)키 - 값 쌍을 삭제하려면 내장 함수 delete 를 사용해야 합니다. 예를 들어
func main() {
mp := map[string]int{
"a": 0,
"b": 1,
"c": 2,
"d": 3,
}
fmt.Println(mp)
delete(mp, "a")
fmt.Println(mp)
}map[a:0 b:1 c:2 d:3]
map[b:1 c:2 d:3]주의할 점은 값이 NaN 일 경우 해당 키 - 값 쌍을 삭제할 수도 없다는 것입니다.
func main() {
mp := make(map[float64]string, 10)
mp[math.NaN()] = "a"
mp[math.NaN()] = "b"
mp[math.NaN()] = "c"
fmt.Println(mp)
delete(mp, math.NaN())
fmt.Println(mp)
}map[NaN:c NaN:a NaN:b]
map[NaN:c NaN:a NaN:b]순회
for range 를 사용하여 맵을 순회할 수 있습니다. 예를 들어
func main() {
mp := map[string]int{
"a": 0,
"b": 1,
"c": 2,
"d": 3,
}
for key, val := range mp {
fmt.Println(key, val)
}
}c 2
d 3
a 0
b 1결과가 무순서임을 알 수 있으며, 이는 맵이 무순서 저장임을 입증합니다.值得一提的是, NaN 은 정상적으로 가져올 수는 없지만 순회를 통해 액세스할 수 있습니다. 예를 들어
func main() {
mp := make(map[float64]string, 10)
mp[math.NaN()] = "a"
mp[math.NaN()] = "b"
mp[math.NaN()] = "c"
for key, val := range mp {
fmt.Println(key, val)
}
}NaN a
NaN c
NaN b비우기
go1.21 이전에는 맵을 비우려면 맵의 모든 키에 대해 delete 를 수행해야 했습니다.
func main() {
m := map[string]int{
"a": 1,
"b": 2,
}
for k, _ := range m {
delete(m, k)
}
fmt.Println(m)
}하지만 go1.21 에서 clear 함수가 업데이트되어 더 이상 이전과 같은 작업을 수행할 필요가 없으며, clear 하나만으로 비울 수 있습니다.
func main() {
m := map[string]int{
"a": 1,
"b": 2,
}
clear(m)
fmt.Println(m)
}출력
map[]Set
Set 은 무순서이며 중복 요소를 포함하지 않는 집합입니다. Go 는 이와 유사한 데이터 구조 구현을 제공하지 않지만, 맵의 키가 무순서이며 중복될 수 없으므로 맵을 사용하여 set 을 대체할 수 있습니다.
func main() {
set := make(map[int]struct{}, 10)
for i := 0; i < 10; i++ {
set[rand.Intn(100)] = struct{}{}
}
fmt.Println(set)
}map[0:{} 18:{} 25:{} 40:{} 47:{} 56:{} 59:{} 81:{} 87:{}]TIP
빈 구조체는 메모리를 차지하지 않습니다.
주의사항
맵은 동시성 안전 데이터 구조가 아닙니다. Go 팀은 대부분의 경우 맵 사용 시 고동시성 시나리오가 포함되지 않으며, 뮤텍스 잠금을 도입하면 성능이 크게 저하된다고 판단했습니다. 맵 내부에는 읽기/쓰기 감지 메커니즘이 있어 충돌 시 fatal error 가 발생합니다. 예를 들어 다음 상황에서는 fatal 이 발생할 가능성이 매우 큽니다.
func main() {
group.Add(10)
// map
mp := make(map[string]int, 10)
for i := 0; i < 10; i++ {
go func() {
// 쓰기 작업
for i := 0; i < 100; i++ {
mp["helloworld"] = 1
}
// 읽기 작업
for i := 0; i < 10; i++ {
fmt.Println(mp["helloworld"])
}
group.Done()
}()
}
group.Wait()
}fatal error: concurrent map writes이러한 경우 sync.Map 를 사용하여 대체해야 합니다.
