문제의 역사:
슬라이스(slices)는 Go의 주요 동적 구조 중 하나로, 메모리의 편리함과 경제성을 높이기 위해 고정 길이 배열에 대한 대안으로 등장했습니다. 이들은 배열의 하위 집합과 유연하게 작업할 수 있도록 하지만, 성능과 안전한 코드를 위해 중요한 몇 가지 미세한 점들이 있습니다.
문제:
많은 개발자들이 슬라이스가 정확히 어떻게 구성되어 있는지 이해하지 못합니다: 슬라이스는 배열 자체가 아니라 배열에 대한 포인터와 길이, 용량(capacity)을 가진 구조체입니다. 이는 메모리 누수, 복사 작업 시 버그 및 원래 배열이 변경될 때 예기치 않은 효과를 초래할 수 있습니다.
해결책:
슬라이스는 타입입니다:
type slice struct { ptr unsafe.Pointer len int cap int }
append()를 사용하여 슬라이스를 확장할 때, 백업 배열이 재배치되면 모든 이전 슬라이스의 참조는 유효하지만 이전 데이터에 대한 참조를 유지하게 됩니다. 이 특성을 모르면 오류와 메모리 누수로 이어질 수 있습니다.
메모리 할당 및 복사의 예:
src := []int{1,2,3,4,5} dst := make([]int, len(src)) copy(dst, src)
[:]를 사용하여 생성된 슬라이스는 기본 배열을 공유하며, 복사가 이루어지지 않으면 서로 영향을 미칩니다.
주요 특징:
cap을 초과하여 append로 슬라이스를 증가시키면, 다른 슬라이스가 같은 배열에 대한 참조를 가지고 있을 때 무슨 일이 일어나는가?
cap을 초과할 경우 append는 메모리에서 새로운 위치에 기본 배열을 만들어내고, 오직 이 슬라이스만 새로운 배열을 참조하며 나머지는 이전 배열을 참조합니다. 이는 데이터 불일치의 흔한 원인입니다.
크기가 작은 슬라이스를 큰 배열에서 얻어 계속 유지하는 것이 왜 중요한가?
슬라이스가 매우 작더라도, 그 포인터는 전체 백업 배열에 대한 참조를 유지하여 큰 배열이 메모리에 남아 메모리 누수를 초래할 수 있습니다.
배열의 경계를 넘어 슬라이스를 만들면 무슨 일이 일어나는가?
panic이 발생합니다: runtime error: slice bounds out of range.
함수는 큰 파일을 바이트 배열로 읽고 첫 100개 요소의 슬라이스를 반환합니다. 이 슬라이스는 오래 유지되지만 전체 큰 배열에 대한 메모리는 GC에서 유지됩니다.
장점:
단점:
슬라이스를 얻은 후 즉시 필요한 조각을 새 슬라이스로 만들고 복사하여 이전 배열을 즉시 잊어버립니다. GC가 메모리를 해제합니다.
장점:
단점: