go언어
이 글은 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:] }