Go 컴파일러
Go 컴파일러는 go 커맨드로 실행한다. 이 커맨드는 단순히 실행 파일을 생성하는데 그치지 않고 상당히 많은 일을 한다
Go 언어로 작성된 소스 파일을 컴파일하려면 go tool compile 커맨드를 실행한다. 그러면 object file이 생성되며, 확장자는 .o다.
오브젝트 파일은 일종의 바이너리 파일로서 오브젝트 코드를 담고 있다. 이는 재배치 할 수 있는 포맷으로 된 기게어로서, 대부분의 경우 이 상태로 직접 실행하지 않는다. 이렇게 재배치 가능한 포맷으로 생성함으로써 얻을 수 있는 가장 큰 장점은, 링크 단계에서 차지하는 메모리 공간을 최소화할 수 있다는 점이다.
go tool compile을 실행할 때 -pack 플래그를 지정하면 오브젝트 파일이 아닌 아카이브 파일이 생성된다
아카이브 파일이란 일종의 바이너리 파일로서 여러 파일을 하나의 파일로 묶을 때 사용한다. Go 언어에서 사용하는 아카이브 포맷은 ar이며, 확장자는 .a다
go tool compile 커맨드에서 -race라는 플래그가 있다. 이렇게 하면 경쟁 상태를 감지할 수 있다.
가비지 콜렉션
가비지 콜렉션이란 더 이상 사용하지 않는 메모리 공간을 해제하는 프로세스이다. 다시 말해 가비지 컬렉터는 현재 스코프를 벗어난 오브젝트를 발견하고, 이를 더 이상 참조할 일이 없다고 판단하면 그 공간을 해제한다. 이 프로세스는 Go 프로그램이 시작하기 전이나 끝난 후가 아닌, 실행되는 도중에 동시에 진행된다.
Go 공식 문서에서는
- GC는 mutator thread와 함께 동시에 실행되며, 타입을 엄격히 따지고 여러 GC thread를 병렬로 실행할 수 있다. 쓰기 장벽을 사용하는 mark and sweep 알고리즘의 동시성 버전으로, non-generational, non-compacting 방식이다. 할당 작업은 lock이 일어나지 않으며 fragmentation을 최소화하도록 P할당 영역마다 분리한 크기로 처리한다
용어가 참 많아 너무 난해하다. 다행히 표준 라이브러리에서 제공하는 몇 가지 함수를 이용하면 GC의 동작을 살펴보고, 어떤 작업을 눈에 띄지 않게 수행하는지 파악할 수 있다.
package main
import (
"fmt"
"runtime"
)
func printStatus(mem runtime.MemStats) {
runtime.ReadMemStats(&mem)
fmt.Println("mem.Alloc : ", mem.Alloc)
fmt.Println("mem.TotalAlloc : ", mem.TotalAlloc)
fmt.Println("mem.HeapAlloc : " , mem.HeapAlloc)
fmt.Println("mem.NumGC : ", mem.NumGC)
fmt.Println("===========")
}
여기서 주목할 점은 가바지 컬렉션 통계에 대한 최신 정보를 조회하려면, 매번 runtime.ReadMemstatus() 함수를 호출해야한다.
func main() {
var mem runtime.MemStats
printStatus(mem)
for i := 0; i < 10; i++ {
s := make([]byte, 50000000)
if s == nil {
fmt.Println("Operation failed")
}
}
printStatus(mem)
for 문을 통해 여러 개의 거대한 Go 슬라이스를 생성한다. 이를 통해 방대한 양의 메모리를 할당했다가 가비지 컬렉터를 호출 할 것이다.
for i := 0; i < 10; i++ {
s := make([]byte, 100000000)
if s == nil {
fmt.Println("Operation failed")
}
time.Sleep(5 * time.Second)
}
printStatus(mem)
}
더 많은 메모리를 할당한다. 결과는 아래와 같다
➜ goblog go run foo.go
mem.Alloc : 83608
mem.TotalAlloc : 83608
mem.HeapAlloc : 83608
mem.NumGC : 0
===========
mem.Alloc : 74000
mem.TotalAlloc : 500128048
mem.HeapAlloc : 74000
mem.NumGC : 10
===========
mem.Alloc : 74104
mem.TotalAlloc : 1500208344
mem.HeapAlloc : 74104
mem.NumGC : 20
===========
좀 더 상세하게 출력을 위해
➜ goblog GODEBUG=gctrace=1 go run foo.go
gc 1 @0.013s 1%: 0.019+0.33+0.026 ms clock, 0.077+0.50/0.19/0+0.10 ms cpu, 4->4->0 MB, 5 MB goal, 4 P
gc 2 @0.028s 1%: 0.041+0.40+0.002 ms clock, 0.16+0.19/0.17/0.35+0.011 ms cpu, 4->4->0 MB, 5 MB goal, 4 P
gc 3 @0.053s 0%: 0.039+0.36+0.043 ms clock, 0.15+0.25/0.10/0.36+0.17 ms cpu, 4->4->0 MB, 5 MB goal, 4 P
gc 4 @0.081s 0%: 0.039+0.48+0.013 ms clock, 0.15+0.22/0.22/0.54+0.054 ms cpu, 4->4->0 MB, 5 MB goal, 4 P
gc 5 @0.103s 0%: 0.059+0.64+0.003 ms clock, 0.23+0.20/0.42/0.34+0.015 ms cpu, 4->4->0 MB, 5 MB goal, 4 P
gc 6 @0.109s 0%: 0.032+0.60+0.005 ms clock, 0.12+0.23/0.29/0.47+0.021 ms cpu, 4->4->0 MB, 5 MB goal, 4 P
gc 7 @0.115s 1%: 0.032+0.69+0.022 ms clock, 0.13+0.31/0.55/0+0.089 ms cpu, 4->4->0 MB, 5 MB goal, 4 P
gc 8 @0.130s 1%: 0.042+0.73+0.71 ms clock, 0.17+0.12/0.45/0.93+2.8 ms cpu, 4->4->0 MB, 5 MB goal, 4 P
# command-line-arguments
gc 1 @0.001s 14%: 0.009+2.6+0.019 ms clock, 0.038+0.13/2.3/1.2+0.078 ms cpu, 4->7->6 MB, 5 MB goal, 4 P
gc 2 @0.012s 10%: 0.002+4.0+0.003 ms clock, 0.011+0.46/3.7/1.3+0.012 ms cpu, 10->14->14 MB, 12 MB goal, 4 P
gc 3 @0.046s 5%: 0.002+6.3+0.029 ms clock, 0.010+0.12/4.8/9.5+0.11 ms cpu, 22->23->21 MB, 28 MB goal, 4 P
mem.Alloc : 84200
mem.TotalAlloc : 84200
mem.HeapAlloc : 84200
mem.NumGC : 0
===========
gc 1 @0.001s 2%: 0.004+0.16+0.002 ms clock, 0.019+0.12/0.015/0.17+0.010 ms cpu, 47->47->0 MB, 48 MB goal, 4 P
gc 2 @0.029s 0%: 0.002+0.13+0.003 ms clock, 0.009+0.10/0.049/0.033+0.012 ms cpu, 47->47->0 MB, 48 MB goal, 4 P
gc 3 @0.032s 0%: 0.036+0.12+0.001 ms clock, 0.14+0.11/0.040/0.006+0.007 ms cpu, 47->47->0 MB, 48 MB goal, 4 P
gc 4 @0.035s 0%: 0.019+0.11+0.002 ms clock, 0.076+0.093/0.059/0.035+0.009 ms cpu, 47->47->0 MB, 48 MB goal, 4 P
gc 5 @0.037s 0%: 0.032+0.11+0.002 ms clock, 0.13+0.081/0.042/0.065+0.011 ms cpu, 47->47->0 MB, 48 MB goal, 4 P
gc 6 @0.040s 1%: 0.40+0.13+0.023 ms clock, 1.6+0.10/0.008/0.14+0.092 ms cpu, 47->47->0 MB, 48 MB goal, 4 P
gc 7 @0.043s 1%: 0.041+0.15+0.002 ms clock, 0.16+0.10/0.016/0.081+0.009 ms cpu, 47->47->0 MB, 48 MB goal, 4 P
gc 8 @0.046s 1%: 0.035+0.15+0.002 ms clock, 0.14+0.10/0.030/0.058+0.008 ms cpu, 47->47->0 MB, 48 MB goal, 4 P
gc 9 @0.048s 1%: 0.017+0.12+0.002 ms clock, 0.069+0.084/0.010/0.075+0.009 ms cpu, 47->47->0 MB, 48 MB goal, 4 P
gc 10 @0.050s 1%: 0.018+0.12+0.002 ms clock, 0.073+0/0.10/0.084+0.009 ms cpu, 47->47->0 MB, 48 MB goal, 4 P
mem.Alloc : 73864
mem.TotalAlloc : 500128328
mem.HeapAlloc : 73864
mem.NumGC : 10
===========
gc 11 @0.082s 1%: 0.071+0.12+0.002 ms clock, 0.28+0.084/0.035/0.091+0.010 ms cpu, 95->95->0 MB, 96 MB goal, 4 P
gc 12 @5.093s 0%: 0.021+0.13+0.003 ms clock, 0.084+0/0.11/0.095+0.012 ms cpu, 95->95->0 MB, 96 MB goal, 4 P
gc 13 @10.101s 0%: 0.035+0.17+0.015 ms clock, 0.14+0/0.15/0.066+0.063 ms cpu, 95->95->0 MB, 96 MB goal, 4 P
gc 14 @15.112s 0%: 0.020+0.17+0.002 ms clock, 0.081+0/0.11/0.098+0.010 ms cpu, 95->95->0 MB, 96 MB goal, 4 P
gc 15 @20.123s 0%: 0.019+0.15+0.002 ms clock, 0.079+0/0.12/0.11+0.010 ms cpu, 95->95->0 MB, 96 MB goal, 4 P
gc 16 @25.128s 0%: 0.037+0.21+0.002 ms clock, 0.15+0/0.16/0.11+0.011 ms cpu, 95->95->0 MB, 96 MB goal, 4 P
gc 17 @30.137s 0%: 0.037+0.19+0.002 ms clock, 0.14+0/0.14/0.080+0.010 ms cpu, 95->95->0 MB, 96 MB goal, 4 P
gc 18 @35.145s 0%: 0.023+0.13+0.002 ms clock, 0.093+0/0.10/0.053+0.011 ms cpu, 95->95->0 MB, 96 MB goal, 4 P
gc 19 @40.154s 0%: 0.038+0.19+0.003 ms clock, 0.15+0/0.13/0.092+0.013 ms cpu, 95->95->0 MB, 96 MB goal, 4 P
gc 20 @45.159s 0%: 0.020+0.11+0.002 ms clock, 0.081+0/0.099/0.068+0.011 ms cpu, 95->95->0 MB, 96 MB goal, 4 P
mem.Alloc : 74056
mem.TotalAlloc : 1500208696
mem.HeapAlloc : 74056
mem.NumGC : 20
===========
힙 크기를 자세히 알 수 있는데 예를 들어 47→47→0 MB에서
첫 째 숫자는 가비지 컬렉터가 실행될 시점, 두번 째 숫자는 GC가 마칠 때, 세번 째는 현재 힙 사이즈이다.
삼색 알고리즘
Go 언어의 GC는 tricolor algorithm에 의해 작동한다.
정확히 말하자면, Go 언어에서는 tricolor mark-and-sweep algorithm이다. 이 알고리즘은 프로그램과 동시에 작동하며 write barrier를 활요한다. 따라서, Go 프로그램이 실행될 때, Go 스케줄러는 마치 여러 개의 goroutine으로 구성된 일반 애플리케이션을 다루듯이, 애플리케이션과 가바지 컬렉터에 대한 스케줄링을 담당한다.
이 알고리의 핵심 원리는 힙에 있는 오브젝트를 이 알고리즘에 따라 세 가지 색깔로 지정된 집합으로 나눈다.
흰색 집합에 있는 오브젝트는 프로그램에서 더 이상 접근할 수 없어서 GC 대상이 된다. 검은색 집합에 속한 오브젝트는 프로그램이 사용하고 있으며, 흰색 집합의 오브젝트를 가리키는 포인터가 확실히 없는 것이다. 하지만 흰색 오브젝트에 있는 오브젝트가 검은색 집합의 오브젝트를 가리키는 포인터를 가질 수 있다. 회색 집합에 있는 오브젝트는 프로그램이 현재 사용하고 있지만 흰색 오브젝트를 가리킬 수도 있어 검사 과정을 거쳐야 할 것들이다.
검은색 집합에서 곧바로 흰색 집합으로 갈 수 없다. 그래서 GC가 흰색 집합에 있는 오브젝트를 확인한 후 제거하는 작업을 수행할 수 있다. 또한 검은색 집합에 있는 오브젝트는 흰색 집합의 오브젝트를 직접 가리킬 수 없다.
GC는 모든 오브젝트가 흰색인 상태로 시작하며, 모든 루트 오브젝트를 방문해서 회색으로 칠한다. 여기서 루트 오브젝트는 스택에 있는 오브젝트나 전역 변수처럼, 애플리케이션에서 직접 접근할 수 있는 오브젝트를 말한다. GC는 회색 오브젝트들을 검은색으로 바꾸고, 그 오브젝트에 흰색 집합에 있는 오브젝트를 가르키는 포인터가 있는지 확인한다. 다시 말해, 회색 오브젝트가 가리키는 포인터를 검사할 때, 회색 오브젝트를 검은색으로 칠한다. 이때 흰색 오브젝트를 가리키는 포인터가 한 개 이상 발견되면, 포인터가 가리키는 흰색 오브젝트를 회색 집합에 넣는다. 이 작업을 계속 반복하며 끝나면 더 이상 접근할 수 없는 오브젝트만 흰색 집합에 남게 되며, 여기에 할당된 메모리 공간은 재사용 가능한 상태가 된다.
- GC 작업을 주기적으로 반복하면서 수행하다가 어느 주기에 회색 집합의 오브젝트에 접근할 수 없게 되면 현재 주기가 아닌 다음 주기에 수거한다.
이 작업이 진행되는 동안 mutator라 불리는 애플리케이션이 실행된다. 이 mutator는 힙에 있는 포인터가 변경될 때마다 쓰기 장벽이라 부르는 조그만 함수를 실행한다. 힙에 있는 오브젝트의 포인터가 변경되면, 다시 말해 이 오브젝트에 접근할 수 있게 되었다면, 쓰기 장벽은 이를 회색으로 칠해 회색 집합에 넣는다.
- 뮤테이터는 검은색 집합의 원소 중 어느 것도 흰색 집합 원소를 가리키지 않는다는 불변 속성을 유지하는 역학을 한다. 이 과정에서 쓰기 장벽 함수의 도움을 받는다.
그림처럼 집합들은 검은색, 흰색, 회색으로 구성이된다. 시작할 때 먼저 오브젝트를 모두 흰색으로 칠한다. 그러다가 알고리즘을 진행하면서 흰색 오브젝트를 검은색과 회색 집합으로 옮긴다. 흰색 집합에 남겨진 오브젝트는 제거 대상이 된다.
앞에 나온 그래프를 보면 흰색 집합에 있는 E 오브젝트가 F 오브젝트에 접근 할 수 있지만, 다른 오브젝트들이 E에 접근하지 못하는 것을 확인할 수 있다. 따라서 이 오브젝트는 GC 대상이 되기에 딱 알맞다. 또한 A,B,C는 루트 오브젝트(초록)이라 항상 접근하기 때문에 GC 대상이 될 수 없다.
그렇다면 다음 단계에서 이 그래프가 어떻게 변할까? 일단 회색 집합에 남아 있는 원소를 처리한다. 다시 말해 A와 F 오브젝트가 검은색 집합으로 이동한다. A가 검은색으로 가는 이유는 루트 원소이기 때문이다. F가 검은색으로 가는 이유는 회색 집합에 있는 동안 다른 오브젝트를 가리키지 않기 때문이다. A 오브젝트가 수거된 후, F 오브젝트에 더 이상 접근할 수 없게 되면, F 오브젝트도 다음 주기에 수거된다.
Go 언어의 GC는 채널 변수에도 적용이된다. 도달할 수 없으면서 더 이상 접근하지도 않는 채널을 GC가 발견하면, 그 채널을 아직 닫지 않았더라도 채널에게 할당된 리소스를 해제한다.
Go에서는 수동으로 GC를 실행시킬 수 있도록 runtiem.GC()함수를 제공한다. 이 함수를 실행하면 작업이 완전히 끝날 때까지 코드의 실행이 멈춘다. 이렇게 멈추는 이유는, 모든 것이 굉장히 빠르게 변하고 있는 동안에는 GC작업을 수행할 수 없기 때문이다. 이 일시적으로 멈춘 상태를 GC Safe-Point라고 부른다.
Go 언어 GC의 구체적인 작동 방식
Go언어의 GC 과정에서 가장 신경 쓰는 부분은 지연 시간을 낮추는 것이다. GC 작동 과정에서 일시적으로 멈추는 시간을 최소화해서 연산을 실시간으로 처리하게 하는 것이다. 또 다른 관점에서 보면 하나의 프로그램은 새로운 객체를 생성하고 포인터로 다른 오브젝트를 조작하는 작업을 끊임없이 수행한다. 그러다 보면 더 이상 접근할 수 없는 오브젝트가 생기기 마련이다. 이러한 오브젝트는 GC가 수거해서 여기에 할당된 메모리 공간을 해제할 대상이 된다.
GC에서는 전통적으로 mark and sweep 알고리즘을 사용했다. 먼저 프로그램 실행을 잠시 멈추고, 프로그램의 힙에서 접근할 수 있는 오브젝트를 모두 방문한 뒤에 적절히 표시한다. 그런 다음 접근할 수 없는 오브젝트를 쓸어 담느다. 이 알고리즘에서 표시 단계를 수행하는 동안, 모든 오브젝트가 흰색, 회색, 검은색 중 하나로 표시된다. 회색 오브젝트의 자식도 회색으로 칠하지만, 원래 회색이던 오브젝트는 검은색으로 바꾼다. 그러다가 더 이상 검토할 회색 오브젝트가 업으면 쓸어 담기 단계이다. 이 알고리즘의 불변 속성인 검은색 집합에서 흰색 집합을 가리키는 포인터는 없어야한다.
Mark and sweep 알고리즘은 간단하지만, 프로그램의 실행 시간을 잠시 멈처야 한다. Go언어에서는 GC를 동시성 프로세스로 실행하고 삼색 알고리즘을 적용함으로 지연시간을 최소하려고 한다. 하지만 이렇게 GC가 동시에 실행되는 동안에 얼마든지 다른 프로세스에 의해 포인터가 변경 되거나 새로운 오브젝트가 생성될 수 있다. 결론적으로 삼색 알고리즘을 동시에 실행하는데 가장 중요한 점은, Mark and sweep의 불변 속성을 유지하는 것이다.
이러한 문제를 해결하기 위한 한 가지 방법은 문제 발생 요인을 모두 제거하는 것이다. 따라서 새로운 오브젝트는 반드시 회색 집합으로 가야한ㄷ. 그래야 표시 후 쓸어 담기 알고리즘의 불변 속성이 바뀌지 않는다. 또한 포인터가 이동하면 포인터가 가리키던 오브젝트도 회색으로 변경된다. 회색 집합이 흰색 집합과 검은색 집합 사이의 장벽 역할을 하는 것이다. 마지막으로 포인터가 변경될 때마다 쓰기 장벽이라는 Go 코드가 자동으로 실행되면서 색깔 변경을 진행한다.
쓰기 장벽 코드를 실행함으로써 발생하는 지연 시간은 GC를 동시에 실행하기 위해 치러야 할 대가인셈이다.
언세이프 코드
언세이프 코드란 Go 언어의 타입 안정성 및 메모리 보안 검사를 거치지 않은 코드를 말한다. 언세이프 코드라고 하면 대부분 포인터에 관한 코드를 의미한다. 따라서 꼭 필요한 경우에만 언세이프 코드를 사용한다.
package main
import (
"fmt"
"unsafe"
)
func main() {
var value int64 = 5
var p1 = &value
var p2 = (*int32)(unsafe.Pointer(p1))
fmt.Println("*p1 : ", *p1)
fmt.Println("*p2 : ", *p2)
*p1 = 543123451234532142
fmt.Println(value)
fmt.Println("*p2 : ", *p2)
*p1 = 543123414
fmt.Println(value)
fmt.Println("*p2 : ", *p2)
}
unsafe.Pointer() 함수를 통해 int32 타입 포인터를 생성할 수 있다. 이 포인터를 p2로, value라는 이름의 int64 타입의 변수를 가르킨다. Go 에서는 모든 포인터는 unsafe.Pointer로 변환할 수 있다.
- unsafe.Pointer 타입의 포인터는 Go 언어의 타입 시스템을 무시할 수 있다.
unsafe 패키지
이 패키지가 왜 좋은지 알아보자. 흥미로운 점은 unsafe 패키지에 대한 많은 코드들은 Go 컴파일러가 프로그램이 실행 될때 구현해준다.
package main
import (
"fmt"
"unsafe"
)
func main() {
array := [...]int{0, 1, -2, 3, 4}
pointer := &array[0]
fmt.Print(*pointer, " ")
memoryAddress := uintptr(unsafe.Pointer(pointer)) + unsafe.Sizeof(array[0])
for i := 0; i < len(array) - 1; i++ {
pointer = (*int) (unsafe.Pointer(memoryAddress))
fmt.Print(*pointer, " ")
memoryAddress = uintptr(unsafe.Pointer(pointer)) + unsafe.Sizeof(array[0])
}
fmt.Println()
pointer = (*int) (unsafe.Pointer(memoryAddress))
fmt.Print("One more : ", *pointer, " ")
memoryAddress = uintptr(unsafe.Pointer(pointer)) + unsafe.Sizeof(array[0])
}
pointer 변수는 정수형 배열의 첫 번째 원소인 array[0]에 대한 메모리 주소를 가리킨다. 그 다음 정수 값을 가리키던 pointer 변수를 unsafe.Pointer() 함수로 변환하고, 다시 uintptr로 변환한다. 이 결과를 memoryAddress로 저장한다.
unsafe.Sizeof(array[0])의 값을 더하는 방식으로 배열의 다음 원소를 구할 수 있는데 배열의 각 원소가 메모리에서 차지하는 공간은 모두 같기 때문이다. for 루프를 돌 때마다 memoryAddress 변수에 이 값을 추가한다. 그러면 배열의 바로 다음 원소의 메모리 주소를 구할 수 있다.
➜ go run foo.go
0 1 -2 3 4
One more : 824634355448
마지막 부분을 보면, 지정한 포인터와 주소에는 존재하지 않는 배열의 원소에 접근하고 있다. 이 코드에서 unsafe 패키지를 사용하고 있기 때문에 Go 컴파일러에서 이런 논리적 에러를 잡아주지 않는다. 따라서 이상한 값이 리턴된다.
Go에서 C호출하기
가장 간단한 방법은 Go 소스 파일에 C 코드를 집어 넣는 것이다.
package main
import "C"
import "fmt"
//#include <stdio.h>
//void callC() {
// printf("Calling C node!\n");
//}
func main() {
fmt.Println("A Go statement!")
C.callC()
fmt.Println("Another Go statement!")
}
다른 방법은 c코드파일을 불러오는 것이다.
- C 코드 : callC.h
#ifndef CALLC_H
#define CALLC_H
void cHello();
void printMessage(char* messagE);
#endif
- callC.c
#include <stdio.h>
#include "callC.h"
void cHello() {
printf("Hello from C!\n");
}
void printMessage(char* message) {
printf("Go send me %s\n", message);
}
이 파일들을 callClib이란 디렉토리에 저장
- Go 코드
package main
// #cgo CFLAGS: -I${SRCDIR}/callClib
// #cgo LDFLAGS: ${SRCDIR}/callC.a
// #include <stdlib.h>
// #include <callC.h>
import "C"
C는 가상 go 패키지로, go 컴파일러에서 본격적으로 컴파일하기 전에, 이렇게 작성한 입력 파일을 cgo 도구로 전처리하도록 go build에 지시하기만 한다.
import (
"fmt"
"unsafe"
)
func main() {
fmt.Println("A Go statement!")
C.cHello();
fmt.Println("Calling another C function")
myMessage := C.CString("This is Mihalis!")
defer C.free(unsafe.Pointer(myMessage))
C.printMessage(myMessage)
fmt.Println("finished")
}
C 코드에서 Go 함수 호출하기
- Go 패키지
package main
import "C"
import "fmt"
//export PrintMessage
func PrintMessage() {
fmt.Println("A go function")
}
//export Multiply
func Multiply(a, b int) int {
return a * b
}
func main() {
}
C 코드에서 호출할 go 함수는 반드시 export부터 해야 한다
$ go build -o usedByC.o -ubildmode=c-shared foo.go
- C 코드
#include <stdio.h>
#include "usedByC.h"
int main(int argc, char **argv) {
GoInt x = 12;
GoInt y = 23;
printf("About to call a go function!\n");
PrintMessage();
}
defer 키워드
어떤 함수를 호출하는 문장 앞에 defer 키워드를 붙이면, defer가 존재하는 함수가 리턴될 때까지 defer 뒤의 함수의 실행을 미룬다. 보통 파일 입력 및 연산을 수행할 대 흔히 사용하는데, 이렇게 하면 연 파일을 언제 닫을지 신경쓸 필요가 없기 때문이다
defer문을 담고 있는 함수가 리턴된 후에, defer 키워드를 이용하면 deferred function이 호출되는 순서는 LIFO 방식이다.
package main
import "fmt"
func d1() {
for i := 3; i > 0; i-- {
defer fmt.Print(i, " ")
}
}
func d2() {
for i := 3; i> 0; i-- {
defer func() {
fmt.Print(i, " ")
}()
}
fmt.Println()
}
func d3() {
for i := 3; i > 0; i-- {
defer func(n int) {
fmt.Print(n, " ")
}(i)
}
}
func main() {
d1()
d2()
fmt.Println()
d3()
fmt.Println()
}
➜ go run foo.go
1 2 3
0 0 0
1 2 3
f1이 1 2 3으로 출력된 이유는 defer가 lifo방식이기 때문이다.
f2의 결과 값이 0 0 0인 이유는 defer가 실행될 시점은 for문이 끝나고 i가 0이되기 때문이다.
panic & recover
panic() 함수는 Go 언어에서 기본으로 제공하는 것으로, 프로개름의 실행 흐름을 종료하고 패닉 상태에 빠트린다. recover()함수 역시 Go 언어에서 기본으로 제공하는 것이지만, panic()으로 인해 패닉 상태에 빠졌던 goroutine으로 부터 제어권을 다시 가져오게 한다.
package main
import "fmt"
func a() {
fmt.Println("Inside a()")
defer func() {
if c := recover(); c != nil {
fmt.Println("Recover inside a()!")
}
}()
fmt.Println("About to call b()")
b()
fmt.Println("b() exited")
fmt.Println("Exiting a()")
}
func b() {
fmt.Println("Inside b()")
panic("Panic in b()!")
fmt.Println("Exiting b()")
}
func main() {
a()
fmt.Println("main() ended")
}
➜ goblog go run foo.go
Inside a()
About to call b()
Inside b()
Recover inside a()!
main() ended
a()함수가 정상적으로 종료되지 않고, recover을 통해 다시 살아난 것을 확인할 수 있다. → 제어권을 다시 얻다
panic 함수만 사용하기
recover함수로 복구하지 않고, panic() 함수만으로 처리할 수도 있다.
package main
import (
"fmt"
"os"
)
func main() {
if len(os.Args) == 1 {
panic("Not enough arguments")
}
fmt.Println("Thanks~")
}
➜ go run foo.go
panic: Not enough arguments
goroutine 1 [running]:
main.main()
/Users/imac/Workspace/golang/src/github.com/callistaenterprise/goblog/foo.go:10 +0xa9
exit status 2
패닉 함수만 사용한다면 복구할 기회 없이 프로그램을 종료해버린다.
유용한 유닉스 유틸리티
유닉스 프로그램을 실행할 때, 간혹 알 수 없는 이유로 인해 죽거나 성능이 떨어지는 이유가 있다. 수많은 디버그용 문장을 추가하지 않고도 원인을 알아내려면 어떻게 해야 할까?
실행 파일에서 수행하는 C 시스템 콜을 들여다보는데 유용한 두 가지 커맨드 라인 유틸리티를 소개합니다. strace & dtrace입니다.
- 유닉스 머신에서 구동하는 모든 프로그램이 수행하는 작업의 대부분은 결국 유닉스 커널과 통신하는 C 시스템 콜을 이용한다는 점을 명심하자
strace
strace을 이용하면 시스템 콜과 시그널의 실행 흐름을 추적할 수 있다.
strace에서 출력한 결과를 보면 호출된 시스템 콜과 여기에 전달된 매개변수, 그리고 그 결과로 리턴되는 값을 모두 볼 수 있다. 이렇게 분석하고 싶은 실행 파일 앞에 strace 커맨드를 붙여서 실행한다.
dtrace
strace와 truss 같은 디버깅 유틸리티를 이용하면 프로세스에서 사용한 시스템콜의 실행 상태를 추적할 수는 있지만, 속도가 느린 편이다. DTrace라는 도구를 사용하면 분석 대상을 수정하거나 다시 컴파일할 필요 없이 내부에서 일어나는 일을 시스템에서 들여다 볼 수 있다.
이런식으로 go 코드에서 잠재적인 병목 지점을 금방 찾아낼 수 있다.
Go 환경 파악하기
Go 환경에 대한 정보를 runtime 패키지에서 제공하는 함수나 속성을 이용해 알어내보자. runtime 패키지는 시스템의 여러 가지 환경 정보에 대한 함수와 속성을 다양하게 제공한다.
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Print("You are using ", runtime.Compiler, " ")
fmt.Println("on a ", runtime.GOARCH, "machine")
fmt.Println("Using Go version", runtime.Version())
fmt.Println("Number of CPUs : ", runtime.NumCPU())
fmt.Println("Number of Goroutines : ", runtime.NumGoroutine())
}
$ go run foo.go
You are using gc on a amd64 machine
Using Go version go1.14.4
Number of CPUs : 4
Number of Goroutines : 1
package main
import (
"fmt"
"runtime"
"strconv"
"strings"
)
func main() {
myVersion := runtime.Version()
major := strings.Split(myVersion, ".")[0][2]
minor := strings.Split(myVersion, ".")[1]
m1, _ := strconv.Atoi(string(major))
m2, _ := strconv.Atoi(string(minor))
if m1 == 1 && m2 < 8 {
fmt.Println("Need Go version 1.8 or higher")
return
}
fmt.Println("You are using go version 1.8 or higher")
}
Go 어셈블러
Go 어셈블러란 Go언어에서 제공하는 도구 중 하나다. Go 어셈블러를 이용하면 Go 컴파일러에서 사용하는 어셈블리 언어를 볼 수 있다.
$ GOOS=darwin GOARCH=amd64 go tool compile -S foo.go
GOOS 변수 값으로 타겟 OS 이름을 지정하고, GOARCH 변수로 컴파일 아키텍처를 지정한다.
노드 트리
Go 노드는 수 많은 속성으로 구성된 하나의 struct이다. Go 프로그램에서 작성한 모든 내용은 Go 언어에서 정한 문법에 따라 컴파일러가 파싱하고 분석한다. 이러한 분석을 거쳐 최종적으로 나오는 결과는 Go 코드에 작성된 내용에 맞게 트리 형태로 나오며, 개발자보다는 컴파일러 입장에서 활용하기에 적합한 형태로 프로그램을 표현한 것이다.
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello There")
}
$ go tool compile -W foo.go
'Go 언어 공부' 카테고리의 다른 글
[GO 마스터하기] 05-Go 자료구조 (0) | 2020.09.16 |
---|---|
[GO 마스터하기] 04-합성 타입 사용법 (0) | 2020.09.07 |
[GO 마스터하기] 03-Go언어의 기본 데이터 타입 (0) | 2020.09.07 |
[GO 마스터하기] 01-Go 언어와 운영 체제 (0) | 2020.09.05 |
[GO 마스터하기] 00-Intro (0) | 2020.09.05 |