그래 그리 쉽지는 않겠지

검색

검색 아이콘검색을 여는 아이콘

Tucker의 Go 언어 프로그래밍 Ch23 ~ Ch26

2023-10-29

이 글은 골든래빗 《 Tucker의 Go언어 프로그래밍 》의 23장~26장 써머리입니다.

Ch23 ~ Ch26

  • 에러 핸들링
  • 고루틴과 동시성 프로그래밍
  • 채널과 컨텍스트
  • 단어 검색 프로그램 만들기

# 에러 핸들링

# 에러 타입

1
2
3
type error interface {
	Error() string
}

문자열을 반환하는 Error() 메서드를 구현하고 있다면 에러로 사용 가능

# 에러 생성

사용자 error 생성

1
errors.New("에러 메시지")

에러 랩핑

  • 에러를 감싸 새로운 에러 생성
1
2
3
fmt.Errorf("에러 랩핑: %w", err)

if errors.As(err,  &numError) { // 감싸진 에러 타입을 검사 (감싸진 에러 중에 numError 타입으로 변환될 수 있는 에러가 있는지)

# 패닉

프로그램 흐름을 중지시키는 기능 (강제 종료)

  • 프로그램을 바로 종료시켜 빠르게 문제 발생 시점을 알 수 있음
  • 콜 스택(함수 호출 순서)을 확인할 수 있음
1
2
3
4
5
// panic() 함수 선언
func panic(interface{})

panic("에러 메시지")
panic(fmt.Errorf("This is error num: %d", num))

recover()

프로그램이 종료되지 않게 패닉을 복구

  • 발생한 panic 객체를 반환
1
2
3
4
5
6
// recover() 함수 선언
func recover() interface{}

if r, ok := recover().(net.Error); ok { // 반환된 panic 객체를 사용하려면 타입 검사가 필요
	fmt.Println("r is net.Error Type")
}

# 고루틴

Go언어에서 관리하는 경량 스레드

  • 함수나 명령을 동시에 실행할 때 사용
1
go 함수_호출

메인 루틴

main() 함수와 함께 시작되고 종료 (프로그램 또한 종료)

  • main() 함수로 부터 생성된 서브 고루틴들도 main() 함수 종료시 즉시 종료

# WaitGroup

고루틴이 종료될 때까지 대기하기 위해 사용

1
2
3
4
5
var wg sync.WaitGroup

wg.Add(3) // 작업 개수 설정
wg.Done() // 작업 완료 (작업 개수를 1씩 감소)
wg.Wait() // 모든 작업이 완료될 때까지 대기

# 동작 방법

OS 스레드를 이용하는 경량 스레드

  • CPU 코어마다 OS 스레드를 하나만 할당해서 사용
  • 남는 코어가 없을 때는 생길 때까지 고루틴들이 대기
  • 스레드를 할당 받은 고루틴이 대기 상태(e.g. 네트워크 수신 대기)에 들어가면 대기 중인 고루틴들과 교체

장점

컨텍스트 스위칭 비용이 발생하지 않음 (CPU 코어가 스레드를 변경하지 않고 고루틴만 옮겨 다님)

# 동시성 프로그래밍

# 뮤텍스

상호 배제

  • 자원 접근 권한을 통제
1
2
3
4
var mutex sync.Mutex

mutex.Lock() // 뮤텍스 획득: 다른 고루틴은 뮤텍스가 반납될 때까지 대기
defer mutex.Unlock() // 뮤텍스 반납

단점

  • 동시성 프로그래밍으로 얻는 성능 향상을 얻을 수 없음 > - 오직 하나의 고루틴만 공유 자원에 접근 가능
  • 데드락 발생 가능성이 생김 > - 데드락이 걸리지 않는지 확인하고 좁은 범위에서 사용

# 자원 관리 기법

고루틴들이 같은 자원에 접근하지 않도록 자원을 관리

  • 영역을 나누는 방법
    • e.g. 파일별로 고루틴 할당
  • 역할을 나누는 방법
    • 채널 사용

# 채널과 컨텍스트

# 채널

고루틴끼리 메시지를 전달할 수 있는 메시지 큐

인스턴스 생성

1
2
3
4
var messages chan string = make(chan string)

// 버퍼(크기: 2)를 가지는 채널
var messages chan string = make(chan string, 2)

사용

1
2
3
4
5
// 데이터 넣기
messages <- "this is a message"

// 데이터 빼기
var msg string = <- messages

# range 문

채널의 값을 모두 가져오고 닫혀있으면 탈출

1
2
3
for n := range ch {
	...
}

# select문

1
2
3
4
5
6
7
select {
case n := <-ch1:
	...
case n2 := <-ch2:
	...
case ...
}
  • 여러 채널을 동시에 대기
  • for 문과 함께 사용하여 종료하지 않고 계속해서 데이터 처리
  • 고루틴 안에 사용하여 다른 작업을 하며 채널 대기

time 채널

  • time.Tick(): 일정 시간 간격 주기로 신호를 보내주는 채널을 생성
  • time.After(): 일정 시간 경과 후에 신호를 보내주는 채널을 생성

# 생산자 소비자 패턴

역할을 나누는 방법 (컨베이어 벨트 시스템)

고루틴(작업)들 사이에 채널을 두고 작업 결과를 채널로 넘겨줌

장점

다음 작업 시작시 모든 작업이 끝날때까지 기다리지 않고 본인 작업을 시작할 수 있음

# 컨텍스트

context 패키지에서 제공하는 기능으로 작업 명세서 역할을 함

작업 취소가 가능한 컨텍스트

1
2
3
4
5
6
7
8
9
// 취소 가능한 컨텍스트 생성
ctx, cancel := context.WithCancel(context.Background())
...
cancel() // 컨텍스트의 Done() 채널에 시그널을 보냄
...
...
case <- ctx.Done():
	wg.Done()
	return

작업 시간을 설정한 컨텍스트

1
2
// 설정한 시간이 지난 뒤 컨텍스트의 Done() 채널에 시그널을 보냄
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)

특정 값을 설정한 컨텍스트

1
2
3
4
5
6
7
ctx := context.WithValue(context.Background(), "number", 9)
...

if v := ctx.Value("number"); v != nil {
	n := v.(int) // 반환 타입이 빈 인터페이스라 타입 변환 필요
	...
}

컨텍스트 랩핑

컨텍스트 생성시 이미 만들어진 컨텍스트 객체를 인자로 주어

컨텍스트 랩핑이 가능

# 단어 검색 프로그램 만들기

구조

tucker-golang-programming-ch26.png

코드

  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
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
package main

import (
	"bufio"
	"errors"
	"fmt"
	"os"
	"strconv"
	"strings"
	"sync"

	"path/filepath"
)

const linePrint = "------------------------------"

func main() {
	word, paths, err := input()
	if err != nil {
		panic(err)
	}

	var filePaths []string
	for _, path := range paths {
		matches, err := getFiles(path)
		if err != nil {
			fmt.Printf("파일 경로가 잘못되었습니다. path: %s, err: %s\n", path, err)
			continue
		}
		filePaths = append(filePaths, matches...)
	}
	if len(filePaths) == 0 {
		fmt.Println("일치하는 파일이 없습니다.")
		return
	}

	resultCh := make(chan string, len(filePaths))
	f := finder{
		filePaths,
	}
	go f.find(word, resultCh)

	sb := strings.Builder{}
	for result := range resultCh {
		sb.WriteString(result)
	}
	fmt.Println(sb.String())
}

// finder 찾으려는 파일 경로들을 가짐
type finder struct {
	filePaths []string
}

// input 경로에 해당하는 파일 반환
func input() (string, []string, error) {
	if len(os.Args) < 2 {
		return "", nil, errors.New("empty word argument")
	}
	word := os.Args[1]
	if len(os.Args) < 3 {
		return "", nil, errors.New("empty filepath argument")
	}
	filePaths := os.Args[2:] // 와일드 카드 사용시 가변 인자로 인식
	return word, filePaths, nil
}

// getFiles 경로에 해당하는 파일 리스트 반환
func getFiles(path string) ([]string, error) {
	return filepath.Glob(path)
}

// find 파일리스트의 파일들을 탐색
func (f finder) find(word string, resultCh chan string) {
	var wg sync.WaitGroup
	for _, filePath := range f.filePaths {
		wg.Add(1)
		go func(filePath string) {
			defer wg.Done()
			s, err := search(word, filePath)
			if err != nil {
				resultCh <- err.Error()
			}
			if s != "" {
				resultCh <- s
			}
		}(filePath)
	}
	wg.Wait()
	close(resultCh)
}

// search 파일을 열어 단어와 일치하는 라인 탐색, 출력문자열 생성
func search(word string, filePath string) (string, error) {
	// open file
	file, err := os.Open(filePath)
	if err != nil {
		return "", fmt.Errorf("파일을 찾을 수 없습니다. err: %w", err)
	}
	defer func() {
		if err := file.Close(); err != nil {
			fmt.Printf("Error closing file: %s, err: %s\n", filePath, err)
		}
	}()

	// find word in file
	result := find(word, file)

	// print string
	sb := strings.Builder{}
	sb.WriteString(file.Name()) // file name
	sb.WriteRune('\n')
	sb.WriteString(linePrint)
	sb.WriteRune('\n')
	sb.WriteString(result)
	sb.WriteString(linePrint)
	sb.WriteRune('\n')
	sb.WriteRune('\n')

	return sb.String(), nil
}

// find 파일에서 단어와 일치하는 라인 탐색 및 문자열 생성
func find(word string, file *os.File) string {
	sb := strings.Builder{}
	sc := bufio.NewScanner(file)
	for lineNum := 1; sc.Scan(); lineNum++ {
		text := sc.Text()
		if strings.Contains(text, word) {
			sb.WriteRune('\t')
			sb.WriteString(strconv.Itoa(lineNum))
			sb.WriteRune('\t')
			sb.WriteString(text)
			sb.WriteRune('\n')
		}
	}
	if sb.Len() == 0 {
		sb.WriteString("not found")
		sb.WriteRune('\n')
	}
	return sb.String()
}

# References