여행 기간이 얼마나 될지 모르고 여행을 위해 짐을 싸본 적이 있나요? Go에 데이터를 저장할 때 바로 이런 일이 발생합니다. 때로는 주말 여행을 위해 짐을 꾸릴 때와 같이 얼마나 많은 물건을 보관해야 하는지 정확히 알 수 있습니다. 예를 들어 여행을 위해 짐을 꾸릴 때 "준비되면 돌아올게요"라고 말하는 경우에는 그렇지 않습니다.
간단한 일러스트레이션을 통해 Go 배열과 슬라이스 내부의 세계에 대해 자세히 알아보세요. 다음 사항을 살펴보겠습니다.
이 글을 끝까지 읽으면 실제 사례와 메모리 다이어그램의 도움을 받아 언제 배열을 사용해야 하는지, 언제 슬라이스를 사용해야 하는지 이해할 수 있을 것입니다
배열을 완벽하게 배열된 상자처럼 각 요소가 서로 나란히 배치된 단일 메모리 블록으로 생각하세요.
var number [5]int를 선언하면 Go는 그 이상도 그 이하도 아닌 5개의 정수를 담을 수 있는 충분한 연속 메모리를 예약합니다.
연속된 고정 메모리가 있으므로 런타임 중에 크기를 조정할 수 없습니다.
func main() { // Zero-value initialization var nums [3]int // Creates [0,0,0] // Fixed size nums[4] = 1 // Runtime panic: index out of range // Sized during compilation size := 5 var dynamic [size]int // Won't compile: non-constant array bound }
크기는 배열 유형의 일부입니다. 이는 int와 string이 다른 것처럼 [5]int와 [6]int도 완전히 다른 유형임을 의미합니다.
func main() { // Different types! var a [5]int var b [6]int // This won't compile a = b // compile error: cannot use b (type [6]int) as type [5]int // But this works var c [5]int a = c // Same types, allowed }
Go에서 배열을 할당하거나 전달하면 기본적으로 복사본이 생성됩니다. 이는 데이터 격리를 보장하고 예상치 못한 돌연변이를 방지합니다.
func modifyArrayCopy(arr [5]int) { arr[0] = 999 // Modifies the copy, not original } func modifyArray(arr *[5]int){ arr[0] = 999 // Modifies the original, since reference is passed } func main() { numbers := [5]int{1, 2, 3, 4, 5} modifyArrayCopy(numbers) fmt.Println(numbers[0]) // prints 1, not 999 modifyArray(&numbers) fmt.Println(numbers[0]) // prints 999 }
좋아요. var Dynamic [size]int를 사용하여 동적 크기를 설정할 수는 없습니다. 여기서 slice가 중요합니다.
마법은 빠른 운영을 유지하면서 이러한 유연성을 유지하는 방법에 있습니다.
Go의 모든 슬라이스는 세 가지 중요한 구성 요소로 구성됩니다.
type slice struct { array unsafe.Pointer // Points to the actual data len int // Current number of elements cap int // Total available space }
안전하지 않은 포인터란 무엇입니까??
unsafe.Pointer는 유형 안전성 제약 없이 원시 메모리 주소를 처리하는 Go의 방식입니다. Go의 유형 시스템을 우회하여 직접적인 메모리 조작을 허용하므로 "안전하지 않음"입니다.
Go의 C의 void 포인터와 동일하다고 생각하세요.
그 배열은 무엇인가요?
슬라이스를 생성할 때 Go는 배열과 달리 힙에 백업 배열이라는 연속적인 메모리 블록을 할당합니다. 이제 슬라이스 구조체의 배열은 해당 메모리 블록의 시작을 가리킵니다.
배열 필드는 다음과 같은 이유로 unsafe.Pointer를 사용합니다.
실제 알고리즘에 대한 직관력을 키워 보겠습니다.
직관을 따르면 두 가지 일을 할 수 있습니다.
넓은 공간을 확보하여 필요할 때마다 사용할 수 있습니다
장점: 특정 시점까지 늘어나는 요구사항을 처리합니다.
단점: 메모리 낭비, 사실상 한계에 도달할 수도 있음
처음에는 임의의 크기를 설정할 수 있으며 요소가 추가될 때마다 추가할 때마다 메모리를 재할당할 수 있습니다.
장점: 이전 사례를 처리하고 필요에 따라 확장 가능
단점: 재할당은 비용이 많이 들고 추가할 때마다 최악의 상황이 될 것입니다
용량이 한계에 도달하면 용량을 늘려야 하므로 재배치를 피할 수 없습니다. 후속 삽입/추가 비용이 일정하도록(O(1)) 재할당을 최소화할 수 있습니다. 이를 상각비용이라고 합니다.
어떻게 하면 되나요?
Go 버전 v1.17까지는 다음 공식이 사용되었습니다.
func main() { // Zero-value initialization var nums [3]int // Creates [0,0,0] // Fixed size nums[4] = 1 // Runtime panic: index out of range // Sized during compilation size := 5 var dynamic [size]int // Won't compile: non-constant array bound }
Go 버전 v1.18:
func main() { // Different types! var a [5]int var b [6]int // This won't compile a = b // compile error: cannot use b (type [6]int) as type [5]int // But this works var c [5]int a = c // Same types, allowed }
큰 슬라이스를 두 배로 늘리는 것은 메모리 낭비이므로 슬라이스 크기가 증가할수록 성장 인자는 감소합니다.
func modifyArrayCopy(arr [5]int) { arr[0] = 999 // Modifies the copy, not original } func modifyArray(arr *[5]int){ arr[0] = 999 // Modifies the original, since reference is passed } func main() { numbers := [5]int{1, 2, 3, 4, 5} modifyArrayCopy(numbers) fmt.Println(numbers[0]) // prints 1, not 999 modifyArray(&numbers) fmt.Println(numbers[0]) // prints 999 }
슬라이스에 몇 가지 요소를 추가해 보겠습니다
type slice struct { array unsafe.Pointer // Points to the actual data len int // Current number of elements cap int // Total available space }
정원(5명)이 있으므로 > 길이(3), 이동:
기존 백업 어레이 사용
인덱스 3에 10위
길이를 1씩 늘립니다
// Old growth pattern capacity = oldCapacity * 2 // Simple doubling
한계에 도전해보자
// New growth pattern if capacity < 256 { capacity = capacity * 2 } else { capacity = capacity + capacity/4 // 25% growth }
앗! 이제 우리는 능력에 도달했으니 성장해야 합니다. 일어나는 일은 다음과 같습니다.
func main() { // Zero-value initialization var nums [3]int // Creates [0,0,0] // Fixed size nums[4] = 1 // Runtime panic: index out of range // Sized during compilation size := 5 var dynamic [size]int // Won't compile: non-constant array bound }
큰 조각이면 어떻게 되나요?
func main() { // Different types! var a [5]int var b [6]int // This won't compile a = b // compile error: cannot use b (type [6]int) as type [5]int // But this works var c [5]int a = c // Same types, allowed }
용량이 256명이므로 Go는 1.18 이후 성장 공식을 사용합니다.
새 용량 = oldCap oldCap/4
256 256/4 = 256 64 = 320
func modifyArrayCopy(arr [5]int) { arr[0] = 999 // Modifies the copy, not original } func modifyArray(arr *[5]int){ arr[0] = 999 // Modifies the original, since reference is passed } func main() { numbers := [5]int{1, 2, 3, 4, 5} modifyArrayCopy(numbers) fmt.Println(numbers[0]) // prints 1, not 999 modifyArray(&numbers) fmt.Println(numbers[0]) // prints 999 }
type slice struct { array unsafe.Pointer // Points to the actual data len int // Current number of elements cap int // Total available space }
슬라이스 헤더의 모양은 다음과 같습니다.
// Old growth pattern capacity = oldCapacity * 2 // Simple doubling
우발적인 업데이트
slice는 참조 의미 체계를 사용하므로 주의하지 않을 경우 원본 슬라이스에 대한 우발적인 변형이 발생할 수 있는 복사본을 생성하지 않습니다.
// New growth pattern if capacity < 256 { capacity = capacity * 2 } else { capacity = capacity + capacity/4 // 25% growth }
비용이 많이 드는 추가 작업
numbers := make([]int, 3, 5) // length=3 capacity // Memory Layout after creation: Slice Header: { array: 0xc0000b2000 // Example memory address len: 3 cap: 5 } Backing Array at 0xc0000b2000: [0|0|0|unused|unused]
복사 vs 추가
numbers = append(numbers, 10)
명확한 선택 가이드로 마무리하겠습니다.
? 다음과 같은 경우 어레이를 선택하십시오:
? 슬라이스를 선택하는 경우:
? notion-to-md 프로젝트를 확인해보세요! Notion 페이지를 Markdown으로 변환하는 도구로, 콘텐츠 제작자와 개발자에게 적합합니다. Discord 커뮤니티에 참여하세요.
위 내용은 Go의 배열과 슬라이스: 시각적으로 작동하는 '내부' 이해의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!