게시물에서 발췌한 내용입니다. 전체 게시물은 여기에서 볼 수 있습니다: Go 어레이가 작동하고 For-Range를 사용하여 까다로워지는 방법.
고전적인 Golang 배열과 슬라이스는 매우 간단합니다. 배열은 크기가 고정되어 있고 슬라이스는 동적입니다. 하지만 Go는 표면적으로는 단순해 보일 수 있지만 내부적으로는 많은 일이 벌어지고 있다는 점을 꼭 말씀드리고 싶습니다.
늘 그렇듯이 기본부터 시작하여 좀 더 자세히 살펴보겠습니다. 걱정하지 마세요. 배열을 다른 각도에서 보면 꽤 흥미로워집니다.
다음 부분에서 슬라이스를 다루겠습니다. 준비가 되면 여기에 놓겠습니다.
Go의 배열은 다른 프로그래밍 언어의 배열과 매우 유사합니다. 고정된 크기를 가지며 동일한 유형의 요소를 인접한 메모리 위치에 저장합니다.
이는 배열의 시작 주소와 요소의 인덱스를 기반으로 주소가 계산되므로 Go가 각 요소에 빠르게 액세스할 수 있음을 의미합니다.
func main() { arr := [5]byte{0, 1, 2, 3, 4} println("arr", &arr) for i := range arr { println(i, &arr[i]) } } // Output: // arr 0x1400005072b // 0 0x1400005072b // 1 0x1400005072c // 2 0x1400005072d // 3 0x1400005072e // 4 0x1400005072f
여기서 주목해야 할 몇 가지 사항이 있습니다.
이미지를 주의 깊게 살펴보세요.
우리 스택은 높은 주소에서 낮은 주소로 점점 성장하고 있죠? 이 그림은 arr[4]부터 arr[0]까지 스택에서 배열이 어떻게 보이는지 정확하게 보여줍니다.
그렇다면 첫 번째 요소(또는 배열)의 주소와 요소의 크기를 알면 배열의 모든 요소에 액세스할 수 있다는 뜻인가요? int 배열과 안전하지 않은 패키지로 이것을 시도해 봅시다:
func main() { a := [3]int{99, 100, 101} p := unsafe.Pointer(&a[0]) a1 := unsafe.Pointer(uintptr(p) 8) a2 := unsafe.Pointer(uintptr(p) 16) fmt.Println(*(*int)(p)) fmt.Println(*(*int)(a1)) fmt.Println(*(*int)(a2)) } // Output: // 99 // 100 // 101
첫 번째 요소에 대한 포인터를 얻은 다음 int 크기(64비트 아키텍처에서 8바이트)의 배수를 추가하여 다음 요소에 대한 포인터를 계산합니다. 그런 다음 이 포인터를 사용하여 액세스하고 다시 int 값으로 변환합니다.
이 예는 교육 목적으로 메모리에 직접 액세스하기 위해 안전하지 않은 패키지를 사용해 본 것입니다. 결과를 이해하지 못한 채 프로덕션에서 이 작업을 수행하지 마세요.
이제 T 유형의 배열은 그 자체로는 유형이 아니지만 특정 크기와 T 유형을 갖는 배열은 유형으로 간주됩니다. 제 말은 다음과 같습니다.
func main() { a := [5]byte{} b := [4]byte{} fmt.Printf("%T\n", a) // [5]uint8 fmt.Printf("%T\n", b) // [4]uint8 // cannot use b (variable of type [4]byte) as [5]byte value in assignment a = b }
a와 b가 모두 바이트 배열이지만 Go 컴파일러는 이를 완전히 다른 유형으로 간주하지만 %T 형식을 사용하면 이 점이 명확해집니다.
다음은 Go 컴파일러가 내부적으로 이를 보는 방법입니다(src/cmd/compile/internal/types2/array.go):
// An Array represents an array type. type Array struct { len int64 elem Type } // NewArray returns a new array type for the given element type and length. // A negative length indicates an unknown length. func NewArray(elem Type, len int64) *Array { return &Array{len: len, elem: elem} }
배열의 길이는 유형 자체에서 "인코딩"되므로 컴파일러는 해당 유형에서 배열의 길이를 알 수 있습니다. 한 크기의 배열을 다른 크기의 배열에 할당하거나 비교하려고 하면 유형 불일치 오류가 발생합니다.
Go에서 배열을 초기화하는 방법은 여러 가지가 있으며 그 중 일부는 실제 프로젝트에서 거의 사용되지 않을 수 있습니다.
var arr1 [10]int // [0 0 0 0 0 0 0 0 0 0] // With value, infer-length arr2 := [...]int{1, 2, 3, 4, 5} // [1 2 3 4 5] // With index, infer-length arr3 := [...]int{11: 3} // [0 0 0 0 0 0 0 0 0 0 0 3] // Combined index and value arr4 := [5]int{1, 4: 5} // [1 0 0 0 5] arr5 := [5]int{2: 3, 4, 4: 5} // [0 0 3 4 5]
위에서 수행하는 작업(첫 번째 작업 제외)은 "복합 리터럴"이라고 하는 해당 값을 정의하고 초기화하는 것입니다. 이 용어는 슬라이스, 맵, 구조체에도 사용됩니다.
흥미로운 점은 4개 미만의 요소로 배열을 만들 때 Go가 값을 배열에 하나씩 입력하라는 명령을 생성한다는 것입니다.
따라서 arr := [3]int{1, 2, 3, 4}를 수행할 때 실제로 일어나는 일은 다음과 같습니다.
arr := [4]int{} arr[0] = 1 arr[1] = 2 arr[2] = 3 arr[3] = 4
이 전략을 로컬 코드 초기화라고 합니다. 이는 초기화 코드가 전역 또는 정적 초기화 코드의 일부가 아닌 특정 함수 범위 내에서 생성되고 실행된다는 것을 의미합니다.
아래에서 값이 배열에 하나씩 배치되지 않는 또 다른 초기화 전략을 읽으면 더 명확해질 것입니다.
"요소가 4개 이상인 배열은 어떻습니까?"
컴파일러는 '정적 초기화' 전략으로 알려진 배열의 정적 표현을 바이너리로 생성합니다.
이는 배열 요소의 값이 바이너리의 읽기 전용 섹션에 저장된다는 의미입니다. 이 정적 데이터는 컴파일 타임에 생성되므로 값이 바이너리에 직접 포함됩니다. Go 어셈블리에서 [5]int{1,2,3,4,5}가 어떻게 보이는지 궁금하다면:
main..stmp_1 SRODATA static size=40 0x0000 01 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00 ................ 0x0010 03 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00 ................ 0x0020 05 00 00 00 00 00 00 00 ........
배열의 값을 확인하는 것은 쉽지 않지만 여기에서 여전히 몇 가지 주요 정보를 얻을 수 있습니다.
우리의 데이터는 40바이트(요소당 8바이트) 크기의 읽기 전용 정적 데이터인 stmp_1에 저장되며, 이 데이터의 주소는 바이너리에 하드코딩되어 있습니다.
컴파일러는 이 정적 데이터를 참조하는 코드를 생성합니다. 애플리케이션이 실행되면 배열을 설정하기 위한 추가 코드 없이 사전 초기화된 이 데이터를 직접 사용할 수 있습니다.
const readonly = [5]int{1, 2, 3, 4, 5} arr := readonly
"5개의 요소가 있지만 그 중 3개만 초기화된 배열은 어떻습니까?"
좋은 질문입니다. 이 리터럴 [5]int{1,2,3}는 Go가 값을 하나씩 배열에 넣는 첫 번째 범주에 속합니다.
배열 정의 및 초기화에 관해 이야기할 때 모든 배열이 스택에 할당되는 것은 아니라는 점을 언급해야 합니다. 너무 크면 힙으로 이동됩니다.
하지만 "너무 크다"는 것이 얼마나 큰 것인지 물을 수도 있습니다.
Go 1.23부터 배열뿐만 아니라 변수의 크기가 상수 값인 MaxStackVarSize(현재 10MB)를 초과하는 경우 스택 할당에 너무 큰 것으로 간주되어 힙으로 이스케이프됩니다.
func main() { a := [10 * 1024 * 1024]byte{} println(&a) b := [10*1024*1024 1]byte{} println(&b) }
이 시나리오에서 b는 힙으로 이동하지만 a는 힙으로 이동하지 않습니다.
배열의 길이는 유형 자체로 인코딩됩니다. 배열에 cap 속성이 없더라도 여전히 얻을 수 있습니다:
func main() { a := [5]int{1, 2, 3} println(len(a)) // 5 println(cap(a)) // 5 }
용량은 길이와 같습니다. 의심할 여지 없이 가장 중요한 것은 컴파일 타임에 이를 알 수 있다는 것입니다. 그렇죠?
따라서 len(a)는 런타임 속성이 아니기 때문에 컴파일러에 이해되지 않습니다. Go 컴파일러는 컴파일 타임에 값을 알고 있습니다.
...
게시물에서 발췌한 내용입니다. 전체 게시물은 여기에서 볼 수 있습니다: Go 어레이가 작동하고 For-Range를 사용하여 까다로워지는 방법.
부인 성명: 제공된 모든 리소스는 부분적으로 인터넷에서 가져온 것입니다. 귀하의 저작권이나 기타 권리 및 이익이 침해된 경우 자세한 이유를 설명하고 저작권 또는 권리 및 이익에 대한 증거를 제공한 후 이메일([email protected])로 보내주십시오. 최대한 빨리 처리해 드리겠습니다.
Copyright© 2022 湘ICP备2022001581号-3