Quick Syntax
Go의 slice와 map은 값으로 전달되지만 내부 배열·hash table을 공유할 수 있습니다. 그래서 복사 범위와 초기화 상태를 함께 봐야 합니다.
names := []string{"Mina", "Jin"}
names = append(names, "Noah")
scores := make(map[string]int)
scores["Mina"] = 10
if score, ok := scores["Jin"]; ok {
_ = score
}
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가 남은 capacity를 쓰면 원본의 뒤쪽 요소도 바뀔 수 있습니다. 독립된 결과가 필요하면 복사하거나 full slice expression으로 capacity를 막습니다.
items := []int{1, 2, 3, 4}
head := items[:2]
head = append(head, 99) // items[2]도 99가 될 수 있음
isolated := items[:2:2]
isolated = append(isolated, 99) // 새 backing array를 할당append는 새 slice를 반환한다
items := []string{"a"}
items = append(items, "b")append는 기존 backing array에 붙일 수도 있고, capacity가 부족하면 새 backing array를 만들 수도 있습니다. 그래서 반환값을 다시 받아야 합니다.
// 반환값을 버리면 새 slice header를 잃을 수 있음
items = append(items, "c")nil slice와 empty slice는 읽기·순회에서는 비슷하지만 같은 값은 아니다
var nilNames []string
emptyNames := []string{}
fmt.Println(len(nilNames), nilNames == nil) // 0 true
fmt.Println(len(emptyNames), emptyNames == nil) // 0 false둘 다 len은 0이고 range, append를 쓸 수 있습니다. 다만 nil 여부 자체가 API 계약이거나 JSON 출력이 중요할 때는 구분이 드러납니다. 기본 encoding/json에서 nil slice는 null, empty slice는 []로 인코딩되므로, 응답에서 어느 형태를 약속할지 정한 뒤 초기화 방식을 고릅니다.
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"]++값의 zero value와 key 부재를 구분해야 할 때는 comma-ok를 씁니다. 삭제는 delete, 같은 map을 비우는 것은 clear입니다.
score, ok := counts["x"]
if !ok {
score = 0
}
delete(counts, "x")
clear(counts)일반 map은 여러 goroutine이 동시에 읽고 쓰도록 안전하지 않습니다. 공유 갱신이 필요하면 Mutex로 보호하거나 channel, sync.Map처럼 책임에 맞는 도구를 고릅니다.
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)은 재할당 없이 늘릴 수 있는 여유입니다.
items := make([]int, 3) // len 3, 값은 0, 0, 0
buffer := make([]int, 0, 3) // len 0, append 여유 3인덱스로 바로 채울 배열형 결과에는 첫 형태를, append로 쌓을 결과에는 두 번째 형태를 씁니다. make(map[K]V, hint)의 hint는 초기 크기 힌트일 뿐 map 길이나 제한을 고정하지 않습니다.
선택 기준
| 상황 | 먼저 떠올릴 선택 |
|---|---|
| 순서 있는 목록 | 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