Go의 slice와 map은 배열과 해시맵을 직접 쓰는 느낌보다 참조형 데이터 구조의 수명과 초기화 방식으로 읽어야 합니다.
숏컷 코드
names := []string{"Mina", "Jin"}
names = append(names, "Noah")
scores := map[string]int{"Mina": 10}
scores["Jin"] = 8
buf := make([]byte, 0, 1024)
seen := make(map[string]bool)문법
slice는 배열 위의 창이다
nums := []int{1, 2, 3}
part := nums[1:3]slice는 배열 자체가 아니라, 배열 일부를 가리키는 header입니다. 그래서 slicing 결과는 보통 원본 배열을 공유합니다.
nums := []int{1, 2, 3}
part := nums[:2]
part[0] = 99
fmt.Println(nums[0]) // 99이 동작 때문에 slice를 넘길 때는 "값 복사"보다 "같은 backing array를 볼 수 있는가"를 같이 봐야 합니다.
append는 새 slice를 반환한다
items := []string{"a"}
items = append(items, "b")append는 기존 backing array에 붙일 수도 있고, capacity가 부족하면 새 backing array를 만들 수도 있습니다. 그래서 반환값을 다시 받아야 합니다.
// 반환값을 버리면 새 slice header를 잃을 수 있음
items = append(items, "c")map은 읽기와 쓰기의 nil 동작이 다르다
var counts map[string]int
fmt.Println(counts["x"]) // 0
// counts["x"] = 1 // panicnil map에서 읽기는 zero value를 돌려주지만, 쓰기는 panic입니다. 쓰기 전에 literal이나 make로 초기화해야 합니다.
counts := make(map[string]int)
counts["x"]++make
make는 slice, map, channel 초기화에 쓴다
users := make([]User, 0, 100)
index := make(map[string]User)
jobs := make(chan Job, 10)new가 포인터를 돌려주는 일반 할당이라면, make는 slice, map, channel처럼 내부 런타임 구조가 필요한 타입을 바로 사용 가능한 상태로 만듭니다.
길이와 용량을 구분한다
buf := make([]byte, 0, 1024)
fmt.Println(len(buf)) // 0
fmt.Println(cap(buf)) // 1024길이(len)는 지금 실제로 들어 있는 요소 수이고, 용량(cap)은 재할당 없이 늘릴 수 있는 여유입니다.
선택 기준
| 상황 | 먼저 떠올릴 선택 |
|---|---|
| 순서 있는 목록 | slice |
| key-value 조회 | map |
| append를 자주 함 | make([]T, 0, cap) |
| map에 쓰기 예정 | literal 또는 make(map[K]V) |
| 원본과 공유하지 않을 복사본 필요 | copy 또는 append([]T(nil), src...) |
주의할 점
slice와 map은 겉으로 값처럼 보이지만, 내부 데이터 공유와 초기화 상태가 중요합니다. slice를 잘라 넘기면 원본 backing array를 공유할 수 있고, nil map은 읽기는 되지만 쓰기는 panic입니다. append 반환값을 다시 받고, map은 쓰기 전에 초기화하는 흐름을 기본값으로 두세요.
참고 링크
3 sources