연습 - 조각 살펴보기

완료됨

이전 섹션에서 배열을 살펴보았으며 배열이 조각 및 맵의 기반이라고 배웠습니다. 잠시 후에 그 이유를 알아봅니다. 배열과 마찬가지로 조각은 동일한 형식의 요소 시퀀스를 나타내는 Go의 데이터 형식입니다. 그러나 배열과 큰 차이점은 조각의 크기가 동적이며 고정되지 않는다는 것입니다.

조각은 배열 또는 다른 조각 맨 위의 데이터 구조입니다. 원래 배열 또는 조각을 ‘기본 배열’이라고 합니다. 조각을 사용하면 전체 기본 배열에 액세스하거나 요소의 하위 시퀀스에만 액세스할 수 있습니다.

조각은 다음의 세 가지 구성 요소로만 이루어집니다.

  • 기본 배열의 도달할 수 있는 첫 번째 요소에 대한 포인터. 이 요소는 배열의 첫 번째 요소(array[0])가 아닐 수도 있습니다.
  • 조각의 길이. 조각의 요소 수입니다.
  • 조각의 용량. 조각 시작과 기본 배열의 끝 사이에 있는 요소 수입니다.

다음 그림은 조각이 무엇인지를 나타냅니다.

Go에서 조각이 어떻게 보이는지를 나타내는 다이어그램

어떻게 조각이 기본 배열의 하위 집합이 되는지 알아봅니다. 코드에서 이전 그림을 표시하는 방법을 살펴보겠습니다.

조각 선언 및 초기화

조각을 선언하려면 배열을 선언하는 것과 동일한 방식으로 조각을 선언합니다. 예를 들어, 다음 코드는 조각 그림에서 확인한 내용을 나타냅니다.

package main

import "fmt"

func main() {
    months := []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
    fmt.Println(months)
    fmt.Println("Length:", len(months))
    fmt.Println("Capacity:", cap(months))
}

코드를 실행하면 다음과 같은 출력이 표시됩니다.

[January February March April May June July August September October November December]
Length: 12
Capacity: 12

지금은 조각이 배열과 크게 다르지 않는 것을 알 수 있습니다. 선언할 때도 동일한 방식을 사용합니다. 조각에서 정보를 가져오려면 기본 제공 함수 len()cap()를 사용할 수 있습니다. 이러한 함수를 계속 사용하여 조각이 기본 배열의 후속 요소를 포함할 수 있는지 확인합니다.

항목 조각화

Go는 조각 연산자 s[i:p]를 지원합니다. 여기서 다음이 적용됩니다.

  • s는 배열을 나타냅니다.
  • i는 새 조각을 추가할 기본 배열(또는 다른 조각)의 첫 번째 요소에 대한 포인터를 나타냅니다. i 변수는 배열에서 인덱스 위치 i에 있는 요소(array[i])에 해당합니다. 이 요소는 기본 배열의 첫 번째 요소(array[0])가 아닐 수도 있습니다.
  • p는 새 조각을 만들 때 사용할 기본 배열의 요소 수와 요소 위치를 나타냅니다. p 변수는 새 조각에서 사용할 수 있는 기본 배열의 마지막 요소에 해당합니다. 기본 배열에서 p 위치의 요소는 array[i+1] 위치에 있습니다. 이 요소는 기본 배열의 마지막 요소(array[len(array)-1])가 아닐 수도 있습니다.

따라서 조각은 요소의 하위 집합만 참조할 수 있습니다.

각 분기를 나타내는 4개의 변수가 필요하고 12개 요소가 포함된 months 조각이 있다고 가정해 보겠습니다. 다음 이미지에서는 months를 네 개의 새 quarter 조각으로 분할하는 방법을 보여 줍니다.

Go에서 여러 조각이 어떻게 보이는지를 나타내는 다이어그램

이전 그림에 표시된 내용을 코드로 나타내기 위해 다음 코드를 사용할 수 있습니다.

package main

import "fmt"

func main() {
    months := []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
    quarter1 := months[0:3]
    quarter2 := months[3:6]
    quarter3 := months[6:9]
    quarter4 := months[9:12]
    fmt.Println(quarter1, len(quarter1), cap(quarter1))
    fmt.Println(quarter2, len(quarter2), cap(quarter2))
    fmt.Println(quarter3, len(quarter3), cap(quarter3))
    fmt.Println(quarter4, len(quarter4), cap(quarter4))
}

이 코드를 실행하면 다음과 같은 출력이 표시됩니다.

[January February March] 3 12
[April May June] 3 9
[July August September] 3 6
[October November December] 3 3

어떻게 조각의 길이는 동일하지만 용량은 다른지 알아봅니다. quarter2 조각을 살펴보겠습니다. 이 조각을 선언하면 조각이 위치 번호 3에서 시작되고 마지막 요소가 위치 번호 6에 있는 것을 볼 수 있습니다. 조각의 길이는 세 개의 요소이지만 기본 배열에는 사용할 수 있지만 조각에는 표시되지 않는 더 많은 요소나 위치가 있으므로 용량은 9입니다. 예를 들어 fmt.Println(quarter2[3])과 같이 출력하려고 하면 panic: runtime error: index out of range [3] with length 3과 같은 오류 메시지가 표시됩니다.

조각의 용량은 조각을 확장할 수 있는 정도를 나타내는 데 불과합니다. 따라서 다음 예제와 같이 quarter2에서 확장된 조각을 만들 수 있습니다.

package main

import "fmt"

func main() {
    months := []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
    quarter2 := months[3:6]
    quarter2Extended := quarter2[:4]
    fmt.Println(quarter2, len(quarter2), cap(quarter2))
    fmt.Println(quarter2Extended, len(quarter2Extended), cap(quarter2Extended))
}

위의 코드를 실행하면 다음과 같이 출력됩니다.

[April May June] 3 9
[April May June July] 4 9

quarter2Extended 변수를 선언할 때는 초기 위치([:4])를 지정할 필요가 없습니다. 이 작업을 수행하는 경우 Go에서는 사용자가 조각의 첫 번째 위치를 원한다고 가정합니다. 마지막 위치([1:])에 대해 동일한 작업을 수행할 수 있습니다. Go는 사용자가 조각의 최신 위치(len()-1)까지의 모든 요소를 참조하려 한다고 가정합니다.

항목 추가

조각이 어떻게 작동하고 배열과 어떻게 유사한지를 살펴봤습니다. 이제 배열과 어떻게 다른지 알아보겠습니다. 첫 번째 차이점은 조각의 크기는 고정되지 않으며 동적이라는 것입니다. 조각을 만든 후에는 요소를 더 추가할 수 있으며 조각이 확장됩니다. 기본 배열에 발생하는 작업을 잠시 살펴보겠습니다.

조각에 요소를 추가하기 위해 Go는 append(slice, element) 기본 제공 함수를 제공합니다. 수정하려는 조각과 추가할 요소를 해당 함수에 값으로 전달합니다. 그러면 append 함수는 변수에 저장하는 새 조각을 반환합니다. 이 변수는 변경하려는 조각의 경우와 동일한 변수일 수 있습니다.

추가 프로세스가 코드에서는 어떻게 보이는지 살펴보겠습니다.

package main

import "fmt"

func main() {
    var numbers []int
    for i := 0; i < 10; i++ {
        numbers = append(numbers, i)
        fmt.Printf("%d\tcap=%d\t%v\n", i, cap(numbers), numbers)
    }
}

위의 코드를 실행하면 다음과 같이 출력됩니다.

0       cap=1   [0]
1       cap=2   [0 1]
2       cap=4   [0 1 2]
3       cap=4   [0 1 2 3]
4       cap=8   [0 1 2 3 4]
5       cap=8   [0 1 2 3 4 5]
6       cap=8   [0 1 2 3 4 5 6]
7       cap=8   [0 1 2 3 4 5 6 7]
8       cap=16  [0 1 2 3 4 5 6 7 8]
9       cap=16  [0 1 2 3 4 5 6 7 8 9]

이 출력은 흥미롭습니다. cap() 함수에 대한 호출이 반환하는 결과가 특히 흥미로웠습니다. 3번째 반복까지 모든 것이 정상으로 보였습니다. 3번째 반복에서 용량은 4로 바뀌고, 조각에는 요소가 세 개만 있습니다. 5번째 반복에서는 용량은 다시 8이 되고, 9번째 반복에서는 1에서 16으로 다시 바뀝니다.

용량 출력에서 패턴을 확인할 수 있나요? ‘조각에 더 많은 요소를 저장하기에 충분한 용량이 없는 경우’ Go에서 해당 용량을 2배로 만듭니다. 새 용량을 사용하여 새 기본 배열을 만듭니다. 이러한 용량 증가를 위해 사용자는 어떠한 작업도 수행할 필요가 없습니다. Go에서 자동으로 수행합니다. 하지만 주의해야 합니다. 특정 시점에서 조각에 필요한 용량보다 더 많은 용량이 있을 수 있고 메모리가 낭비됩니다.

항목 제거

요소를 제거하는 방법이 궁금할 것입니다. Go에는 조각에서 요소를 제거하는 기본 제공 함수가 없습니다. 필요한 요소만 사용하여 새 조각을 만들기 위해 앞에서 설명한 조각 연산자 s[i:p]를 사용할 수 있습니다.

예를 들어, 다음 코드는 조각에서 요소를 제거합니다.

package main

import "fmt"

func main() {
    letters := []string{"A", "B", "C", "D", "E"}
    remove := 2

	if remove < len(letters) {

		fmt.Println("Before", letters, "Remove ", letters[remove])

		letters = append(letters[:remove], letters[remove+1:]...)

		fmt.Println("After", letters)
	}

}

위의 코드를 실행하면 다음과 같이 출력됩니다.

Before [A B C D E] Remove  C
After [A B D E]

이 코드는 조각에서 요소를 제거합니다. 제거할 요소를 조각의 다음 요소로 대체하거나 마지막 요소를 제거하는 경우 아무 작업도 하지 않습니다.

또 다른 방법으로 조각의 새 복사본을 만들 수도 있습니다. 다음 섹션에서는 조각의 복사본을 만드는 방법을 알아봅니다.

조각의 복사본 만들기

Go에는 조각의 복사본을 만들 수 있는 기본 제공 copy(dst, src []Type) 함수가 있습니다. 대상 조각과 원본 조각을 보냅니다. 예를 들어, 다음 예제와 같이 조각의 복사본을 만들 수 있습니다.

slice2 := make([]string, 3)
copy(slice2, letters[1:4])

복사본을 만드는 데 관심이 있는 이유는 무엇인가요? 조각에서 요소를 변경하는 경우 기본 배열도 변경하게 됩니다. 동일한 기본 배열을 참조하는 다른 모든 조각이 영향을 받습니다. 이 프로세스를 코드에서 확인한 다음, 조각 복사본을 만들어 수정해 보겠습니다.

다음 코드를 사용하여 조각이 배열을 가리키고 조각에서 변경한 모든 내용이 기본 배열에 영향을 주는지 확인합니다.

package main

import "fmt"

func main() {
    letters := []string{"A", "B", "C", "D", "E"}
    fmt.Println("Before", letters)

    slice1 := letters[0:2]
    slice2 := letters[1:4]

    slice1[1] = "Z"

    fmt.Println("After", letters)
    fmt.Println("Slice2", slice2)
}

위의 코드를 실행하면 다음과 같은 출력이 표시됩니다.

Before [A B C D E]
After [A Z C D E]
Slice2 [Z C D]

slice1에서 변경한 내용이 letters 배열 및 slice2에 어떻게 영향을 미치는지 확인합니다. 출력에서 문자 B가 Z로 바뀌었고 letters 배열을 가리키는 모든 사용자에게 영향을 주는 것을 볼 수 있습니다.

이 문제를 해결하려면 조각 복사본을 만들어야 합니다. 이 경우 내부적으로 새 기본 배열이 만들어집니다. 다음 코드를 사용할 수 있습니다.

package main

import "fmt"

func main() {
    letters := []string{"A", "B", "C", "D", "E"}
    fmt.Println("Before", letters)

    slice1 := letters[0:2]

    slice2 := make([]string, 3)
    copy(slice2, letters[1:4])

    slice1[1] = "Z"

    fmt.Println("After", letters)
    fmt.Println("Slice2", slice2)
}

위의 코드를 실행하면 다음과 같은 출력이 표시됩니다.

Before [A B C D E]
After [A Z C D E]
Slice2 [B C D]

어떻게 slice1의 변경 내용이 기본 배열에는 영향을 주었지만 새 slice2에는 영향을 주지 않았는지 확인합니다.