Day 7 with Go-Lang
Go defer 와 Panic
- 지연실행 defer
Go 언어의 defer
키워드는 특정 문장이나 함수를 defer 를 호출한 함수가 Return 하기 직전에 실행하게 한다. 일반적으로 defer 는 함수 마지막에 Clean-up 작업을 위해 사용된다. 아래 예제는 파일을 Open 한 후, 파일을 Close 하는 작업을 defer 로 쓰고 있다. 이를 통해 차후 문장에서 어떤 에러가 발생했더라도 항상 파일을 Close 할 수 있도록 한다.
package main
import "os"
func main() {
f, err := os.Open("1.txt")
defer f.Close()
if err != nil {
panic(err)
}
bytes := make([]byte, 1024)
f.Read(bytes)
println(len(bytes))
}
- panic 함수
Go 내장함수인 panic()
함수는 현재 함수를 즉시 멈추고, 현재 함수의 defer 함수들을 모두 실행한 후 즉시 Return 한다. 이러한 panic 모드 실행 방식은 상위함수에도 똑같이 적용되고, 콜스택을 타고 올라가며 계속 적용된다. 마지막에는 프로그램이 에러를 내고 종료하게 된다.
package main
import "os"
func main() {
openFile("Invalid.txt") // 잘못된 파일명
// openFile() 안에서 panic 이 실행되면
// 아래의 println 문장은 실행되지 않음
println("Done")
}
func openFile(fn string) {
f, err := os.Open(fn)
defer f.Close()
if err != nil {
panic(err)
}
}
- Recover 함수
Go 내장함수인 recover()
함수는 panic 함수에 의한 패닉 상태를 다시 정상 상태로 되돌리는 함수이다. 위의 panic 예제에서는 main 함수에서 println()
이 호출되지 못하고 프로그램이 종료되지만, recover 함수를 사용하면 panic 상태를 제거하고 openFile() 의 다음 문장인 println() 을 호출하게 된다.
package main
import (
"fmt"
"os"
)
func main() {
// 잘못된 파일명
openFile("Invalid.txt")
// recover 에 의해 다음 문장이 실행됨
println("Done")
}
func openFile(fn string) {
// defer 함수, panic 호출시 실행됨
defer func() {
if r := recover(); r != nil {
fmt.Println("OPEN ERROR", r)
}
}()
f, err := os.Open(fn)
defer f.Close()
if err != nil {
panic(err)
}
}
Go Routine
Go 루틴은 Go 런타임이 관리하는 Lightweight 논리적 (혹은 가상의) 쓰레드(주1)이다. Go 에서 “go
” 키워드를 사용해 함수를 호출하면, 런타임시 새로운 goroutine 을 실행한다. goroutine 은 비동기적으로 함수루틴을 실행하므로, 여러 코드를 동시에 실행하는데 사용된다.
(주1)goroutine 은 OS 쓰레드보다 훨씬 가볍게 비동기 Concurrent 처리를 구현하기 위해 만든 것으로, 기본적으로 Go 런타임이 자체 관리한다. Go 런타임 상에서 관리되는 작업 단위인 여러 goroutine 들은 종종 하나의 OS 쓰레드 1개로 실행되곤 한다. 즉, Go 루틴들은 OS 쓰레드와 1대 1로 대응되지 않고, Multiplexing 으로 훨씬 적은 OS 쓰레드를 사용한다. 메모리 측면에서도 OS 쓰레드가 1 MB 의 스택을 갖는 반면, goroutine 은 이보다 훨씬 적은 몇 KB 의 스택을 갖는다(필요시 동적으로 증가). Go 런타임은 Go 루틴을 관리하면서 Go 채널을 통해 Go 루틴 간의 통신을 쉽게 할 수 있도록 했다.
다음 예제에서 main 함수를 보면, 먼저 sync()
함수를 동기적으로 호출하고, 다음으로 동일한 say()
함수를 비동기적으로 3번 호출한다. 첫 번째 동기적 호출은 say()
함수가 완전히 끝났을 때 다음 문장으로 이동하고, 다음 3개의 go say()
비동기 호출은 별도의 Go 루틴들에서 동작하면서, 메인 루틴은 계속 다음 문장을 실행한다. 이 때 goroutine 은 실행순서가 일정하지 않으므로 프로그램 실행시마다 다른 출력 결과를 나타낼 수 있다.
package main
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i<10; i++ {
fmt.Println(s, "***", i)
time.Sleep(time.Millisecond * 50)
}
}
func main() {
say("Sync")
go say("Async1")
go say("Async2")
go say("Async3")
time.Sleep(time.Second * 3)
}
- 익명함수 Go 루틴
Go 루틴은 익명함수에 대해 사용할 수 있다.
아래 예제에서 첫 번째 익명함수는 Hello 문자열을 출력하는데, 이를 goroutine 으로 실행하면 비동기적으로 익명함수를 실행하게 된다. 두 번째 익명함수는 Parameter 를 전달하는 예제로 익명함수에 Parameter 가 있는 경우, go 익명함수 뒤에 Parameter 를 함께 전달하게 된다.
package main
import (
"fmt"
"sync"
)
func main() {
var wait sync.WaitGroup
wait.Add(2)
go func() {
defer wait.Done()
fmt.Println("Hello")
}()
go func(msg string) {
defer wait.Done()
fmt.Println(msg)
}("Hi")
go func() {
defer wait.Done()
fmt.Println("test")
}()
wait.Wait()
}
여기서 sync.WaitGroup
을 사용하고 있는데, 이는 여러 Go 루틴들이 끝날 때까지 기다리는 역할을 한다. Waitgroup 을 사용하기 위해 먼저 Add() 메소드에 몇 개의 Go 루틴을 기다릴 것인지 지정하고, 각 Go 루틴에서 Done() 메소드를 호출한다. 메인루틴에서는 Wait() 메소드를 호출해, Go 루틴들이 끝나기를 기다린다.
- 다중 CPU 처리
Go 는 Default 로 1개의 CPU 를 사용한다. 즉, 여러 개의 Go 루틴을 만들더라도 1개의 CPU 에서 작업을 시분할(Concurrent)하여 처리한다. 만약 머신이 복수 개의 CPU 를 가진 경우, Go 프로그램을 다중 CPU 에서 병렬처리(Parallel)하게 할 수 있는데, 병렬처리를 위해서는 아래와 같이 runtime.GOMAXPROCS(CPU수)
함수를 호출해야 한다. 이 때 CPU 수는 Logical CPU 수를 의미한다.
package main
import (
"runtime"
"sync"
)
func main() {
var wait sync.WaitGroup
wait.Add(10)
runtime.GOMAXPROCS(4)
for i := 1; i<=10; i++ {
go func(n int) {
defer wait.Done()
println("Start Func", n)
for i := 0; i < 50; i++ {
println(n, i)
}
println("End Func", n)
}(i)
}
wait.Wait()
}
Concurrency 와 Parallelism 은 비슷하지만, 전혀 다른 개념이다. 아래는 이 둘을 구분하기 위한 글이다.
In Programming, concurrency is the composition of independently executing processes, while parallelism is the simultaneous execution of computations. Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once. |
Reference : 예제로 배우는 Go 프로그래밍