프로젝트를 하면 할수록, 내가 명확하게 무엇을 아는가? 에 대해서 고민하게 되었고. Ultimate Go라는 글을 읽어보고자 한다.
사실 Go Slack에서 물어보니 이거랑, Go in Action을 추천해 주더라 먼저 Ultimate Go를 읽어보자.
문법
- 지금 와서 생각해 보면 바이트, 비트의 개념이 생각보다 많이 없지 않았나 싶다.
1바이트 => 8비트이다. 그렇다면 +- 부호비트를 제외하고 7자리 최댓값은 2의 7승 127까지 표현된다. 특히나 고에서는 이러한 표현에 있어 그냥 넘어가는 게 아닌 기민하게 받아들여야 한다.
생성되는 모든 변수는 초기화되어야 한다. 어떤 값으로 초기화할지 명시하지 않는다면, 제로값으로 초기화된다. 할당된 메모리의 모든 비트는 0으로 리셋된다.
- 사실 저 부분 4번 읽어봤다. 이해한 바에 따르면 언어의 변수를 생성하고 값을 명시하지 않는다. 예를 들어
var a int64라고 가정한다면 이는 8바이트짜리 메모리 공간을 할당한다.
즉 메모리의 시작주소와 크기가 정해져 있지만 값을 할당하지 않아 8바이트 의 모든 비트는 00000000..... 0000으로 표기된다는 의미이다.
문자열은 uint8 타입의 연속이다.
- 문자열 가지고 for 문 돌려보면 rune 타입으로 반한 된다, 여기서 rune 은 int32의 alias 별칭타입이다 그냥 숫자 덩어리이다.
그러면 의문이 생긴다. uint8과 rune 은 엄연히 다른 타입 즉 메모리 사이즈 가 다르다. 왜 다를까?
1. 문자열 : uint8 타입의 연속이고 이는 UTF8 인코딩 된 문자를 나타낸다.
2. rune : unit32의 별칭이고, 주로 Unicode 코드포인트를 표현하는 데 사용된다.
다시 말하자면 utf8 인코딩 은 다양한 길이의 바이트 시퀀스를 이용해 유니코드 문자를 나타낸다.
어떤 문자는 1바이트, 어떤 문자는 2,3,4 바이트로 나타내야 할 수도 있다. 그렇기 때문에 for 문을 이용해서 range 처리를 하게 되면
uint8로 표현을 하게 되면 올바르게 utf-8 을표현할 수가 없다. 따라서 rune을 이용해 반환하게 된다.
유니코드 : 세계의 모든 문자를 컴퓨터에서 일관되게 표현하고 다룰 수 있도록 설계된 산업표준으로 코드포인트가 존재한다.
예를 들어 ) ㅇ : U+3147 , 안
UTF-8 : 유니코드를 바이트 시퀀스로 인코딩하는 방식이다.
func Test_String_Rune(t *testing.T){
a := "안녕"
}
위와 같이 a라는 값이 할당되면 우리의 명석한 고 컴파일러 님은
1. 안과 녕 에 해당하는 유니코드 포인트로 해석을 하고
2. 해석된 값을 UTF-8로 인코딩을 하고 (한글은 3바이트) 총 6바이트 길이의 배열이 필요하고 이에 메모리 할당을 하고
3. 문자열 a는 해당된 문자열 바이트 의 주소 시작값을 가지고, 6이라는 사이즈의 길이를 가진다.
이렇게 되면 len(a) 문자열의 길이를 추출하게 될 때 이는 6이라는 숫자를 반환한다.
실제 "안녕"에 대해 추출하고 싶을 때 rune을 이용하게 된다.
다시 말해
a의 길이를 뽑으면 6이 될 것이고, rune 으로 변환해서 조회한다면 2라는 크기가 나온다.
for 루프를 이용해서 a를 조회하면 총 6번의 값을 조회해서 나타내주고, for... range를 이용해서 조회하면 유니코드 기준으로 조회되어 2번의 값을 조회해서 나타내준다.
for loop의 바이트 시퀀스 조회하는 것의 복잡성을 숨기고자 for... range는 문자열을 더 자연스럽게 순회가 가능하다.
func Test_Variable_String_Rune(t *testing.T) {
a := "안녕"
for i := range a {
fmt.Printf("%v ", a[i])
}
fmt.Println("------------------")
for i := 0; i < len(a); i++ {
fmt.Printf("%v ", a[i])
}
fmt.Println()
}
그렇다면 for loop 의 바이트를 문자열로 조회하고자 한다면?
func Test_Variable_String_Rune(t *testing.T) {
a := "안녕"
for i := 0; i < len(a); {
r, size := utf8.DecodeRuneInString(a[i:])
fmt.Printf("%d\t%c\n", i, r)
i += size
}
}
이렇게 조회를 하게 되면 "안", "녕"으로 조회가 가능하다. 해당 함수 utf8.DecodeRuneInString(s string)를 살펴보면
내부적으로 단순하게 주어진 s에 대해서 0~3까지 총 4개의 자리에 대해서 탐색을 한다. 즉 4바이트 탐색을 실시하고,
그거에 맞춰 rune과 크기 값을 반환한다. 각 바이트 별 검사하는 로직은 비트연산과 미리 정해놓은 상수값들을 이용해 비교하고 반환을 해주는데 생각보다 코드가 어지럽지만 해당 함수가 어떻게 검사하고 반환하는지에 대해 살펴보았다.
구조체 선언과 구조체 패딩
- 구조체에 할당된 메모리에는 메모리 패딩이라는 것이 존재한다.
func Test_Memeory_Address(t *testing.T) {
type ex struct {
counter int64
pi float32
flag bool
}
type ex2 struct {
flag bool
counter int64
pi float32
}
var e ex
var e2 ex2
fmt.Println(unsafe.Sizeof(e), unsafe.Sizeof(e2))
}
해당 테스트의 결괏값으로 e는 16 e2는 24의 메모리 사이즈를 가져간다 왜 동일한 구조체에 서로 다른 메모리 사이즈일까? 고 언어 에는 메모리패딩을 주어 cpu 가 각 경계별로 손쉽게 읽을 수 있도록 해주고 있다.
ex1의 경우 8바이트(counter) + 4바이트(pi) + 패딩 3바이트 + 1바이트(flag) => 이렇게 총 16바이트가 된다.
ex2의 경우 1바이트(flag) + 7바이트(패딩) + 8바이트(counter) + 4바이트(pi)+4바이트(구조체패딩) => 이렇게 24바이트가 된다.
구조체 패딩은 구조체 내에 가장 큰 바이트 기준의 배수로 구조체가 정렬되어야 하기 때문에 4바이트의 추가 패딩이 붙는다.
구조체의 패딩을 제외한다면 실제 구조체의 패딩에 각각 3,7 바이트의 차이가 존재한다.
구조체를 뭐 어마무시하게 많은 필드를 넣어서 생성하지는 않겠지만 이러한 규칙이 있어 메모리의 효율적인 사용을 위한다면 가장 큰 메모리를 앞단에 위치시켜야 한다는 사시을 알아야 한다.
포인터 항상 값을 전달한다
- 함수는 함수자체적으로 스택프레임을 가진다. 즉 함수 내에서 사용되고자 하는 값들에 대해 스택프레임에 위치해 해당 함수를 호출하면 모두 제로값으로 초기화되고 함수가 종료되면 스택 은알아서 정리가 된다. 이에 따라 다른 함수에서 해당 값에 접근을 할 수가 없다.
이에 값의 공유 고 루틴(고 루틴 또한 일반적으로 2K 스택메모리를 가지게 된다.) 이러한 고 루틴의 스택들 간에 값을 공유하기 위해서는?
포인터라는 값을 공유해야 한다.
func stayOnStack() user {
u := user{
name: "Ho",
email: "email",
}
return u
}
func escapeToHeap() *user {
u := user{
name: "Ho",
email: "email",
}
return &u
}
해당 함수들을 보면 stayOnStack의 경우 해당 값을 반환함과 동시에 함수 내의 u는 스택에 적재되어 있다가 한 번에 같이 사라지게 된다.
반면 esacpeToHeap을 보면 u의 주소값은 함수 밖을 나와 메모리의 값이 반환되어 스택이 아닌 힙에 적재된다.
이를 go에서는 이스케이프 분석이라고 하며, 변수의 생명주기를 컴파일러가 스택에 넣을지 힙에넣을지 여부를 판단하여 할당하는 것을 의미한다.
func Test_Pointer_Address(t *testing.T) {
fmt.Println(stayOnStack())
fmt.Println(escapeToHeap())
// go test -gcflags '-m -l' advance/variable_test.go
}
go test -gcflags=' -m -l' variable_test.go
위에 테스트를 제시된 코드로 실행하게 되면 아래와 같은 결과를 받을 수 있다.
./variable_test.go:61:25: stayOnStack() escapes to heap
./variable_test.go:62:13:... argument does not escape
음? stayOnStack() 은 heap으로 빠지면 안 된다 왜 빠진 거고, 포인터를 반환하는 escapeToHeap 은 왜 이스케이프 되지 않았는가?
fmt.Println()의 함수 인자값으로 넘길 때 해당 인자를 힙으로 이스케이프 되었고, escapeToHeap의 경우는 이미 힙으로 이스케이프 되어있기 때문에 할 필요가 없어 위와 같은 메시지를 받게 되는 것이다.
생각보다 모르는 부분이 많았고, 정말 언어 개발자들은 천재이지 아닌가 싶다.
'Go > Go Basic' 카테고리의 다른 글
Ultimate-Go-03 (2) | 2023.09.07 |
---|---|
Ulitmate-Go-02 (0) | 2023.09.05 |
Go Interface, embedded (0) | 2023.02.28 |
Effective Go 04 (0) | 2023.02.12 |
Effective Go 03 (0) | 2023.02.10 |