알고리즘 문제를 풀며 발생한 의문점에 대해 소개한다.
Slice는 GoLang에서 사용하는 동적배열이다. Java의 ArrayList
, C++의 vector
와는 다르게 배열 포인터, 길이, 용량만을 가진다.
배열을 나타낼 때는 포인터와 길이만으로도 충분한데 왜 용량을 가질까?
정답은 재할당을 방지하기 위함이다. 유지하고 있는 배열의 용량보다 많은 것을 할당하게 된다면, 새롭게 배열을 생성하고 값들을 복사하는 재할당 과정이 필요하다. 이 과정은 비용이 굉장히 크기에 미리 큰 용량을 선언할 수 있는 것이다.
따라서
func main() {
original := make([]int, 2, 3) // Length 2, capacity 3
original[0] = 10
original[1] = 20
newSlice := append(original, 30) // No reallocation needed, fits in capacity
fmt.Printf("original address: %p, original: %v\n", &original[0], original)
fmt.Printf("newSlice address: %p, newSlice: %v\n", &newSlice[0], newSlice)
newSlice = append(newSlice, 40) // Likely requires reallocation, exceeds capacity
fmt.Printf("After appending 40:\n")
fmt.Printf("newSlice address: %p, newSlice: %v\n", &newSlice[0], newSlice)
}
와 같은 경우에서, newSlice의 %p
(포인터) 출력이 처음과 두번째가 다르다.
JS의 Banana문제 처럼 이런 어이없는 경우도 생긴다.
func main() {
a := []int{1,2,3,4}
b := a
b = append(b[0:1], b[2:]...)
fmt.Println(b) // [1,3,4]
fmt.Println(a) // [1,3,4,4]
}
이 경우는 b가 a라는 배열을 가리키고 있었고 append()
를 통해 첫 3개의 요소 ([1,2,3]
)가 [1,3,4]
로 바뀌니 원본 배열 또한 변경되는 것이다.
깊은 복사가 아니라 포인터로 가리키고만 있기 때문에 원본 배열의 수정 또한 일어난다. 막기 위해선 copy()
등을 이용해 deep copy를 해야한다.
func main() {
a := []int{1, 2, 3, 4}
b := a
b = append(b[0:1:1], b[2:]...)
fmt.Println(b) // [1,3,4]
fmt.Println(a) // [1,2,3,4]
}
또한, Slicing([:]
)을 사용할 때 용량을 적절한 크기로 지정해 재할당을 유도할 수도 있다.
참고: go - Slice copy mutating original slice - Stack Overflow
왜 이렇게 특이한 동적 배열을 만들었는지 궁금해서 살펴봤었다. 아무래도 C
언어 기반이다보니 간단하게 (포인터, 길이, 용량만 있으면 되므로) 설계한 게 아닐까 싶었다. 역시나 Rust에서도 비슷한 문법이 있다고 한다.
참고: Noob is wondering why slices need to exist - Getting Help - Go Forum