GoLang Slice..!?

Hansu Park·2024년 5월 15일
1

알고리즘 문제를 풀며 발생한 의문점에 대해 소개한다.

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

0개의 댓글