일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- passwd
- calico
- code-server
- ansible
- Kind
- Docker
- vscode
- 워커노드
- curl
- nested virtualization
- 504
- 패스워드 재설정
- 네트워크 네임스페이스
- 코어 쿠버네티스
- Pane
- go
- 도커
- ALB
- 컨테이너
- ubuntu
- WSL
- windows
- kubernetes
- 중첩가상화
- 쿠버네티스
- kubernets
- 묘공단
- containerd
- web ide
- network namespace
- Today
- Total
a story
Go스터디: 5주차(23~26장) 본문
이 글은 골든래빗 ‘Tucker의 Go 언어 프로그래밍의 23~26장 써머리입니다.
Go에서 에러를 처리하는 방법과 동시성 프로그래밍에 대한 주제를 다루고 있습니다.
23. 에러 핸들링
에러 핸들링(error handling)은 프로그램의 에러를 처리하는 방법을 말한다. 특정 에러가 발생했을 때 프로그램이 강제 종료 되는 것보다는 적절한 메시지를 출력하고, 에러를 다른 방식으로 처리해서 사용자 경험을 향상 시킬 수 있다.
package main
import (
"os"
)
const filename string = "data.txt"
func main() {
file, _ := os.Open(filename)
defer file.Close()
}
이렇게 하면 Program exited.로 종료한다.
package main
import (
"fmt"
"os"
)
const filename string = "data.txt"
func main() {
file, err := os.Open(filename)
if err != nil {
fmt.Println(err)
return
}
defer file.Close()
}
이렇게하면 open data.txt: no such file or directory으로 종료한다.
fmt 패키지의 Errorf() 함수로 err을 반환하면 원하는 에러 메시지를 만들어 전달할 수 있다. 또는 errors 패키지의 New()함수를 이용해서 error를 생성할 수도 있다.
import "errors"
errors.New("에러 메시지")
error 타입은 인터페이스로 문자열을 반환하는 Error() 메서드로 구성되어 있다. 즉 어떤 타입이든 문자열을 반환하는 Error()메서드를 포함하고 있다면 에러로 사용할 수 있다.
type error interface {
Error() sting
}
회원 가입에서 패스워드 길이를 체크하는 예제를 살펴본다.
package main
import (
"fmt"
)
type PasswordError struct { // 에러 구조체 선언
Len int
RequireLen int
}
func (err PasswordError) Error() string {
return "암호 길이가 짧습니다."
}
func RegisterAccount(name, password string) error {
if len(password) < 8 {
return PasswordError{len(password), 8} // 에러 반환, 암호 길이가 짧을 때 PasswordError 구조체 정보 반환
}
return nil
}
func main() {
err := RegisterAccount("myaccnt", "mypw")
if err != nil { // 에러 확인
if errInfo, ok := err.(PasswordError); ok { // 인터페이스 변환, 인터페이스 변환 성공 여부 검사(ok)->다양한 에러 타입에 대응
fmt.Printf("%v Len:%d RequireLen:%d\\n",
errInfo, errInfo.Len, errInfo.RequireLen)
}
} else {
fmt.Println("회원 가입 했습니다.")
}
}
한편 패닉(panic)은 프로그램을 정상 진행시키기 어려운 상황을 만났을 때 프로그램 흐름을 중지시키는 기능이다.
지금까지 error 인터페이스를 사용해 에러를 처리해 사용자에게 에러의 이유를 알려주는 것이지만(사용자 관점), 이와는 다르게 panic()은 문제 발생 시점 빠르게 프로그램을 종료시켜서 빠르게 문제 발생 시점을 알게하는 방식이다(개발자 관점).
panic() 내장 함수를 호출하고 인수로 에러 메시지를 입려갛면 프로그램을 즉시 종료하고 에러 메시지를 추력하고, 함수 호출 순서를 나타내는 콜 스택(call stack)을 표시한다.
package main
import (
"os"
)
const filename string = "data.txt"
func main() {
file, err := os.Open(filename)
if err != nil {
panic("파일을 읽을 수 없습니다")
}
defer file.Close()
}
앞선 예제를 panic()으로 처리하면 아래와 같이 call stack이 떨어진다.
panic: 파일을 읽을 수 없습니다
goroutine 1 [running]:
main.main()
/tmp/sandbox3015253680/prog.go:12 +0x85
Program exited.
콜 스택이란 panic 이 발생한 마지막 함수 위치부터 역순으로 호출 순서를 표시한다.
프로그램을 개발하는 시점에는 문제점을 파악하고 수정하는 것이 중요하지만, 사용자에게 인도가 된 이후에는 문제가 발생하더라도 프로그램이 종료되는 대신 에러 메시지를 표시하고 복구를 시도하는 것이 나을 수 있다.
panic은 호출 순서를 거슬러 올라가며 전파되는데, main() → f() → g() → h() 순서로 호출되었을 때, h()에서 패닉이 발생하면 호출 순서를 거꾸로 올라가면서 g() → f() → main() 으로 전달된다. 이때, recover()를 만나면 패닉을 복구할 수 있다.
다만 recover() 또한 제한적으로 사용하는 것이 좋다. 복구가 되더라도 프로그램 상태가 불안정한 상태(데이터가 일부가 쓰여지거나 하면서 데이터가 비정상 적으로 저장될 수 있음)로 남는 것을 주의해야 한다.
24. 고루틴과 동시성 프로그래밍
고루틴(goroutine)은 경량 스레드로 함수나 명령을 동시에 실행할 때 사용한다. 프로그램 시작점인 mian() 또한 고루틴에 의해 실행된다.
보통 단일 Core에서 멀티 스레드 프로세스를 지원하기 위해 시분할로 다수의 스레드를 처리하지만 이 과정에서 컨텍스트 스위치(context switch: 현재의 상태_context를 보관하고 새로운 상태를 복원) 비용이 발생한다.
Go에서는 CPU 코어 마다 OS 스레드를 하나만 할당해서 사용하기 때문에 컨텍스트 스위칭 비용이 발생하지 않는다. 실제로 하나의 OS 스레드에 하나의 고루틴이 실행된다.
컨텍스트 스위칭은 CPU 코어가 스레드를 변경할 때 발생하는데, 고루틴을 이용하면 코어와 스레드는 변경되지 않고 오직 고루틴만 옮겨 다니기 때문에(코어가 스레드를 변경하지 않음) 컨텍스트 스위칭 비용이 발생하지 않는다.
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup // wiatGroup 객체
func SumAtoB(I, a, b int) {
sum := 0
for i := a; i <= b; i++ {
sum += i
}
fmt.Printf("%d번째: %d ~ %d 합계는 %d 이다\\n", I, a, b, sum)
wg.Done() // 작업 1개 완료를 의미
}
func main() {
wg.Add(10) // 총 작업 수 설정
for i := 0; i < 10; i++ {
go SumAtoB(i, 1, 100000)
}
wg.Wait() // 모든 작업의 완료 대기
}
순서를 보장하지 않는다.
9번째: 1 ~ 100000 합계는 5000050000 이다
5번째: 1 ~ 100000 합계는 5000050000 이다
0번째: 1 ~ 100000 합계는 5000050000 이다
1번째: 1 ~ 100000 합계는 5000050000 이다
2번째: 1 ~ 100000 합계는 5000050000 이다
3번째: 1 ~ 100000 합계는 5000050000 이다
4번째: 1 ~ 100000 합계는 5000050000 이다
8번째: 1 ~ 100000 합계는 5000050000 이다
6번째: 1 ~ 100000 합계는 5000050000 이다
7번째: 1 ~ 100000 합계는 5000050000 이다
동시성 프로그래밍의 문제점은 동일한 메모리 자원에 여러 고루틴이 접근할 때 발생한다. 이 문제를 해결하기 위해서 한 고루틴에서 값을 변경할 때 다른 고루틴이 건들지 못하게 해당 자원에 대해서 뮤텍스(mutex)를 이용할 수 있다.
뮤텍스의 Lock() 메서드를 호출해 뮤텍스를 획득하면, 다른 고루틴에서는 획득한 뮤텍스가 반납될 때까지 대기하게 된다. 앞서 뮤텍스를 획득했으면 Unlock() 메서드를 호출해 반납해야 한다.
아래는 뮤텍스 예제이다. 특정 리소스를 대상으로 뮤텍스를 획득한다기 보다는, 동시성 문제를 일으킬 수 있는 동작 자체를 뮤텍스 처리한다.
package main
import (
"fmt"
"sync"
"time"
)
var mutex sync.Mutex // 패키지 전역 변수 뮤텍스
type Account struct {
Balance int
}
func DepositAndWithdraw(account *Account) {
mutex.Lock() // 뮤텍스 획득
defer mutex.Unlock() // defer로 Unlock() 지연 호출
if account.Balance < 0 {
panic(fmt.Sprintf("Blaance should not be negative"))
}
// 1000원을 입금하고 1000원을 출금한다.
account.Balance += 1000
time.Sleep(time.Millisecond)
account.Balance -= 1000
}
func main() {
var wg sync.WaitGroup
account := &Account{0}
wg.Add(10)
for i := 0; i < 10; i++ {
go func() {
DepositAndWithdraw(account)
wg.Done()
}()
}
wg.Wait()
}
다만 뮤텍스를 사용하면 동시성 프로그래밍 문제를 해결할 수 있지만, 성능 향상을 저해할 수 있고 또한 데드락(deadlock, 서로 뮤텍스에 들어간 자원을 얻으려고 대기하는 상황)이 발생할 수 있는 문제가 있다.
25. 채널과 컨텍스트
채널(channel)과 컨텍스트(context)는 Go에서 동시성 프로그래밍을 도와주는 기능이다.
25.1 채널
채널은 고루틴 간 메시지를 전달하는 메시지 큐이다. 메시지가 들어온 순서대로 쌓이고, 차례대로 읽게 된다.
아래의 예시와 같이 사용할 수 있다.
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
ch := make(chan int) // 채널 생성
wg.Add(1)
go square(&wg, ch) // 고루틴 생성, 채널 전달
ch <- 9 // 채널에 데이터 넣음
wg.Wait() // 작업이 완료될때까지 기다림
}
func square(wg *sync.WaitGroup, ch chan int) {
n := <-ch // 채널에서 데이터 빼옴
time.Sleep(time.Second)
fmt.Printf("Square: %d\\n", n*n)
wg.Done()
}
채널을 아래와 같이 생성하면 버퍼(내부에 데이터를 보관할 수 있는 메모리 영역)를 가진 채널을 만들 수 있다.
var chan string messages = make(chan string, 2)
채널을 활용하면 생산자 소비자 패턴(Producer Consumer Pattern, 한쪽에서 데이터를 생성해서 넣어주면 다른 쪽에서 생성된 데이터를 빼서 사용하는 방식)을 구현할 수 있다.
25.2 컨텍스트
컨텍스트는 고루틴에 작업을 요청할 때 작업 취소나 작업 시간 등을 설정할 수 있는 작업 명세서 역할을 한다.
작업 취소가 가능한 컨텍스트를 아래와 같이 만들 수 있다.
ctx, cancel := context.WithCancel(context.Background())
작업 시간을 설정한 컨텍스트를 아래와 같이 만들 수 있다. 아래는 3초 후 종료한다.
ctx, cancle := context.WithTimeout(context.Background(), 3*time.Second)
별도 지시사항을 추가한 컨텍스틀 아래와 같이 만들 수 있다.
ctx := context.WithValue(context.Background(), "number", 9)
26. 단어 검색 프로그램 만들기
파일에서 단어 검색하는 프로그램에 대한 예제이다.
여기서는 앞서 다루지 않은 프로그램 그램 실행 시점 실행 인수를 전달하는 방법과 파일에서 한 줄 씩 읽어서 처리하는 방식을 추가로 다루고 있다. 한편 검색해야 하는 파일이 여러 개일 때, 파일을 하나 씩 검색하는 것보다 빠르게 실행하기 위해서, 각 파일에 대한 검색을 고루틴으로 적용한다.
먼저 Go에서는 실행 인수를 os.Args 변수를 이용해서 가져올 수 있다.
os.Args // 실행 인수 개수
os.Args[1] // 실행 인수 가져오기
그리고 파일에서 단어를 찾기 위해서 파일을 열고, bufio 패키지의 NewScanner()함수를 이용해 스캐너를 만들고 파일 내용을 한 라인씩 읽어 올 수 있다.
scanner := bufio.NewScanner(file) // 스캐너를 생성해서 한 줄씩 읽기
for scanner.Scan() {
fmt.Println(scanner.Text())
}
실제 scanner.Text()에서 파일이 있는지 여부는 한 라인에 대해서 strings.Contains(라인, 찾는단어)로 확인한다.
다만 실행 인수에 단어를 찾을 다수의 파일이 들어온다면, 다수 파일에서 단어 검색을 처리하기 위해서 고루틴을 활용해 시간을 단축할 수 있다.
'Book Study > Tucker의 Go Programming' 카테고리의 다른 글
Go스터디: 7주차(31장) (0) | 2023.11.09 |
---|---|
Go스터디: 6주차(27~30장) (0) | 2023.11.05 |
Go스터디: 4주차(18~22장) (1) | 2023.10.22 |
Go스터디: 3주차(12~17장) (1) | 2023.10.15 |
Go스터디: 2주차(3~11장) (1) | 2023.10.08 |