슬라이스에 원소 붙이기와 복사하기
슬라이스에 원소 붙이기와 복사하기
슬라이스를 다루다 보면 "값을 더 추가하고 싶은데 어떻게 하죠?" "한 슬라이스를 다른 슬라이스로 옮기고 싶은데요?" 하는 순간이 꼭 오는데요, Go는 이걸 위해 내장 함수 append와 copy를 준비해 두고 있어요. 이번 글에서는 이 두 함수가 하는 일과, 겹치는 메모리에서도 안전하게 동작하는 이유까지 함께 살펴볼게요.
출처: Go Specification
본문
먼저 두 함수의 공통점부터 짚고 갈게요. 내장 함수 append와 copy는 흔한 슬라이스 연산을 도와주는 함수예요. 그리고 두 함수 모두, 인자들이 참조하는 메모리가 서로 겹치더라도(overlap) 결과는 그 영향에서 자유롭다는 점이 핵심이에요. 겹치는 범위가 있어도 결과가 어긋나지 않게 설계되어 있다는 뜻이죠.
append: 원소를 붙여 새 슬라이스 만들기
가변 인자(variadic) 함수 append는 슬라이스 s에 값 x를 0개 이상 붙이고, 그 결과인 슬라이스(역시 타입 S)를 돌려줘요.
여기서 값 x들은 타입 ...E의 매개변수로 전달되는데, 이 E는 S의 원소 타입이에요. 이때 일반적인 인자 전달 규칙(parameter passing rules)이 그대로 적용됩니다.
특별한 경우도 있어요. append는 타입 []byte에 대입 가능한 슬라이스를 받을 때, 두 번째 인자로 string 타입을 뒤에 ...를 붙여 넘겨줄 수도 있어요. 이 형태로 호출하면 문자열의 바이트 그대로를 슬라이스에 붙여 줍니다.
append(s S, x ...E) S // E is the element type of S
만약 S가 타입 매개변수(type parameter)라면, 그 타입 집합에 속한 모든 타입이 동일한 기본 슬라이스 타입 []E를 가져야 해요.
이제 실제 동작을 봐볼게요. s의 용량(capacity) 이 추가할 값들을 담기엔 부족하다면, append는 기존 슬라이스 원소와 추가할 값 모두를 담을 수 있는 충분히 큰 새 기본 배열을 할당(allocates)해요. 반대로 용량이 넉넉하다면 기존 기본 배열을 재사용합니다.
s0 := []int{0, 0}
s1 := append(s0, 2) // append a single element s1 is []int{0, 0, 2}
s2 := append(s1, 3, 5, 7) // append multiple elements s2 is []int{0, 0, 2, 3, 5, 7}
s3 := append(s2, s0...) // append a slice s3 is []int{0, 0, 2, 3, 5, 7, 0, 0}
s4 := append(s3[3:6], s3[2:]...) // append overlapping slice s4 is []int{3, 5, 7, 2, 3, 5, 7, 0, 0}
var t []interface{}
t = append(t, 42, 3.1415, "foo") // t is []interface{}{42, 3.1415, "foo"}
var b []byte
b = append(b, "bar"...) // append string contents b is []byte{'b', 'a', 'r' }
예시를 하나씩 따라가 볼게요. s1 := append(s0, 2)는 단일 원소를 붙이고, s2는 여러 원소를 한 번에 붙여요. s3은 ...를 써서 슬라이스 전체를 통째로 붙였고, s4는 서로 겹치는 슬라이스를 붙이는 경우예요 — 앞서 말한 겹침 안전성 덕분에 예상대로 동작해요. 마지막으로 t처럼 서로 다른 타입의 값을 섞어 붙이려면 원소 타입을 []interface{}로 잡으면 되고, b는 문자열을 ...로 풀어 바이트로 붙인 경우예요.
copy: 원소를 복사하기
함수 copy는 소스 src에서 목적지 dst로 슬라이스 원소를 복사하고, 복사된 원소의 개수를 돌려줘요.
두 인자는 모두 동일한(identical) 원소 타입 E를 가져야 하고, 타입 []E의 슬라이스에 대입 가능해야 해요. 그리고 복사되는 원소의 수는 len(src)와 len(dst) 중 작은 값이에요. 목적지가 더 짧으면 목적지 길이만큼만, 소스가 더 짧으면 소스 길이만큼만 복사된다고 이해하면 돼요.
copy에도 특별한 경우가 있는데요, 타입 []byte에 대입 가능한 목적지 인자와 string 타입의 소스 인자를 받을 수 있어요. 이 형태는 문자열의 바이트를 바이트 슬라이스로 복사해 줍니다.
copy(dst, src []T) int
copy(dst []byte, src string) int
마찬가지로 인자 중 하나 또는 둘의 타입이 타입 매개변수(type parameter)라면, 각 타입 집합의 모든 타입이 동일한 기본 슬라이스 타입 []E를 가져야 합니다.
예시를 볼게요.
var a = [...]int{0, 1, 2, 3, 4, 5, 6, 7}
var s = make([]int, 6)
var b = make([]byte, 5)
n1 := copy(s, a[0:]) // n1 == 6, s is []int{0, 1, 2, 3, 4, 5}
n2 := copy(s, s[2:]) // n2 == 4, s is []int{2, 3, 4, 5, 4, 5}
n3 := copy(b, "Hello, World!") // n3 == 5, b is []byte("Hello")
n1은 배열 a의 첫 원소부터 슬라이스 s로 복사해서 6개가 들어갔어요. n2처럼 src와 dst가 같은 슬라이스여도 겹침 안전성 덕분에 정상적으로 동작하고요, 복사되는 개수는 len(s)와 len(s[2:]) 중 작은 값인 4가 돼요. 마지막 n3은 문자열을 바이트 슬라이스로 복사하는 특별 형태라, 목적지 길이 5만큼인 "Hello"만 들어갑니다.
더 알아보기
- Go Specification — 종합 목차에서 슬라이스 관련 다른 섹션들도 함께 살펴보세요.
- 슬라이스의 메모리 구조(길이·용량·기본 배열)를 알면
append가 언제 새 배열을 할당하는지 훨씬 직관적으로 이해할 수 있어요. append와copy모두 겹치는 메모리에서도 안전하지만, 실제 코드에서 겹침을 자주 쓴다면 로직을 한 번 더 점검해 보는 습관이 좋습니다.