에러
Go는 내장 타입으로 error 라는 인터페이스(Interface) 타입을 갖는다. Go 에러는 이 error 인터페이스를 통해서 주고 받게 되는데, 이 인터페이스는 다음과 같은 하나의 메소드를 갖는다.
개발자는 이 인터페이스를 구현하는 커스텀 에러 타입을 만들 수 있다.
type error interface {
Error() string
}
에러 처리
함수가 결과와 에러를 함께 반환한다면, 이 에러가 nil 인지를 체크해서 에러가 없는 지를 확인할 수 있다.
아래의 예제에서 os.Open() 함수는 func Open(name string) (file *File, err error)와 같은 함수 원형을 갖는 것으로 첫 번째는 File 포인터를 두 번째는 error 인터페이스를 반환한다.
그래서 이 경우 두 번째 error를 체크해서 nil이 없으면 에러가 없는 것이고, nil이 아니면 err.Error()로 부터 해당 에러를 알 수 있다.
예제에서는 파일을 오픈 하는데 에러가 발생하면 에러 메세지를 출력하고 빠져나가는 예제이다.
log.Fatal()은 메세지를 출력하고 os.Exit(1)를 호출하여 프로그램을 종료한다.
package main
import (
"log"
"os"
)
func main() {
f, err := os.Open("c:\temp\1.txt")
if err != nil {
log.Fatal(err.Error())
}
println(f.Name())
}
또 다른 에러 처리로서 error의 Type을 체크해서 에러 타입별로 별도의 에러 처리를 하는 방식이 있다.
아래의 예제에서 otherFunc()를 호출한 후 error가 err로 반환 되었을 때, 이 err의 타입별로 다른 처리를 하는 것을 볼 수 있다.
default의 경우는 에러 타입이 nil인 경우로서 에러가 없는 경우고, 에러가 있으면 다음 case문에서
그 에러 타입이 MyError인지를 체크하고, 아니면 다음 case에서 일반 에러 케이스를 처리한다.
모든 에러는 error 인터페이스를 구현하므로 마지막 case문은 모든 에러에 적용된다.
_, err := otherFunc()
switch err.(type) {
default: // no error
println("ok")
case MyError:
log.Print("Log my error")
case error:
log.Fatal(err.Error())
}
지연 실행(Defer)
Go언어의 defer 키워드는 특정 문장 또는 함수를 나중에 (defer를 호출하는 함수가 반환하기 직전에) 실행하게 한다.
일반적으로 defer는 C#, Java와 같은 언어에서의 finally 블록처럼 마지막에 Clean-up 작업을 위해 사용된다.
아래의 예제는 파일을 Open 한 후 바로 파일을 Close하는 작업을 defer로 쓰고 있다.
이는 차후 문장에서 어떤 에러가 발생하더라도 항상 파일을 Close할 수 있도록 한다.
package main
import "os"
func main() {
f, err := os.Open("1.txt")
if err != nil {
panic(err)
}
// main 마지막에 파일 close 실행
defer f.Close()
// 파일 읽기
bytes := make([]byte, 1024)
f.Read(bytes)
println(len(bytes))
}
panic 함수
Go 내장 함수인 panic() 함수는 현재 함수를 즉시 멈추고 현재 함수에 defer 함수들을 모두 실행한 후 즉시 반환한다.
이러한 panic 모드 실행 방식은 다시 상위 함수에도 똑같이 적용되고, 계속 콜 스택(Call Stack)을 타고 올라가며 적용된다.그리고 마지막에는 프로그램이 에러를 내고 종료하게 된다.
package main
import "os"
func main() {
// 잘못된 파일 명을 넣음
openFile("Invalid.txt")
// openFile() 안에서 panic이 실행되면 아래 println 문장은 실행 안됨
println("Done")
}
func openFile(fn string) {
f, err := os.Open(fn)
if err != nil {
panic(err)
}
defer f.Close()
}
recover 함수
Go 내장 함수인 recover() 함수는 panic 함수에 의한 패닉 상태를 다시 정상 상태로 되돌리는 함수이다.
위의 panic 예제에서는 main 함수에서 println() 이 호출되지 못하고 프로그램이 crash 하지만
아래의 예제와 같이 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)
if err != nil {
panic(err)
}
defer f.Close()
}
루틴(Routine)
- Go 루틴은 Go 런타임이 관리하는 경량(Lightweight) 논리적 (또는 가상적) 쓰레드(Thread)이다.
- Go에서 'go' 키워드를 사용하여 함수를 호출하면, 런타임 시 새로운 goroutine을 실행한다.
- goroutine은 비동기적으로(Asynchronously) 함수 루틴을 실행하므로, 여러 코드를 동시에(Concurrently) 실행하는데 사용된다.
- goroutine은 OS 쓰레드 보드 훨씬 가볍게 비동기 Concurrent 처리를 구현하기 위하여 만든 것으로, 기본적으로 Go 런타임이 자체 관리한다.Go 런타임 상에서 관리되는 작업 단위인 여러 goroutine들은 종종 하나의 OS 쓰레드 1개로도 실행되곤 한다.즉, Go 루틴들은 OS 쓰레드와 1대1로 대응되지 않고, Multiplexing으로 훨씬 적은 OS 쓰레드를 사용한다.메모리 측면에서도 OS 쓰레드가 1MB의 스택을 갖는 반면, goroutine은 이보다 훨씬 작은 몇 KB의 스택을 갖는다.(필요 시 동적으로 증가한다.)Go 런타임은 Go 루틴을 관리하면서 Go 채널을 통해 Go 루틴 간의 통신을 쉽게 할 수 있도록 하였다.
아래의 예제에서 main함수를 보면, 먼저 say()라는 함수를 동기적으로 호출하고, 다음으로 동일한 say() 함수를 비동기적으로 3번 호출하고 있다.
첫 번째 동기적 호출은 say() 함수가 완전히 끝났을 때 다음 문장으로 이동하고, 다음 3개의 go say() 비동기 호출은 별도의 Go 루틴들에서 동작하면서, 메인 루틴은 계속 다음 문장(time.Sleep)을 실행한다.
여기서 goroutine들은 그 실행 순서가 일정하지 않으므로 프로그램을 실행할 때 마다 다른 출력 결과를 나타낼 수 있다.
pacakge main
import (
"fmt"
"time"
)
func say(s string) {
for i:= 0; i < 10; i++ {
fmt.Println(s, "***", i)
}
}
func main() {
// 함수를 동기적으로 실행
say("Sync")
// 함수를 비동기적으로 실행
go say("Async1")
go say("Async2")
go say("Async3")
// 3초 대기
time.Sleep(time.Second * 3)
}
익명 함수 Go루틴(Anonymous Function Go Routine)
Go루틴은 익명 함수에 대해 사용할 수도 있다.
go키워드 뒤에 익명 함수를 바로 정의하는 것으로, 이는 익명 함수를 비동기로 실행하게 된다.
아래의 예제에서 첫 번째 익명 함수는 간단히 Hello 문자열을 출력하는데, 이를 goroutine으로 실행하면 비동기적으로 그 익명 함수를 실행하게 된다.
두 번째 익명 함수는 매개변수를 전달하는 예제로 익명 함수에 매개변수가 있는 경우, Go 익명 함수 바로 뒤에 매개변수(Hi)를 함께 전달하게 된다.
package main
import (
"fmt"
"sync"
)
func main() {
// WaitGroup 생성. 2개의 Go루틴을 기다림.
var wait sync.WaitGroup
wait.Add(2)
// 익명함수를 사용한 goroutine
go func() {
defer wait.Done() // 끝나면 .Done() 호출
fmt.Println("Hello")
}()
// 익명함수에 매개변수를 전달
go func(msg string) {
defer wait.Done() // 끝나면 .Done() 호출
fmt.Println(msg)
}("Hi")
wait.Wait() // Go루틴이 모두 끝날 때까지 대기
}
여기서 sync.WaitGroup을 사용하고 있는데, 이는 기본적으로 여러 Go루틴들이 끝날 때 까지 기다리는 역할을 한다.
WaitGroup을 사용하기 위해서는 먼저 Add() 메소드에 몇 개의 Go루틴을 기다릴 것인지 지정하고, 각 Go루틴에서 Done() 메소드를 호출한다.(여기서는 defer를 사용하였다.)
그리고 메인 루틴에서는 Wait() 메소드를 호출하여, Go루틴들이 모두 끝나기를 기다린다.
다중 CPU 처리
Go는 디폴트(Default)로 1개의 CPU를 사용한다.
여러 개의 Go루틴을 만들더라도, 1개의 CPU에서 작업을 시분할 하여 처리한다.(Concurrent 처리)
만약 머신이 여러개의 CPU를 가진 경우, Go 프로그램을 다중 CPU에서 병렬 처리(Parallel)하게 할 수 있는데,
병렬 처리를 위해서는 아래와 같이 runtime.GOMAXPROCS(CPU의 수) 함수를 호출하여야 한다.(여기서 CPU의 수는 Logical CPU 수를 가리킨다.)
pacakge main
import (
"runtime"
)
func main() {
// 4개의 CPU 사용
runtime.GOMAXPROCS(4)
// ...
}
Concurrency(또는 Concurrent 처리)와 Parallelism(또는 Parallel 처리)는 비슷하게 들리지만, 전혀 다른 개념이다.
In programming, concurrency is the composition of independently executing processes, while parallelism is the simultaneous execution of (possibly related) computations.
Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once.
Go 채널
Go 채널은 그 채널을 통하여 데이터를 주고 받는 통로라 볼 수 있는데, 채널은 make() 함수를 통해 미리 생성되어야 하며, 채널 연산자 <- 을 통해 데이터를 보내고 받는다.채널은 흔히 goroutine들 사이 데이터를 주고 받는데 사용되는데, 상대편이 준비될 때까지 채널에서 대기함으로써 별도의 lock을 걸지 않고 데이터를 동기화 하는데 사용된다.
아래의 예제는 정수형 채널을 생성하고, 한 goroutine 에서 그 채널에 123이란 정수 데이터를 보낸 후, 이를 다시 메인 루틴에서 채널로부터 123 데이터를 받는 코드이다.
채널을 생성할 때는 make() 함수에 어떤 타입의 데이터를 채널에서 주고 받을 지를 미리 지정해 주어야 한다.
채널로 데이터를 보낼 때는 채널 명 <- 데이터와 같이 사용하고, 채널로부터 데이터를 받을 경우는 <- 채널 명 와 같이 사용한다.메인 루틴은 마지막에서 채널로부터 데이터를 받고 있는데, 상대편 goroutine에서 데이터를 전송할 때까지는 계속 대기하게 된다.
따라서, 이 예제에서는 time.Sleep() 이나 fmt.Scanf() 같이 goroutine 이 끝날 때까지 기다리는 코드를 적지 않았다.
package main
func main() {
// 정수형 채널을 생성한다.
ch := make(chan int)
go func() {
ch <- 123 // 채널에 123을 보낸다.
}()
var i int
i =<- ch // 채널에 123을 보낸다.
println(i)
}
Go 채널은 수신자와 송신자가 서로를 기다리는 속성 때문에, 아래의 예제와 같이 이를 이용하여 Go 루틴이 끝날 때까지 기다리는 기능을 구현할 수 있다.
즉, 익명 함수를 사용한 한 Go 루틴에서 어떤 작업이 실행되고 있을 때, 메인 루틴은 <- done 에서 계속 수신하며 대기하고 있게 된다.
익명 함수 Go 루틴에서 작업이 끝난 후, done채널에 true를 보내면, 수신자 메인 루틴은 이를 받고 프로그램을 끝내게 된다.
package main
import "fmt"
func main() {
done := make(chan bool)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(i)
}
done <- true
}()
// 위의 Go루틴이 끝날 때까지 대기
<-done
}
채널 버퍼링
Go 채널은 2가지의 채널이 있는데, Unbuffered Channel과 Buffered Channel이 있다.
위의 예제에서의 Go 채널은 Unbuffered Channel로서 이 채널에서는 하나의 수신자가 데이터를 받을 때까지 송신자가 데이터를 보내는 채널에 묶여 있게 된다.
하지만, Buffered Channel을 사용하면 비록 수신자가 받을 준비가 되어 있지 않을 지라도 지정된 버퍼만큼 데이터를 보내고 계속 다른 일을 수행할 수 있다.
버퍼 채널은 make(chan type, N) 함수를 통해 생성되는데, 두번 째 파라미터 N에 사용할 버퍼 갯수를 넣는다.
예를 들어, make(chan int, 10)은 10개의 정수형을 갖는 버퍼 채널을 만든다.
버퍼 채널을 이용하지 않는 경우, 아래와 같은 코드는 에러 (fatal error: all goroutines are asleep - deadlock!) 를 발생시킨다.
왜냐하면 메인 루틴에서 채널에 1을 보내면서 상대편 수신자를 기다리고 있는데, 이 채널을 받는 수신자 Go 루틴이 없기 때문이다.
package main
import "fmt"
func main() {
c := make(chan int)
c <- 1 // 수신 루틴이 없으므로 데드락
fmt.Println(<- c) // 코멘트를 해도 데드락 (별도의 Go루틴없기 때문)
}
하지만 아래와 같이 버퍼 채널을 사용하면, 수신자가 당장 없더라도 최대 버퍼 수까지 데이터를 보낼 수 있으므로, 에러가 발생하지 않는다.
package main
import "fmt"
func main() {
ch := make(chan int, 1)
// 수신자가 없더라도 보낼 수 있다.
ch <- 101
fmt.Println(<-ch)
}
채널 매개변수
채널을 함수의 매개변수도 전달할 때, 일반적으로 송수신을 모두 하는 채널을 전달하지만, 특별히 해당 채널로 송신만 할 것인지 혹은 수신만 할 것인지를 지정할 수도 있다.
송신 파라미터는 (p chan <- int)와 같이 chan <- 을 사용하고, 수신 파라미터는 (p <- chan int)와 같이 <- chan 을 사용한다.
만약 송신 채널 파라미터에서 수신을 한다거나, 수신 채널에 송신을 하게되면, 에러가 발생한다.
아래의 예제에서 만약 sendChan() 함수 안에서 x := <- ch 를 실행하면 송신 전용 채널에 수신을 시도하므로 에러가 발생한다.
package main
import "fmt"
func main() {
ch := make(chan string, 1)
sendChan(ch)
receiveChan(ch)
}
func sendChan(ch chan<- string) {
ch <- "Data"
// x := <-ch // 에러발생
}
func receiveChan(ch <-chan string) {
data := <-ch
fmt.Println(data)
}
채널 닫기
채널을 오픈한 후 데이터를 송신한 후, close()함수를 사용하여 채널을 닫을 수 있다.
채널을 닫게 되면, 해당 채널로는 더 이상 송신을 할 수 없지만, 채널이 닫힌 이후에도 계속 수신은 가능하다.
채널 수신에 사용되는 <- ch 은 두 개의 반환값을 갖는데, 첫 째는 채널 메시지이고, 두번 째는 수신이 제대로 되었는가를 나타낸다.
만약 채널이 닫혔다면, 두번 째 반환값은 false를 반환한다.
package main
func main() {
ch := make(chan int, 2)
// 채널에 송신
ch <- 1
ch <- 2
// 채널을 닫는다
close(ch)
// 채널 수신
println(<-ch)
println(<-ch)
if _, success := <-ch; !success {
println("더이상 데이타 없음.")
}
}
채널 range문
채널에서 송신자가 송신을 한 후, 채널을 닫을 수 있다. 그리고 수신자는 임의의 갯수의 데이터를 채널이 닫힐 때까지 계속 수신할 수 있다.
아래의 예제는 이러한 송수신 방법을 표현한 것으로, 수신자는 채널이 닫히는 것을 체크하면서 계속 루프를 돌게 된다.
방법1은 무한 for 루프 안에서 if 문으로 수신 채널의 두번 째 파라미터를 체크하는 방식이고, 방법2는 방법1과 동일한 표현이지만, for range문으로 보다 간결하게 표현한 것이다.
채널 range문은 range 키워드 다음의 채널로부터 계속 수신하다가 채널이 닫힌 것을 감지하면 for 루프를 종료한다.
package main
func main() {
ch := make(chan int, 2)
// 채널에 송신
ch <- 1
ch <- 2
// 채널을 닫는다
close(ch)
// 방법1
// 채널이 닫힌 것을 감지할 때까지 계속 수신
/*
for {
if i, success := <-ch; success {
println(i)
} else {
break
}
}
*/
// 방법2
// 위 표현과 동일한 채널 range 문
for i := range ch {
println(i)
}
}
채널 select 문
- Go의 select문은 복수 채널들을 기다리면서 준비된 (데이터를 보내온) 채널을 실행하는 기능을 제공한다. 즉, select문은 여러 개의 case문에서 각각 다른 채널을 기다리다가 준비가 된 채널 case를 실행하는 것이다.
- select문은 case 채널들이 준비되지 않으면 계속 대기하게 되고, 가장 먼저 도착한 채널의 case를 실행한다. 만약 복수 채널에 신호가 오면, Go 런타임이 무작위로 그 중 한 개를 선택한다. 하지만, select문에 default 문이 있으면, case문 채널이 준비되지 않더라도 계속 대기하지 않고 바로 default문을 실행한다.
- select { case <- 채널: 코드 }
select {
case <-채널1:
// 채널1에 값이 들어왔을 때 실행할 코드를 작성합니다.
case <-채널2:
// 채널2에 값이 들어왔을 때 실행할 코드를 작성합니다.
default:
// 모든 case의 채널에 값이 들어오지 않았을 때 실행할 코드를 작성합니다.
}
아래 예제는 for 루프 안에 select 문을 쓰면서 두 개의 goroutine이 모두 실행되기를 기다리고 있다.
첫번 째 run1()이 1초간 실행되고 done1 채널로부터 수신하여 해당 case를 실행하고,다시 for 루프를 돈다.
for루프를 다시 돌면서 다시 select문이 실행되는데, 다음 run2()가 2초후에 실행되고 done2 채널로부터 수신하여 해당 case를 실행하게 된다.
done2 채널 case문에 break EXIT 이 있는데, 이 문장으로 인해 for 루프를 빠져나와 EXIT 레이블로 이동하게 된다.
Go의 "break 레이블" 문은 C/C# 등의 언어에서의 goto 문과 다른데, Go에서는 해당 레이블로 이동한 후 자신이 빠져나온 루프 다음 문장을 실행하게 된다.
따라서, 여기서는 for 루프 다음 즉 main() 함수의 끝에 다다르게 된다.
package main
import "time"
func main() {
done1 := make(chan bool)
done2 := make(chan bool)
go run1(done1)
go run2(done2)
EXIT:
for {
select {
case <-done1:
println("run1 완료")
case <-done2:
println("run2 완료")
break EXIT
}
}
}
func run1(done chan bool) {
time.Sleep(1 * time.Second)
done <- true
}
func run2(done chan bool) {
time.Sleep(2 * time.Second)
done <- true
}
'Golang > Basic' 카테고리의 다른 글
Golang 헷갈리는 문법 복습 - interface (0) | 2022.02.19 |
---|---|
Golang 헷갈리는 문법 복습 - 배열과 슬라이스 (0) | 2022.02.19 |
Zero 부터 시작하는 Golang - 기본문법2 (0) | 2022.02.13 |
Zero 부터 시작하는 Golang - 기본문법1 (0) | 2022.02.13 |
Zero 부터 시작하는 Golang - 기본문법 (0) | 2022.02.13 |