예제 하나로 보는 슬라이스
슬라이스는 배열을 바라보는, 늘어날 수 있는 뷰입니다. 리터럴이나 make로 만들고, append로 늘리고, len으로 크기를 읽습니다.
[]string은 대괄호 안에 길이가 없습니다. 크기가 고정된 [4]string 같은 배열 타입과의 차이가 이것입니다.
슬라이스의 정체
슬라이스 값은 필드 세 개로 된 작은 헤더입니다:
- 기반 배열의 한 요소를 가리키는 포인터
- 길이: 슬라이스가 볼 수 있는 요소 수
- 용량: 그 포인터부터 기반 배열 끝까지 있는 요소 수
슬라이스를 복사하면(대입하거나 함수에 넘기면) 이 헤더만 복사됩니다. 두 복사본이 같은 요소를 가리킵니다. 슬라이스 때문에 놀라는 일은 거의 모두 이 사실을 잊어서 생깁니다.
make: 길이와 용량
make([]T, len, cap)은 기반 배열을 할당하고 그 위의 슬라이스를 반환합니다. 용량은 선택이며 기본값은 길이입니다.
흔한 혼동이 있습니다. make([]int, 5) 뒤에 append를 다섯 번 호출하면 앞의 다섯 개가 0인 요소 열 개가 됩니다. append할 계획이라면 make([]int, 0, 5)를, 인덱스로 대입할 계획이라면 make([]int, 5)를 쓰세요.
최종 크기를 알 때 용량을 미리 할당하면 반복적인 증가를 피할 수 있습니다. 필수가 아니라 최적화입니다.
append와 증가
append는 현재 길이 뒤에 요소를 추가합니다. 용량이 충분하면 제자리에 씁니다. 그렇지 않으면 더 큰 배열을 할당하고, 이전 요소를 복사한 뒤, 새 배열을 가리키는 슬라이스를 반환합니다.
용량은 슬라이스가 작을 때는 대략 두 배씩 늘고, 256개를 넘으면 더 작은 비율(1.25배 쪽으로)로 늘어납니다. 정확한 숫자는 Go 버전마다 바뀌어 온 런타임 세부 사항이므로 거기에 의존하는 코드는 절대 쓰지 마세요.
append는 다른 배열을 반환할 수 있으므로 결과를 반드시 써야 합니다:
append(s, 4) // compile error: append(s, 4) (value of type []int) is not used
s = append(s, 4) // correct
슬라이스 전체를 추가할 때는 펼치기 문법을 씁니다: s = append(s, other...).
슬라이싱은 기반 배열을 공유한다
s[low:high]는 같은 배열 위에 인덱스 low부터 high 직전까지를 덮는 새 슬라이스 헤더를 만듭니다. 어느 쪽 경계든 생략할 수 있습니다. 요소는 복사되지 않습니다.
출력:
[10 99 30 40 50]
3 4
이 공유 덕분에 슬라이싱이 저렴합니다. 그리고 가장 유명한 슬라이스 버그의 원인이기도 합니다.
append 앨리어싱 버그
하위 슬라이스에 남는 용량이 있으면, 거기에 append할 때 부모의 배열에, 부모가 아직 쓰고 있는 요소 위에 쓰게 됩니다.
출력:
[1 2 100 4 5]
[1 2 100]
[cmd test] [cmd test]
a와 b는 "cmd" 다음의 배열 칸을 공유하므로 두 번째 append가 첫 번째를 덮어썼습니다. 코드는 멀쩡해 보이고 용량이 우연히 꽉 차 있을 때는 잘 동작하기 때문에, 이 버그는 가끔씩만 나타납니다.
해결책은 두 가지입니다:
- 완전한 슬라이스 표현식
s[low:high:max]는 용량을max-low로 제한합니다. 남는 용량이 없으면 다음append는 할당할 수밖에 없습니다.first := base[:2:2]로 첫 번째 예제를 안전하게 만들 수 있습니다. - 슬라이스가 호출보다 오래 살거나 독립적으로 append될 때는 명시적으로 복사하세요:
slices.Clone(prefix)또는append([]string(nil), prefix...).
관련된 함정이 있습니다. 거대한 배열의 작은 슬라이스는 가비지 컬렉터 입장에서 배열 전체를 살려 둡니다. 100MB 파일을 읽고 data[:10]을 보관하면 100MB가 메모리에 남습니다. 보관할 부분을 복제하세요.
copy
내장 함수 copy(dst, src)는 min(len(dst), len(src))개의 요소를 복사하고 그 개수를 반환합니다. dst를 늘리지는 않습니다.
copy는 소스와 대상이 겹쳐도 올바르게 처리하므로 copy(s[1:], s)는 요소를 망가뜨리지 않고 오른쪽으로 옮깁니다.
slices 패키지
Go 1.21부터 표준 slices 패키지에 예전에 직접 작성하던 연산을 위한 제네릭 헬퍼가 있습니다.
| 함수 | 하는 일 |
|---|---|
Contains, Index | 값 찾기(ContainsFunc, IndexFunc는 조건 함수를 받음) |
Sort, SortFunc, SortStableFunc | 제자리 정렬 |
BinarySearch | 정렬된 슬라이스 검색 |
Insert, Delete, DeleteFunc | 삽입 또는 제거, 새 슬라이스 반환 |
Compact | 연속된 중복 제거 |
Equal, Compare | 요소별 비교 |
Clone, Reverse, Max, Min | 이름 그대로 |
Collect, Sorted, Values, All | 이터레이터와 함께 사용(Go 1.23) |
Delete와 Insert는 append처럼 대입해야 하는 슬라이스를 반환합니다. 정렬은 별도 페이지가 있습니다: 정렬.
인덱스로 반복하면서 요소를 제거하면 요소를 건너뛰게 됩니다. 대신 slices.DeleteFunc를 쓰세요:
s = slices.DeleteFunc(s, func(n int) bool { return n%2 == 0 }) // drop evens
2차원 슬라이스
슬라이스의 슬라이스는 행마다 길이가 다를 수 있는 격자를 줍니다. 각 행은 따로 할당해야 합니다.
안쪽의 make를 잊으면 각 행이 nil로 남고, grid[1][2] = 7은 index out of range [2] with length 0으로 패닉을 일으킵니다.
nil 슬라이스와 빈 슬라이스
nil 슬라이스는 len, cap, range, append에서 빈 슬라이스처럼 동작하므로 제로 상태로는 var s []T를 쓰세요. 차이가 중요할 때만 []T{}로 초기화하며, 주로 JSON 출력(null과 [])이 그런 경우입니다. 비었는지는 s == nil이 아니라 len(s) == 0으로 확인하세요.
슬라이스와 함수
슬라이스를 받은 함수는 요소를 바꿀 수 있고, 호출자는 그 변경을 봅니다. 헤더의 복사본을 받았기 때문에 호출자의 길이는 바꿀 수 없습니다. append하는 함수는 새 슬라이스를 반환해야 합니다:
func addAll(s []int, vals ...int) []int {
return append(s, vals...)
}
그래서 append, slices.Delete, slices.Insert는 모두 슬라이스를 반환합니다.
흔한 실수
append의 결과를 무시함. 결과를 통째로 버리면 컴파일되지 않지만, 계속 쓰는 변수가 아닌 다른 변수에append(s, x)를 대입하는 것은 논리 버그입니다.- 한 기반 슬라이스에서 두 번 append함. 저장 공간을 공유할 수 있습니다. 기반 슬라이스를 복제하거나 완전한 슬라이스 표현식을 쓰세요.
make([]T, n)다음에append. 앞쪽에 0이 n개 남습니다.- 범위를 벗어난 인덱스.
s[len(s)]는 패닉을 일으킵니다. 마지막 요소는s[len(s)-1]입니다. - 슬라이스를
==로 비교함.s == nil만 컴파일됩니다.slices.Equal을 쓰세요.
자주 묻는 질문
Go에서 슬라이스의 길이와 용량은 무엇이 다른가요?
len(s)는 슬라이스가 현재 담고 있는 요소 수입니다. cap(s)는 슬라이스의 첫 요소부터 기반 배열에 들어갈 수 있는 요소 수입니다. append는 남는 용량이 있으면 거기에 쓰고, 없으면 더 큰 새 배열을 할당합니다.
Go에서 슬라이스에 요소를 추가하려면 어떻게 하나요?
내장 함수 append를 호출하고 결과를 다시 대입합니다: s = append(s, x). 값을 여러 개(append(s, 1, 2, 3)) 추가하거나 다른 슬라이스(append(s, other...))를 추가할 수 있습니다. append는 새 배열을 가리키는 슬라이스를 반환할 수 있으므로 반환 값을 항상 쓰세요.
Go에서 슬라이스의 요소를 제거하려면 어떻게 하나요?
slices.Delete(s, i, i+1)(Go 1.21)을 씁니다. 뒤쪽 요소를 앞으로 당기고 더 짧아진 슬라이스를 반환합니다. Go 1.22부터는 비워진 뒤쪽 자리를 제로 값으로 채우기도 합니다. 순서가 상관없다면 마지막 요소와 바꾼 뒤 잘라 내세요: s[i] = s[len(s)-1]; s = s[:len(s)-1].
Go에서 슬라이스에 값이 들어 있는지 어떻게 확인하나요?
표준 slices 패키지(Go 1.21)의 slices.Contains(s, v)를 씁니다. slices.Index(s, v)는 위치나 -1을 반환하고, slices.ContainsFunc는 조건 함수를 받습니다. 큰 데이터에서 조회를 반복한다면 대신 맵을 만드세요.
Go에서 nil 슬라이스와 빈 슬라이스의 차이는 무엇인가요?
var s []int는 nil이고, s := []int{}는 비어 있지만 nil이 아닙니다. 둘 다 길이가 0이고 len, range, append와 함께 동작합니다. 차이는 s == nil 검사와 encoding/json에서 드러납니다. encoding/json은 nil 슬라이스를 null로, 빈 슬라이스를 []로 인코딩합니다.