Menu

Golang 슬라이스: append, make, len과 cap, 공유 버그

슬라이스는 Go에서 매일 쓰는 리스트 타입입니다. make, append와 용량 증가, len과 cap의 차이, 슬라이싱이 기반 배열을 공유하는 방식(과 그로 인한 버그), copy, slices 패키지, 2차원 슬라이스, nil과 빈 슬라이스를 알아봅니다.

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

예제 하나로 보는 슬라이스

슬라이스는 배열을 바라보는, 늘어날 수 있는 뷰입니다. 리터럴이나 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]

ab"cmd" 다음의 배열 칸을 공유하므로 두 번째 append가 첫 번째를 덮어썼습니다. 코드는 멀쩡해 보이고 용량이 우연히 꽉 차 있을 때는 잘 동작하기 때문에, 이 버그는 가끔씩만 나타납니다.

해결책은 두 가지입니다:

  1. 완전한 슬라이스 표현식 s[low:high:max]는 용량을 max-low로 제한합니다. 남는 용량이 없으면 다음 append는 할당할 수밖에 없습니다. first := base[:2: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)

DeleteInsertappend처럼 대입해야 하는 슬라이스를 반환합니다. 정렬은 별도 페이지가 있습니다: 정렬.

인덱스로 반복하면서 요소를 제거하면 요소를 건너뛰게 됩니다. 대신 slices.DeleteFunc를 쓰세요:

s = slices.DeleteFunc(s, func(n int) bool { return n%2 == 0 }) // drop evens

2차원 슬라이스

슬라이스의 슬라이스는 행마다 길이가 다를 수 있는 격자를 줍니다. 각 행은 따로 할당해야 합니다.

안쪽의 make를 잊으면 각 행이 nil로 남고, grid[1][2] = 7index 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로, 빈 슬라이스를 []로 인코딩합니다.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기