이 글은 Effective Go를 일부 번역한 글입니다.


자료

배열

배열은 메모리 형태를 자세히 설계하기에 좋고 가끔은 할당을 피하는 데 쓸 수도 있지만, 주로 다음 절의 주제인 조각의 구성 요소로 사용한다. 조각을 다루기 전에 배열을 조금 다루겠다.

Go와 C는 배열 작동 방법에 큰 차이가 있다. Go에서

  • 배열은 값이다. 배열을 다른 배열에 할당하면 모든 요소를 복사한다.
  • 특히, 함수에 배열을 넘기면 배열을 가리키는 포인터가 아니라 배열 복사본을 받는다.
  • 배열 크기는 배열 형의 일부다. [10]int와 [20]int는 다르다.

값 특성은 쓰기엔 좋지만 비용이 클 수도 있다. C처럼 작동하고 효율적이길 원한다면 배열 포인터를 넘기라.

func Sum(a *[3]float64) (sum float64) {
    for _, v := range *a {
        sum += v
    }
    return
}

array := [...]float64{7.0, 8.5, 9.1}
x := Sum(&array)  // Note the explicit address-of operator

하지만 이 방식도 자연스러운 Go는 아니다. 조각을 대신 쓰라.

조각(Slice)

조각은 배열을 감싸 더 보편적이며 강력하고 간편한 자료 배열 인터페이스를 제공한다. 변환 행렬처럼 크기가 분명한 항목을 제외하면 Go에서 배열 프로그래밍 대부분은 단순한 배열 대신 조각으로 한다.

조각은 기반 배열을 참조하며, 조각을 다른 조각에 할당하면 둘 다 같은 배열을 참조한다. 함수 인수로 받은 조각은 기반 배열을 포인터로 받은 것처럼 수정 내용을 함수 호출자가 볼 수 있다. 따라서 Read 함수는 포인터와 그 크기가 아닌 조각을 인수로 받는다. 조각에 그 길이가 포함돼 있어 읽을 자료 양의 상한선을 정하기 때문이다. os 패키지에 있는 File 형의 Read 메서드 형태를 보라.

func (f *File) Read(buf []byte) (n int, err error)

이 메서드는 읽은 바이트 숫자를 반환하고 오류가 있다면 오류를 반환한다. 버퍼 buf가 32 바이트보다 클 때 처음 32 바이트만 읽어 채우려면 버퍼를 조각내라.

   n, err := f.Read(buf[0:32])

이렇게 조각내는 게 일반적이고 효율적이다. 실제로, 효율 면에서 잠시 벗어나 보면 아래 토막으로도 버퍼의 처음 32 바이트만 읽어낼 수 있다.

    var n int
    var err error
    for i := 0; i < 32; i++ {
        nbytes, e := f.Read(buf[i:i+1])  // Read one byte.
        if nbytes == 0 || e != nil {
            err = e
            break
        }
        n += nbytes
    }

조각 길이를 기반 배열 한계 내에서 바꿀 수 있다. 조각에 조각의 조각을 할당하면 된다. 내장 함수 cap으로 접근할 수 있는 조각 용량으로 조각이 쓸 수 있는 최대 길이를 알 수 있다. 아래 함수는 조각에 자료를 덧붙이는 함수다. 자료가 용량을 넘어서면 조각을 다시 할당하고, 그 결과로 나온 조각을 반환한다. 이 함수는 nil 조각에 len과 cap을 사용할 수 있고, 0이 나온다는 사실을 이용한다.

func Append(slice, data []byte) []byte {
    l := len(slice)
    if l + len(data) > cap(slice) {  // reallocate
        // Allocate double what's needed, for future growth.
        newSlice := make([]byte, (l+len(data))*2)
        // The copy function is predeclared and works for any slice type.
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0:l+len(data)]
    for i, c := range data {
        slice[l+i] = c
    }
    return slice
}

그 뒤에 반드시 조각을 반환해야 한다. Append가 slice의 요소를 수정할 수 있더라도, slice 자체는 (포인터, 길이, 용량을 담은 런타임 자료 구조체) 값으로 넘어왔기 때문이다.

조각에 덧붙이는 개념은 매우 유용해서 내장 함수 append에 넣었다. 이 함수의 설계를 이해하려면 더 많은 정보가 있어야 하니, 나중에 다시 논하겠다.

2차원 조각

Go의 배열과 조각은 1차원이다. 2차원 배열과 조각을 만들려면 배열의 배열이나 조각의 조각을 정의해야 한다. 이렇게 말이다.

type Transform [3][3]float64  // A 3x3 array, really an array of arrays.
type LinesOfText [][]byte     // A slice of byte slices.

조각은 길이를 고정하지 않아 안쪽 조각 각각 길이가 다를 수 있다. 이런 경우가 흔할 수도 있다. 아래 LinesOfText 예를 보라. 각 줄의 길이가 제각각이다.

text := LinesOfText{
	[]byte("Now is the time"),
	[]byte("for all good gophers"),
	[]byte("to bring some fun to the party."),
}

가끔 2차원 조각을 할당할 때, 예를 들면 픽셀 행들을 훑을 때, 두 가지 방법이 있다. 하나는 조각을 각각 할당하는 것이고, 다른 하나는 배열 하나를 할당한 다음 조각이 그 안을 가리키게 하는 것이다. 응용 프로그램에 따라 선택해 사용하라. 조각이 커지거나 작아질 수 있다면 다음 행에 덧쓰는 걸 막기 위해 각각 할당해야 한다. 아니라면 할당 하나로 객체를 구성하는 게 더 효율적일 수 있다. 아래 요약한 두 메서드를 참고하라. 먼저, 한 번에 한 행씩이다.

// Allocate the top-level slice.
picture := make([][]uint8, YSize) // One row per unit of y.
// Loop over the rows, allocating the slice for each row.
for i := range picture {
	picture[i] = make([]uint8, XSize)
}

그리고 이번엔 한 번 할당하고 행으로 나누기다.

// Allocate the top-level slice, the same as before.
picture := make([][]uint8, YSize) // One row per unit of y.
// Allocate one large slice to hold all the pixels.
pixels := make([]uint8, XSize*YSize) // Has type []uint8 even though picture is [][]uint8.
// Loop over the rows, slicing each row from the front of the remaining pixels slice.
for i := range picture {
	picture[i], pixels = pixels[:XSize], pixels[XSize:]
}