Skip to content

Latest commit

 

History

History
639 lines (471 loc) · 28.6 KB

20-37.md

File metadata and controls

639 lines (471 loc) · 28.6 KB

데이터 구조(Data Structures)

배열(Array)

CPU 캐시(CPU Cache)

코어들은 메인 메모리로 바로 접근하지 않고 로컬 캐시로 접근한다. 캐시에는 데이터와 명령어가 저장되어 있다.

캐시 속도는 L1, L2, L3, 메인 메모리 순으로 빠르다. Scott Meyers에 따르면 "만약 퍼포먼스가 중요하다면 모두 캐시 메모리로 접근해야 한다"고 한다. 메인 메모리 접근은 굉장히 느리다. 사실상 접근이 힘들다고 보면 된다.

그래서 캐시 미스(cache miss)가 발생하지 않거나 잠재적인 문제로부터 최소화하는 캐싱 시스템(caching system)은 어떻게 작성해야 할까?

프로세서(Processor)는 프리페처(Prefetcher)를 가지고 있다. 프리페처는 어떤 데이터가 필요할지 예상한다. 데이터 단위(granularity)는 사용하는 기계에 따라 다르다. 대체로 프로그래밍 모델은 바이트를 사용한다. 한 번에 바이트를 읽고 쓸 수 있다. 그러나, 캐싱 시스템의 관점에서 보면 데이터 단위는 1바이트가 아니다. 캐싱 시스템의 관점에서 데이터의 단위는 64바이트고 이걸 캐시 라인(cache line)이라고 부른다. 모든 메모리는 64바이트의 캐시 라인으로 구성되어 있다. 캐싱 메커니즘(caching mechanism)은 복잡하지만 프리페처는 모든 지연 시간을 없애려 한다. 프리페처가 예측 가능한 데이터 접근 패턴을 파악 할 수 있어야 한다. 즉, 예측 가능한 데이터 접근 패턴을 생성하는 코드를 작성해야 한다.

쉬운 방법의 하나는 메모리의 연속 할당을 만들고 이것을 순회하는 것이다. 배열(Array) 데이터 구조는 연속 할당과 순회를 할 수 있게 해준다. 하드웨어 관점에서 배열은 가장 중요한 데이터 구조이며 Go 관점에서 슬라이스(slice)가 그러하다. C++의 vector와 마찬가지로 슬라이스는 배열 기반 자료구조이다. 사이즈가 어떻든 배열을 할당하면, 모든 원소는 각각 다른 원소로부터 같은 거리를 갖게 된다. 배열을 순회하는 건 캐시 라인을 한줄 한줄 순회하는 것과 같다. 프리페처는 접근 패턴을 고르고 모든 지연 시간을 숨긴다.

예를 들어, 큰 nxn 행렬이 있다고 하자. 연결 리스트 순회(LinkedList Traverse), 열 순회(Column Traverse), 행 순회(Row Traverse)를 했을 때 각 순회의 성능을 측정해보자. 당연하게도 행 순회가 가장 높은 성능을 가진다. 행 순회는 행렬의 캐시 라인을 순회하면서 예측 가능한 접근 패턴을 만든다. 이와 달리 열 순회는 캐시 라인을 순회하지 않는다. 메모리 임의 접근 패턴을 가지고 있다. 이게 성능이 제일 느린 이유다. 연결 리스트 순회의 성능이 왜 중간인지 설명하지 않았다. 단순히 열 순회만큼 성능이 안 좋을 거라고 생각해 볼 수 있다. 자세한 이해를 위해 또 다른 캐시인 TLB - 변환 색인 버퍼(Translation Lookaside Buffer)를 알아보자. 변환 색인 버퍼는 물리적 메모리에 있는 운영 체제의 페이지(page)와 오프셋(offset)을 유지한다.

변환 색인 버퍼(Translation Lookaside Buffer)

캐싱 시스템은 하드웨어로 한 번에 64바이트씩 데이터를 옮긴다. 데이터 단위는 기계 별로 다르듯이, 운영 체제는 4k(운영 체제의 기존 페이지 크기) 바이트씩 페이징 함으로써 메모리를 관리한다.

관리되는 모든 페이지는 가상 메모리 주소(소프트웨어는 가상 주소를 물리적 메모리를 사용하고 공유하는 샌드박스에서 실행한다)를 갖게 되는데, 올바른 페이지에 매핑되고 물리적 메모리로 오프셋 하기 위해 사용된다.

변환 색인 버퍼의 미스는 캐시 미스보다 나쁠 수 있다. 연결 리스트 순회가 중간 성능인 이유는 다수의 노드가 같은 페이지에 있기 때문이다. 캐시 라인은 예측 가능한 거리를 요구하지 않기 때문에 캐시 미스가 발생 할 수 있지만, 많은 변환 색인 버퍼의 미스는 발생하지 않을 수 있다. 열 순회에서는 캐시 미스뿐만 아니라 엑세스할 때마다 변환 색인 버퍼의 캐시 미스가 발생 할 수 있다.

즉, 데이터 지향 설계가 중요하다. 효율적인 알고리즘을 작성하는 것에 그치지 않고, 어떻게 데이터에 접근하는 것이 알고리즘보다 성능에 좋은 영향을 미칠지 고려해야 한다.

선언과 초기화(Declare and initialize)

문자열이 원소이고 길이가 5인 배열을 선언하고, 제로 값(zero value)으로 초기화 해보자. 다시 한 번 더 말하지만, 문자열은 포인터와 길이를 표현하는 두 워드(word)로 이루어진 데이터 구조다. 이 배열을 제로 값(zero value)으로 설정하면, 배열속의 모든 문자열도 제로 값(zero value)이 된다. 각각의 문자열의 첫 번째 워드는 nil을 가리키고 두 번째 워드는 0이 된다.

22-1

var strings [5]string

인덱스 0의 문자열은 이제 바이트들(문자열을 구성하는 문자들)을 실제로 저장하고 있는 배열에 대한 포인터와 길이 정보 5를 가지게 된다.

비용이 얼마나 드는가?(What is the cost?)

할당에는 2 바이트를 복사하는 비용이 발생한다. 두 문자열은 같은 배열을 가리키며, 그래서 할당의 비용은 2 단어에 대한 비용뿐이다.

strings[0] = "Apple"

22-2

슬라이스의 남은 부분에도 값을 할당한다.

strings[1] = "Orange"
strings[2] = "Banana"
strings[3] = "Grape"
strings[4] = "Plum"

문자열 배열 반복(Iterate over the array of strings)

range를 사용하면, 인덱스와 복사된 원소의 값을 얻을 수 있다. for 문 내에서 fruit 변수는 문자열 값을 가지게 된다. 첫 번째 반복에서는 "Apple"을 가진다. 이 문자열 역시 위 이미지의 (1) 배열을 가리키는 워드와 길이 5를 나타내는 두 번째 워드를 가진다. 이제 세 개의 문자열의 같은 배열을 공유하고 있다.

Println 함수에는 무엇을 전달하는가

여기서는 value의 의미로서 사용한다. 문자열 값을 공유하지 않는다. Println은 문자열의 값을 복사해서 가진다. Println을 호출 할 때 같은 배열을 공유하는 4개의 문자열을 가지게 되는 것이다. 문자열의 주소를 함수에 전달하지 않으면 이점이 있다. 문자열의 길이를 알고 있으니 스택에 둘 수 있고, 그 덕분에 힙에 할당하여 GC를 해야하는 부담을 덜게 된다. 문자열은 값을 전달하여 스택에 둘 수 있게 디자인 되어, 가비지가 생성되지 않는다. 그래서 문자열(들)이 가리키는 배열만이 힙에 저장되고 공유된다.

fmt.Printf("\n=> Iterate over array\n")
for i, fruit := range strings {
    fmt.Println(i, fruit)
}
=> Iterate over array
0 Apple
1 Orange
2 Banana
3 Grape
4 Plum

4개의 정수를 가지는 배열을 선언하고 리터럴 표기법(literal syntax)으로 특정 값으로 초기화 한다.

numbers := [4]int{10, 20, 30, 40}

전통적인 방법으로 배열을 반복한다.

fmt.Printf("\n=> Iterate over array using traditional style\n"​)

for i := 0;i < len(numbers);i++ {
    fmt.Println(i, numbers[i])
}
=> Iterate over array using traditional style
0 10
1 20
2 30
3 40

다른 타입의 배열들(Different type arrays)

제로 값으로 초기화 된 길이가 5인 정수 형 배열을 선언하자.

var five [5]int

특정 값으로 초기화 된 길이가 4인 정수 형 배열을 선언하자.

four := [4]int{10, 20, 30, 40}
fmt.Printf("\n=> Different type arrays\n")
fmt.Println(five)
fmt.Println(four)
=> Different type arrays
[0 0 0 0 0]
[10 20 30 40]

five = four와 같이 변수 four를 변수 five에 할당하려고 할 때, 컴파일러는 "cannot use four (type [4]int) as type [5]int in assignment"라는 메세지를 출력한다. 타입(길이와 표현)이 다르기 때문에 할당 할 수 없다. 배열의 크기는 타입명에 표시 된다: [4]int vs [5]int. 이것은 포인터의 표현과 같은 맥락이다. *int의 *은 연산자가 아니고 타입명의 일부이다. 당연하게도 모든 배열은 컴파일 타임(compile time) 때 정해진 크기를 갖게 된다.

연속 메모리 할당(Contiguous memory allocations)

특정 값들로 초기화 된 길이가 6인 문자열 배열을 선언하자.

six := [6]string{"Annie", "Betty", "Charley", "Doug", "Edward", "Hoanh"}

이 배열을 반복하면서 각 원소의 값과 주소를 출력하자. Printf의 결과를 보면, 이 배열은 연속 된 메모리 블록으로 이루어진 것을 알 수 있다. 문자열은 두 워드로 되어 있고, 컴퓨터 아키텍처에 따라 x 바이트를 가지게 된다. 연속 된 두 IndexAddr의 거리는 정확히 x 바이트이다. 변수 v는 스택에 있고 매번 같은 주소를 가진다.

fmt.Printf("\n=> Contiguous memory allocations\n")
for i, v := range six {
    fmt.Printf("Value[%s]\tAddress[%p] IndexAddr[%p]\n", v, &v, &six[i])
}
=> Contiguous memory allocations
Value[Annie] Address[0xc000010250] IndexAddr[0xc000052180]
Value[Betty] Address[0xc000010250] IndexAddr[0xc000052190]
Value[Charley] Address[0xc000010250] IndexAddr[0xc0000521a0]
Value[Doug] Address[0xc000010250] IndexAddr[0xc0000521b0]
Value[Edward] Address[0xc000010250] IndexAddr[0xc0000521c0]
Value[Hoanh] Address[0xc000010250] IndexAddr[0xc0000521d0]

슬라이스 (Slice)

선언과 초기화 (Declare and initialize)

5개의 요소를 갖는 슬라이스(slice)를 생성해보자. make 함수는 슬라이스(slice)맵(map) 그리고 채널(channel) 타입에서 사용하는, 특별한 내장 함수이다. make 함수를 사용하여 5개의 문자열 배열을 갖는 슬라이스를 생성하면, 3개의 워드(word) 데이터 구조가 만들어진다. 첫 번째 워드는 배열을 위치를 가리키고 두 번째 워드는 길이를, 세 번째 워드는 용량을 나타낸다.

25

길이와 용량 (Length vs Capacity)

길이(Length)는, 포인터의 위치에서부터 접근해서 읽고 쓸 수 있는 요소의 수를 의미하며, 용량(Capacity)은 포인터의 위치에서부터 배열에 존재할 수 있는 요소의 총량을 뜻한다.

문법적 설탕(syntactic sugar)을 사용하기에, 슬라이스는 언뜻 배열처럼 보인다. 비용도 배열과 동일하게 발생한다. 하지만, 한 가지 다른 점은 make 함수의 []string의 대괄호 안에 값이 없다는 것이다. 이것으로 배열과 슬라이스를 구분할 수 있다.

slice1 := make([]string, 5)
slice1[0] = "Apple"
slice1[1] = "Orange"
slice1[2] = "Banana"
slice1[3] = "Grape"
slice1[4] = "Plum"

슬라이스의 길이를 넘는 인덱스에는 접근할 수 없다.

Error: panic: runtime error: index out of range slice1[5] = "Runtime error"

슬라이스의 주소가 아닌 값을 전달한다. 따라서, Println함수는 슬라이스의 복사본을 갖게 된다.

fmt.Printf("\n=> Printing a slice\n")
fmt.Println(slice1)
=> Printing a slice
[Apple Orange Banana Grape Plum]

참조 타입 (Reference type)

5개의 요소를 갖고 용량이 8개인 슬라이스를 만들기 위해 make 키워드를 이용할 수 있으며, 이를 통해 초기화 시점에 직접 용량을 정할 수 있다.

결국 우리는, 차례대로 8개의 요소를 갖는 배열을 가르키는 포인터와 길이는 5, 용량은 8을 갖는 3개의 워드(word)형 자료 구조를 갖게된다. 이는 첫 5개의 요소에 대해 읽고 쓸 수 있으며, 필요시 이용 가능한 3개의 용량을 갖는 것을 뜻한다.

27

slice2 := make([]string, 5, 8)
slice2[0] = "Apple"
slice2[1] = "Orange"
slice2[2] = "Banana"
slice2[3] = "Grape"
slice2[4] = "Plum"
fmt.Printf("\n=> Length vs Capacity\n")
inspectSlice(slice2)
// inspectSlice는 리뷰를 위해 슬라이스 헤더를 보여주는 함수이다.
// 파라미터 : 다시 말하지만, []string의 대괄호 속에 값이 없으므로 슬라이스를 사용함을 알 수 있다.
// 배열에서 했던 것과 마찬가지로, 슬라이스를 순회한다.
// `len`이 슬라이스의 길이를 알려주며, `cap`은 슬라이스의 용량을 알려준다.
// 결과를 보면, 예상대로 슬라이스의 주소 값들이 정렬되어 표시되는 것을 볼 수 있다.
func inspectSlice(slice []string) {
    fmt.Printf("Length[%d] Capacity[%d]\n", len(slice), cap(slice))
    for i := range slice {
        fmt.Printf("[%d] %p %s\n", i, &slice[i], slice[i])
    }
}
=> 길이 vs 용량 (Length vs Capacity)
Length[5] Capacity[8]
[0] 0xc00007e000 Apple
[1] 0xc00007e010 Orange
[2] 0xc00007e020 Banana
[3] 0xc00007e030 Grape
[4] 0xc00007e040 Plum

추가에 관한 생각: 슬라이스를 동적 자료구조로 만들기. (Idea of appending: making slice a dynamic data structure)

문자열로 구성될 nil 슬라이스를 선언하고 그 값을 제로 값(zero value)으로 설정한다. 이 때, 첫 번째 워드(word)는 nil을 가르키는 포인터로, 두 번째와 세 번째 값은 0을 나타내는 3개의 워드(word) 자료구조를 갖는다.

var data []string

만약, data := string{}을 하게되면, 이 둘은 서로 같을까?

그렇지 않다. 왜냐하면 이 경우, 데이터는 값이 제로 값(zero value)으로 설정되지 않기 때문이다. 빈 리터럴로 생성되는 모든 타입이 제로 값(zero value)을 반환하지는 않기 때문에, 제로 값(zero value)을 위해 var을 사용하는 이유기도하다. 위의 경우에, 반환 되는 슬라이스는 nil 슬라이스가 아닌 포인터를 갖고 있는 빈 슬라이스가 된다. nil 슬라이스와 빈 슬라이스 간에는 각기 다른 의미가 있는데, 제로 값(zero value)으로 설정된 참조 타입은 nil로 여길 수 있다는 점이다. marshal 함수에 nil 슬라이스를 넘긴다면 null을 반환하고, 빈 슬라이스를 넘긴다면 빈 JSON을 반환하게 된다. 그렇다면 이 때 포인터는 어떤 것을 가르키게 될까? 바로, 나중에 살펴볼 빈 구조체를 가르킨다.

슬라이스의 용량을 가져오자.

    lastCap := cap(data)

슬라이스에 약 10만개의 문자열을 덧붙인다.

    for record := 1;record <= 102400;record++ {

내장 함수인 append를 사용해서 슬라이스를 덧붙일 수 있다. 이 함수를 통해 슬라이스에 값을 추가할 수 있으며 자료 구조를 동적으로 만들 수 있으면서도, 기계적 동정심(mechanical sympathy)을 통해 예측 가능한 접근 패턴을 제공함으로써 여전히 인접한 메모리 블럭을 이용할 수 있게 된다. append 함수는 값 개념(value semantic)으로 동작한다. 슬라이스 자체를 공유하는 것이 아니라, 슬라이스에 값을 덧붙이고 그 복사본을 반환하는 식이다. 따라서 슬라이스는 힙 메모리가 아닌 스택에 위치하게 된다.

    data = append(data, fmt.Sprintf("Rec: %d", record))

append가 동작할 때 마다, 매번 길이와 용량을 확인한다. 만약 두 값이 동일하다면, 더 이상 남은 공간이 없다는 것을 뜻한다. 이 때, append 함수는 기존보다 2배를 늘린 크기를 갖는 새로운 배열을 만들어서 예전 값을 복사한 뒤, 새 값을 추가하게 된다. 그리고 스택 프레임에 존재하는 값을 변경시킨 뒤, 그 복사본을 반환한다. 그렇게 기존의 슬라이스가 새로운 복사본으로 치환된다. 만약 길이와 용량이 같지 않다면, 슬라이스 안에 아직 사용할 수 있는 공간이 남아있다는 것을 뜻하므로, 새 복사본을 만드는 일 없이 값을 추가 할 수 있다. 이것은 굉장히 효율적이다. 출력의 마지막 열을 확인해보자. 배열의 요소가 1000개 혹은 그 이하일 때, 배열의 크기는 2배로 늘어난다. 요소의 개수가 1000개를 넘고 나면, 용량의 변화율은 25%로 변한다. 슬라이스의 용량이 변경될 때, 그 변화를 나타낸다.

    if lastCap != cap(data) {

변화율을 계산한다.

    capChg := float64(cap(data)-lastCap) / float64(lastCap) * 100

lastCap에 새 용량을 저장한다.

    lastCap = cap(data)

결과를 표시한다.

    fmt.Printf("Addr[%p]\tIndex[%d]\t\tCap[%d - %2.f%%]\n", &data[0], record, cap(data), capChg)
=> Idea of appending
Addr[0xc0000102a0] Index[1] Cap[1 - +Inf%]
Addr[0xc00000c0c0] Index[2] Cap[2 - 100%]
Addr[0xc000016080] Index[3] Cap[4 - 100%]
Addr[0xc00007e080] Index[5] Cap[8 - 100%]
Addr[0xc000100000] Index[9] Cap[16 - 100%]
Addr[0xc000102000] Index[17] Cap[32 - 100%]
Addr[0xc00007a400] Index[33] Cap[64 - 100%]
Addr[0xc000104000] Index[65] Cap[128 - 100%]
Addr[0xc000073000] Index[129] Cap[256 - 100%]
Addr[0xc000106000] Index[257] Cap[512 - 100%]
Addr[0xc00010a000] Index[513] Cap[1024 - 100%]
Addr[0xc000110000] Index[1025] Cap[1280 - 25%]
Addr[0xc00011a000] Index[1281] Cap[1704 - 33%]
Addr[0xc000132000] Index[1705] Cap[2560 - 50%]
Addr[0xc000140000] Index[2561] Cap[3584 - 40%]
Addr[0xc000154000] Index[3585] Cap[4608 - 29%]
Addr[0xc000180000] Index[4609] Cap[6144 - 33%]
Addr[0xc000198000] Index[6145] Cap[7680 - 25%]
Addr[0xc0001b6000] Index[7681] Cap[9728 - 27%]
Addr[0xc000200000] Index[9729] Cap[12288 - 26%]
Addr[0xc000230000] Index[12289] Cap[15360 - 25%]
Addr[0xc000280000] Index[15361] Cap[19456 - 27%]
Addr[0xc000300000] Index[19457] Cap[24576 - 26%]
Addr[0xc000360000] Index[24577] Cap[30720 - 25%]
Addr[0xc000400000] Index[30721] Cap[38400 - 25%]
Addr[0xc000300000] Index[38401] Cap[48128 - 25%]
Addr[0xc000600000] Index[48129] Cap[60416 - 26%]
Addr[0xc0006ec000] Index[60417] Cap[75776 - 25%]
Addr[0xc000814000] Index[75777] Cap[94720 - 25%]
Addr[0xc000600000] Index[94721] Cap[118784 - 25%]

슬라이스의 슬라이스

slice2의 인덱스 2, 인덱스 3의 값을 갖는 slice3을 생성하자. slice3의 길이는 2이고 용량은 6이다.

매개변수는 [시작 인덱스:(시작 인덱스 + 길이)] 형태이다.

결과를 통해 두 슬라이스는 같은 배열을 공유하고 있는 것을 알 수 있다. 슬라이스의 헤더는 값의 개념으로 사용 될 때 스택에 존재한다. 오직 공유되는 배열만이 힙에 위치한다.

slice3 := slice2[2:4]
fmt.Printf("\n=> Slice of slice (before)\n")
inspectSlice(slice2)
inspectSlice(slice3)

slice3의 인덱스 0의 값을 바꾸면, 어떤 슬라이스가 변경 될까?

=> Slice of slice (before)
Length[5] Capacity[8]
[0] 0xc00007e000 Apple
[1] 0xc00007e010 Orange
[2] 0xc00007e020 Banana
[3] 0xc00007e030 Grape
[4] 0xc00007e040 Plum
Length[2] Capacity[6]
[0] 0xc00007e020 Banana
[1] 0xc00007e030 Grape
slice3[0] = "CHANGED"

두 슬라이스 모두 변한다. 생성되어 있는 슬라이스를 변경한다는 것을 잊지 말아야 한다. 어디서 이 슬라이스를 사용하는지, 또 배열을 공유하고 있는지를 주의깊게 살펴야 한다.

fmt.Printf("\n=> Slice of slice (after)\n")
inspectSlice(slice2)
inspectSlice(slice3)
=> Slice of slice (after)
Length[5] Capacity[8]
[0] 0xc00007e000 Apple
[1] 0xc00007e010 Orange
[2] 0xc00007e020 CHANGED
[3] 0xc00007e030 Grape
[4] 0xc00007e040 Plum
Length[2] Capacity[6]
[0] 0xc00007e020 CHANGED
[1] 0xc00007e030 Grape

slice3 := append(slice3, "CHANGED")는 어떨까? 슬라이스의 길이와 용량이 다르면, append를 사용 할 때 비슷한 문제가 발생한다. slice3의 0번째 인덱스 값을 변경하는 대신에, append를 호출 해보자. slice3의 길이는 2이고 용량은 6이라서 수정을 위한 여유 공간을 가지고 있다. slice2의 4번째 인덱스와 같은 주소 값을 가지는 slice3의 3번째 인덱스의 원소부터 변경 된다. 이런 상황은 굉장히 위험하다. 그러면 슬라이스의 길이와 용량이 서로 같으면 어떨까? 슬라이싱 구문(slicing syntax)의 또 다른 매개변수를 추가하여, slice3의 용량을 6 대신 2로 만들어보자: slice3 := slice2[2:4:4]

길이와 용량이 같은 슬라이스에 대해 append가 호출 되면, slice2의 4번째 원소를 가지고 오지 않는다. 이것은 분리되어 있다. slice3는 길이가 2이고 용량이 2이면서 여전히 slice2와 같은 배열을 공유하고 있다. append가 호출 되면, 길이와 용량이 달라지게 된다. 주소 또한 달라지게 된다. 길이는 3인 새로운 슬라이스가 된다. 새로운 슬라이스 소유의 배열을 갖게 되고 더 이상 원본 슬라이스의 영향을 받지 않는다.

슬라이스 복사

복사는 문자열과 슬라이스 타입에서만 동작 한다. 원본의 요소들을 담을 수 있을 만큼의 크기로 새로운 슬라이스를 만들고 내장 함수인 copy를 사용해서 값을 복사한다.

slice4 := make([]string, len(slice2))
copy(slice4, slice2)

fmt.Printf("\n=> Copy a slice\n")
inspectSlice(slice4)
=> Copy a slice
Length[5] Capacity[5]
[0] 0xc00005c050 Apple
[1] 0xc00005c060 Orange
[2] 0xc00005c070 CHANGED
[3] 0xc00005c080 Grape
[4] 0xc00005c090 Plum

슬라이스와 참조

길이가 7인 정수형 슬라이스를 선언하자.

x := make([]int, 7)

임의의 값을 넣어준다.

for i := 0; i < 7; i++ {
    x[i] = i * 100
}

슬라이스의 두 번째 원소의 포인터를 변수에 할당한다.

twohundred := &x[1]

슬라이스에 새로운 값을 추가해보자. 이 코드는 위험 하다. x 슬라이스는 길이가 7이고 용량 7이다. 길이와 용량이 같기 때문에 용량이 두 배로 늘어나고 값들이 복사된다. 이제 x 슬라이스는 길이가 8이고 용량이 14이며 다른 메모리 블록을 가르킨다.

x = append(x, 800)

슬라이스의 두 번째 원소의 값을 변경 할 때, twohundred는 변경되지 않는다. 이전의 슬라이스를 가리키기 때문이다. 이 변수를 읽을 때 마다, 잘못된 값을 얻는다.

x[1]++

결과를 출력함으로써, 문제를 확인 할 수 있다.

fmt.Printf("\n=> Slice and reference\n")
fmt.Println("twohundred:", *twohundred, "x[1]:", x[1])
=> Slice and reference
twohundred: 100 x[1]: 101

UTF-8

Go의 모든 것은 UTF-8 문자 집합(chrarcter sets)을 근간으로 한다. 만약 다른 인코딩 구조를 사용한다면 문제가 발생한다.

중국어와 영어로 문자열을 선언하자. 중국 문자는 각각 3 바이트를 사용한다. UTF-8는 바이트, 코드 포인트(code point) 그리고 문자로 3 계층을 이루고 있다. Go 관점에서 문자열은 단지 저장되는 바이트일 뿐이다.

아래 예제에서 첫 번째 3 바이트는 하나의 코드 포인트를 표현한다. 하나의 코드 포인트는 하나의 문자를 표현한다. 1 바이트부터 4 바이트를 가지고 코드 포인트를 표현 할 수 있고(코드 포인트는 32 비트 값이다.) 1 부터 대다수의 코드 포인트는 문자를 표현 할 수 있다. 간단하게, 3 바이트로 1 코드 포인트로 1 문자를 표현한다. 그래서 s 문자열을 3 바이트, 3 바이트, 1 바이트, 1 바이트.. 로 읽는다(앞 부분에 중국 문자가 2개 있고 나머지는 영어이기 때문에)

s := "世界 means world"

UTFMax 상수 4다. -- 인코딩 된 룬당 최대 4 바이트 -> 모든 코드 포인트를 표현하기 위해 필요한 최대 바이트 수는 4 바이트다.[재방문] Rune은 자체 타입이다. 이것은 int32 타입의 별칭이다. 우리가 사용하는 byte 타입은 uint8의 별칭이다.

var buf [utf8.UTFMax]byte

위의 문자열을 순회 할 때, 바이트에서 바이트, 코드 포인트에서 코드 포인트, 문자에서 문자로 중 어느 방식으로 순회할까? 정답은 코드 포인트에서 코드 포인트이다. 첫 번째 순회에서 i는 0이다. 그 다음 i는 다음 코드 포인트로 이동되기 때문에 3이다. 그 다음은 6이다.

for i, r := range s {

룬/코드 포인트의 바이트 수를 출력해보자.

    rl := utf8.RuneLen(r)

룬을 표현하는 바이트들의 차이를 계산하자.

    si := i + rl

문자열로부터 룬을 버퍼에 복사하자. 모든 코드 포인트를 순회하며 배열 버퍼에 복사하고 화면에 출력하려는 것이다. Go 에서 "배열은 언제든 슬라이스가 될 준비가 되어있다." 슬라이싱 구문을 사용하여 buf 배열을 가리키는 슬라이스 헤더를 만든다. 헤더는 스택에 생성되기에 힙에 할당하지 않는다.

    copy(buf[:], s[i:si])

출력해보자.

    fmt.Printf("%2d: %q; codepoint: %#6x; encoded bytes: %#v\n"​, i, r, r, buf[:rl])
0: '世'; codepoint: 0x4e16; encoded bytes: []byte{0xe4, 0xb8, 0x96}
3: '界'; codepoint: 0x754c; encoded bytes: []byte{0xe7, 0x95, 0x8c}
6: ' '; codepoint: 0x20; encoded bytes: []byte{0x20}
7: 'm'; codepoint: 0x6d; encoded bytes: []byte{0x6d}
8: 'e'; codepoint: 0x65; encoded bytes: []byte{0x65}
9: 'a'; codepoint: 0x61; encoded bytes: []byte{0x61}
10: 'n'; codepoint: 0x6e; encoded bytes: []byte{0x6e}
11: 's'; codepoint: 0x73; encoded bytes: []byte{0x73}
12: ' '; codepoint: 0x20; encoded bytes: []byte{0x20}
13: 'w'; codepoint: 0x77; encoded bytes: []byte{0x77}
14: 'o'; codepoint: 0x6f; encoded bytes: []byte{0x6f}
15: 'r'; codepoint: 0x72; encoded bytes: []byte{0x72}
16: 'l'; codepoint: 0x6c; encoded bytes: []byte{0x6c}
17: 'd'; codepoint: 0x64; encoded bytes: []byte{0x64}

맵(Map)

프로그램에서 사용할 user를 정의한다.

type user struct {
    name string
    username string
}

선언과 초기화(Declare and initialize)

string 타입을 키로, user 타입을 값으로 갖는 맵을 선언하고 만든다.

func main() {
    users1 := make(map[string]user)

    // 맵에 키/값 쌍을 추가한다.
    users1["Roy"] = user{"Rob", "Roy"}
    users1["Ford"] = user{"Henry", "Ford"}
    users1["Mouse"] = user{"Mickey", "Mouse"}
    users1["Jackson"] = user{"Michael", "Jackson"}

    // `map`을 순회한다.
    fmt.Printf("\n=> Iterate over map\n")
    for key, value := range users1 {
            fmt.Println(key, value)
}
=> Iterate over map
Roy {Rob Roy}
Ford {Henry Ford}
Mouse {Mickey Mouse}
Jackson {Michael Jackson}

맵 리터럴(Map literals)

초기값을 갖는 맵을 선언하고 초기화한다.

users2 := map[string]user{
    "Roy": {"Rob", "Roy"},
    "Ford": {"Henry", "Ford"},
    "Mouse": {"Mickey", "Mouse"},
    "Jackson": {"Michael", "Jackson"},
}
// 맵을 순회한다.
fmt.Printf("\n=> Map literals\n")
for key, value := range users2 {
    fmt.Println(key, value)
}
=> Map literals
Roy {Rob Roy}
Ford {Henry Ford}
Mouse {Mickey Mouse}
Jackson {Michael Jackson}

키 삭제(Delete key)

delete(users2, "Roy")

키 찾기(Find key)

Roy를 찾아보자. 만약 키 중에 Roy가 존재한다면, 그에 해당하는 값을 가져와 할당한다. 그렇지 않다면 u는 여전히 user 타입의 값을 가지겠지만, 그 값은 제로 값으로 설정된다.

u1, found1 := users2["Roy"]
u2, found2 := users2["Ford"]

값과 키의 존재 여부를 나타낸다.

fmt.Printf("\n=> Find key\n")
fmt.Println("Roy", found1, u1)
fmt.Println("Ford", found2, u2)
=> Find key
Roy false { }
Ford true {Henry Ford}

맵의 키 제한(Map key restrictions)

type users []user

이 구문을 사용하여 users 를 새로 정의할 수 있으며, 이는 users를 정의하는 두 번째 방법이다. 이처럼 이미 존재하는 타입을 통해, 다른 타입의 타입으로 사용할 수 있다. 이 때 두 타입은 서로 연관성이 없다. 하지만 다음의 코드 u := make(map[users]int)와 같이 키로서 사용코자 할 때, 컴파일러는 다음의 오류를 발생시킨다. "맵의 키로써 users 타입은 유효하지 않다."

그 이유는, 키로 어떤 것을 사용하던지 그 값은 반드시 비교가능해야 하기 때문이다. 맵이 키의 해시 값을 만들 수 있는 지 보여주는 일종의 불리언 표현식을 사용해야한다.