Study

[스터디] Tucker의 Go 언어 프로그래밍 내용정리 24~26장

mokpolar 2023. 12. 12. 23:37
반응형

24장. 고루틴과 동시성 프로그래밍

24.1 스레드란?

  • 고루틴 : 경량 스레드, 함수나 명령을 동시에 실행할 때 사용

24.1.1 컨텍스트 스위칭 비용

  • 컨텍스트 스위칭 비용 : CPU 코어가 여러 스레드를 전환하면서 수행할 경우 드는 비용
  • 스레드 컨텍스트 : 스레드 전환시 현 상태를 보관하는데, 스레드의 명령 포인터 intstruction pointer, 스택 메모리등
  • 코어 개수의 두 배 이상 스레드를 만들면 스위칭 비용이 많이 발생
  • GO 언어는 CPU 코어마다 OS 스레드를 하나만 할당해서 사용하기에 컨텍스트 스위칭 비용이 발생하지 않는다.

24.2 고루틴 사용

  • 모든 프로그램은 고루틴을 최소 하나는 가지는데 이것이 메인루틴이다.
  • 메인루틴인 고루틴은 main()함수와 함께 시작되고 종료된다.
  • 고루틴 추가 생성법 : go 키워드와 함수 호출
go 함수_호출
  • go 키워드를 사용해서 고루틴을 생성하며, 각기 다른 새로운 고루틴에서 실행되기에 동시에 실행된다.
  • 코어개수가 3개 이상이 되지 않으면 세 고루틴을 동시에 실행시킬 코어가 부족해서 동시에 실행되지는 않는다.
  • main(), PrintHanguls(), PrintNumbers() 각기 3개의 고루틴이다.
package main

import (
	"fmt"
	"time"
)

func PrintHangul() {
	hanguls := []rune{ '가', '나', '다', '라', '마' }
	for _, v := range hanguls {
		time.Sleep(300 * time.Millisecond)
		fmt.Printf("%c ", v)
	}
}

func PrintNumbers () {
		for i := 1; i <= 5; i++ {
			time.Sleep(400 * time.Millisecond)
			fmt.Printf("%d ", i)
	}
}

func main() {
	go PrintHanguls()
	go PrintNumbers() // 새로운 고루틴 생성
	
	time.Sleep(3 * time.Second) // 3초간 대기
}

24.2.1 서브 고루틴이 종료될 때까지 기다리기

  • 고루틴이 종료되는 타이밍은 sync 패키지의 WaitGroup 객체를 통해 알 수 있다.
  • Add() 메서드를 통해 완료해야 하는 작업 개수를 설정
  • Done() 메서드를 호출하여 각 작업이 완료될 때마다 작업 개수 줄이기
  • Wait() 전체 작업이 모두 완료될 때 까지 대기
var wg sync.WaitGroup

wg.Add(3) // 작업 개수 설정
wg.Done() // 작업이 완료될 때마다 호출
wg.Wait() // 모든 작업이 완료될 때까지 대기 
package main

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup // waitGroup 객체

func SumAtoB(a, b int) {
	sum := 0
	for i := a; i <= b; i++ {
		sum += i
	}

	fmt.Printf("%d부터 %d까지 합계는 %d입니다. \\n", a, b, sum)
	wg.Done() // 작업이 완료됨을 표시
}

func main() {
	wg.Add(10) // 총 작업 개수 설정
	for i := 0; i < 10; i++ {
		go SumAtoB(1, 100000000)
	}

	wg.Wait() // 모든 작업이 완료되길 기다림
	fmt.Println("모든 계산이 완료되었습니다.")
}
...
1부터 100000000까지 합계는 5000000050000000입니다. 
1부터 100000000까지 합계는 5000000050000000입니다. 
1부터 100000000까지 합계는 5000000050000000입니다. 
1부터 100000000까지 합계는 5000000050000000입니다. 
1부터 100000000까지 합계는 5000000050000000입니다. 
1부터 100000000까지 합계는 5000000050000000입니다. 
1부터 100000000까지 합계는 5000000050000000입니다. 
1부터 100000000까지 합계는 5000000050000000입니다. 
1부터 100000000까지 합계는 5000000050000000입니다. 
1부터 100000000까지 합계는 5000000050000000입니다. 
모든 계산이 완료되었습니다.

 

24.3 고루틴의 동작 방법

  • 고루틴은 운영체제가 제공하는 OS스레드를 이용하는 경량 스레드

24.3.1 고루틴이 하나일 때

  • 컴퓨터에서 모든 명령은 OS 스레드를 통해 CPU 코어에서 실행된다.
  • Go 로 만든 프로그램 역시 OS위에서 돌아가기 때문에 명령을 수행하려면 OS스레드를 만들어서 명령을 실행해야 한다.
  • main() 루틴만 존재하면 OS 스레드를 하나 만들어 첫 번째 코어와 연결한다.
  • OS 스레드에서 고루틴을 실행하게 된다.24.3.2 고루틴이 두 개 일때 

 

24.3.2 고루틴이 두 개일때

  • 두 번째 코어가 남아 있어서 두 번째 OS 스레드를 생성하여 두 번째 고루틴을 실행할 수 있다.

 

 

 

24.3.3 고루틴이 세 개 일때

  • 코어가 2개인 경우 세 번째 고루틴용 스레드를 만들지 않고 남는 코어가 생길 때까지 대기한다.
  • 만약 두 번째 고루틴이 모두 실행 완료되면 코어2가 비므로 고루틴3이 실행된다.

 

 

 

24.3.4 시스템 콜 호출시

  • 시스템 콜 : 운영체제가 지원하는 서비스를 호출할 때 , 예를 들어 네트워크 기능
  • 시스템 콜을 호출하면 운영체제에서 해당 서비스가 완료될 때까지 대기해야 한다.
  • 예를 들어 네트워크로 데이터를 읽을 때는 데이터가 들어올 때까지 대기 상태가 된다.
  • 대기 상태인 고루틴에 CPU와 OS 스레드를 할당하면 CPU 자원 낭비가 된다.
  • GO 언어에서는 이런 상태에 들어간 루틴을 대기 상태로 보내고,
  • 실행을 기다리는 다른 루틴에 CPU 코어와 OS 스레드를 할당하여 실행할 수 있게 한다.
  • 컨텍스트 스위칭은 CPU코어가 스레드를 변경할 때 발생하는데, 
  • 고루틴의 경우 코어와 스레드는 변경되지 않고 고루틴만 옮겨 다니고 ,
  • 코어가 스레드를 변경하지 않기 때문에 컨텍스트 스위칭 비용이 발생하지 않는다. 

24.4 동시성 프로그래밍 주의점

  • 동일한 메모리 자원에 여러 고루틴이 접근할 때
  • 동시성 문제의 예
    • 아래의 경우 1000원을 입금하고 100원을 출금하기 때문에 잔고가 0원 밑으로 내려가면 안되나, 0원이 되어 패닉이 발생
    • account.Balance += 1000 코드는 먼저 Balance 값을 읽고 1000을 더해서 Balance에 저장한다.
    • 이 단계가 완료되기전에 다른 고루틴이 첫 번째 단계를 수행하면 두 고루틴은 똑같은 값을 읽어서 1000씩 더해 Balance에 저장한다.
    • 고루틴 2개가 각각 입금을 했는데 한번 입금한 효과밖에 나지 않아서 출금이 각각 이루어지면 -가 된다
package main

import (
	"fmt"
	"sync"
	"time"
)

type Account struct {
	Balance int
}

func main() {
	var wg sync.WaitGroup

	account := &Account{0} // 0원 잔고 통장

	wg.Add(10) // waitGroup 객체 생성

	for i := 0; i < 10; i++ { // 고루틴 10개 생성
		go func() {
			for {
				DepositAndWithdraw(account)
			}
			wg.Done()
		}()
	}
	wg.Wait()
}

func DepositAndWithdraw(account *Account) {
	if account.Balance < 0 { // 잔고가 0 미만이면 패닉
		panic(fmt.Sprintf("Balance should not be negative value: %d",
			account.Balance))
	}
	account.Balance += 1000      // 천원 입금
	time.Sleep(time.Microsecond) // 잠시 쉬고
	account.Balance -= 1000      // 천원 출금
}

 

 

24.5 뮤텍스를 이용한 동시성 문제 해결

  • 단순한 해결방법은 뮤텍스 mutex
  • 뮤텍스 mutual exclusion (상호 배제), 자원 접근 권한
  • 뮤텍스의 Lock() 메서드를 호출해 뮤텍스를 획득
  • 이미 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 { // 잔고가 0 미만이면 패닉
		panic(fmt.Sprintf("Balance should not be negative value: %d",
			account.Balance))
	}
	account.Balance += 1000      // 천원 입금
	time.Sleep(time.Microsecond) // 잠시 쉬고
	account.Balance -= 1000      // 천원 출금
}

func main() {
	var wg sync.WaitGroup

	account := &Account{0} // 0원 잔고 통장

	wg.Add(10) // waitGroup 객체 생성

	for i := 0; i < 10; i++ { // 고루틴 10개 생성
		go func() {
			for {
				DepositAndWithdraw(account)
			}
			wg.Done()
		}()
	}
	wg.Wait()
}
  • 패키지 전역 변수로 뮤텍스를 만들고
  • 함수에서 mutex.Lock() 메서드를 호출해서 뮤텍스를 획득.
  • 만약 다른 고루틴이 이미 획득했다면 놓을 때까지 대기
  • defer를 사용해서 함수 종료전에 뮤텍스 Unlock() 메서드가 호출될 수 있도록 보장
  • 한번 획득한 뮤텍스는 반드시 Unlock()을 호출해서 반납해야 한다.

 

24.6 뮤텍스와 데드락

  • 뮤텍스의 문제점
    • 동시성 프로그래밍으로 얻을 수 있는 성능 향상이 제한된다.
    • 데드락이 발생할 수 있다.
      • 어떤 고루틴도 원하는 만큼 뮤텍스를 확보하지 못해서 무한히 대기하게 된다.

24.7 또 다른 자원 관리 기법

  • 같은 자원에 여러 고루틴이 접근하지 않게하면 문제가 해결된다.
  • 두 가지 방법
    • 영역을 나누는 방법
    • 역할을 나누는 방법
  • 첫 번째 영역을 나누는 방법의 예
    • 10가지 작업을 배열에 할당하고 각 작업을 고루틴으로 실행
    • 각 고루틴은 할당된 작업만 하므로 고루틴 간 간섭이 발생하지 않는다.
package main

import (
	"fmt"
	"sync"
	"time"
)

type Job interface { // Job 인터페이스
	Do()
}

type SquareJob struct {
	index int
}

func (j *SquareJob) Do() {
	fmt.Printf("%d 작업 시작\\n", j.index) // 각 작업
	time.Sleep(1 * time.Second)
	fmt.Printf("%d 작업 완료 - 결과: %d\\n", j.index, j.index*j.index)
}

func main() {
	var jobList [10]Job

	for i := 0; i < 10; i++ { // 10가지 작업 할당
		jobList[i] = &SquareJob{i}
	}

	var wg sync.WaitGroup

	wg.Add(10)

	for i := 0; i < 10; i++ {
		job := jobList[i]
		go func() {
			job.Do()
			wg.Done()
		}()
	}
	wg.Wait()
}
3 작업 시작
2 작업 시작
5 작업 시작
0 작업 시작
6 작업 시작
4 작업 시작
7 작업 시작
9 작업 시작
8 작업 시작
1 작업 시작
4 작업 완료 - 결과: 16
2 작업 완료 - 결과: 4
6 작업 완료 - 결과: 36
3 작업 완료 - 결과: 9
5 작업 완료 - 결과: 25
1 작업 완료 - 결과: 1
0 작업 완료 - 결과: 0
7 작업 완료 - 결과: 49
9 작업 완료 - 결과: 81
8 작업 완료 - 결과: 64

 

 

25.1. 채널 사용하기

  • 채널 : 고루틴끼리 메시지를 전달할 수 있는 메시지 큐
  • 메시지 큐에 메시지들을 차례대로 쌓이게 되고 메시지를 읽을 때는 맨 처음 온 메시지부터 차례대로 읽는다.

25.1.1 채널 인스턴스 생성

  • 채널 을 의미하는 chan과 메시지 타입을 합쳐서 사용
  • make() 함수로 만들 수 있다.
var messages chan string = make(chan string)

25.1.2 채널에 데이터 넣기

messages <- "This is a message"

25.1.3 채널에서 데이터 빼기

var msg string = <- messages
  • int 채널을 생성한다.
  • square() 함수를 실행하는 고루틴을 생성하고 채널 인스턴스에서 데이터를 빼오기를 대기한다.
  • 채널에 9를 넣는다.
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) // 1초 대기
	fmt.Printf("Square: %d\\n", n*n)
	wg.Done()
}

Square: 81

25.1.4 채널 크기

  • 채널을 생성하면 크기가 0인 채널이 만들어진다.
  • 0이면 채널에 들어온 데이터를 담아올 곳이 없다.
  • 채널에 데이터를 넣고 다른 고루틴이 데이터를 빼가지 않으면 고루틴들이 대기하며 deadlock 상태가된다.

25.1.5 버퍼를 가진 채널

  • 버퍼 : 내부에 데이터를 보관할 수 있는 메모리 영역
  • 버퍼를 가진 채널 = 보관함을 가진 채널
var chan string messages = make(chan string, 2)

25.1.6 채널에서 데이터 대기

  • 채널 사용시 아래와 같이 close(ch)를 호출해 채널을 닫고 채널이 닫혔음을 알려줘야 한다.
  • 그렇지 않으면 채널에 데이터가 들어오기를 계속 기다리게 된다.
package main

import (
	"fmt"
	"sync"
	"time"
)

func square(wg *sync.WaitGroup, ch chan int) {
	for n := range ch { // 채널이 닫히면 종료
		fmt.Printf("Square: %d\\n", n*n)
		time.Sleep(time.Second)
	}
	wg.Done()
}

func main() {
	var wg sync.WaitGroup
	ch := make(chan int)

	wg.Add(1)
	go square(&wg, ch)

	for i := 0; i < 10; i++ {
		ch <- i * 2
	}
	close(ch)
	wg.Wait()
}
Square: 0
Square: 4
Square: 16
Square: 36
Square: 64
Square: 100
Square: 144
Square: 196
Square: 256
Square: 324

 

25.1.7 select 문

  • 채널에서 데이터가 들어오기를 대기하는 상황에서 만약 데이터가 들어오지 않으면?
  • 다른 작업을 하거나
  • 아니면 여러 채널을 동시에 대기하고 싶을 때
  • select문을 사용한다.
  • 아래와 같이 하면 어떤 채널이라도 하나의 채널에서 데이터를 읽어오면 해당 구문을 실행하고 select 문이 종료된다.
select {
case n := <-ch1: // ch1 채널에서 데이터를 빼낼 수 있을 때 실행
...
case n2 := <=ch2: // ch2 채널에서 데이터를 빼낼 수 있을 때 실행
...
case ...
}
  • quit 종료 채널을 아래와 같이 만들어서 알려줄 수 있다.
package main

import (
	"fmt"
	"sync"
	"time"
)

func square(wg *sync.WaitGroup, ch chan int, quit chan bool) {
	for {
		select { // ch와 quit 양쪽을 모두 기다림
		case n := <-ch:
			fmt.Printf("Square: %d\\n", n*n)
			time.Sleep(time.Second)
		case <-quit:
			wg.Done()
			return
		}
	}
}

func main() {
	var wg sync.WaitGroup
	ch := make(chan int)
	quit := make(chan bool) // 종료 채널

	wg.Add(1)
	go square(&wg, ch, quit)

	for i := 0; i < 10; i++ {
		ch <- i * 2
	}

	quit <- true
	wg.Wait()
}
package main

import (
	"fmt"
	"sync"
	"time"
)

func square(wg *sync.WaitGroup, ch chan int) {
	tick := time.Tick(time.Second)            // 1초 간격 시그널
	terminate := time.After(10 * time.Second) // 10초 이후 시그널

	for {
		select {
		case <-tick:
			fmt.Println("Tick")
		case <-terminate:
			fmt.Println("Terminated!!")
			wg.Done()
			return
		case n := <-ch:
			fmt.Printf("Square: %d\\n", n*n)
			time.Sleep(time.Second)
		}
	}
}

func main() {
	var wg sync.WaitGroup
	ch := make(chan int)

	wg.Add(1)
	go square(&wg, ch)

	for i := 0; i < 10; i++ {
		ch <- i * 2
	}
	wg.Wait()
}

Square: 0
Square: 4
Square: 16
Square: 36
Square: 64
Square: 100
Square: 144
Square: 196
Square: 256
Square: 324
 krafton@mokpolar-mac  ~/Documents/GitHub/my-playground/tucker_golang/hello   main ±  go build
 krafton@mokpolar-mac  ~/Documents/GitHub/my-playground/tucker_golang/hello   main ±  ./hello 
Square: 0
Tick
Square: 4
Tick
Square: 16
Square: 36
Square: 64
Tick
Square: 100
Square: 144
Tick
Square: 196
Tick
Square: 256
Square: 324
Terminated!!

 

25.1.9 채널로 생산자 소비자 패턴 구현하기

  • 뮤텍스를 사용하지 않는 방법 두 번째는 채널을 이용해서 역할을 나누는 것이다
  • 아래와 같은 도식으로 이해해볼 수 있다. 

 

 

 

  • 아래와 같이 채널을 이용해서 역할을 나누면 고루틴 하나를 사용할 때보다 더 빠르게 작업을 완료할 수 있다.
  • 한쪽에서 데이터를 생성해서 넣어주면 한쪽에서 데이터를 빼서 사용하는 방식을 생산자 소비자 패턴 이라고 한다.
  • MakeBody() 루틴이 생산자, InstallTire()가 소비자가 되고 이런 식으로 역할이 바뀐다.

 

 

package main

import (
	"fmt"
	"sync"
	"time"
)

type Car struct {
	Body string
	Title string
	Color string
}

var wg sync.WaitGroup
var startTime = time.Now()

func main() {
	tireCh := make(chan *Car)
	paintCh := make(chan *Car)

	fmt.Printf("Start Factory \\n")

	wg.Add(3)

	go MakeBody(tireCh) // 고루틴 생성
	go InstallTire(tireCh, paintCh) 
	go PaintCar(paintCh)

	wg.Wait()
	fmt.Println("Close the factory")
}

func MakeBody(tireCh Chan *Car) { // 차체 생산
	tick := time.Tick(time.Second)
	after := time.After(10 * time.Second)
	for {
		select {
		case <- Tick:
			// Make a body
			car := &Car{}
			car.Body = "Sport car"
			tireCh <- car
		case <-after: // 10초 뒤 종료
			close(tireCh)
			wg.Done()
			return
		}
	}
}

func InstallTire(tireCh, paintCh chan  *Car) { // 바퀴 설치
	for car := range tireCh {
		//Make a body
		time.Sleep(time.Second)
		car.Tire = "Winter tire"
		paintCh <- car
		}
		Wg.Done()
		close(paintCh)	
}

func PaintCar(paintCh chan *Car) { // 도색
	for car := range paintCh {
		// make a body
		time.Sleep(time.Second)
		car.Color = "Red"
		duration := time.Now().Sub(startTime) // 경과 시간 출력
		fmt.Printf("%.2f Complete Car: %s %s %s\\n", duraton.Seconds(), 
			car.Body, car.Timre, car.Color)
	}
	wg.Done()
}

25.2 컨텍스트 사용하기

  • 컨텍스트 context : 작업을 지시할 때 작업 가능시간, 작업 취소 등의 조건을 지시할 수 있는 작업명세서 역할
  • 새로운 고루틴으로 작업을 시작할 때 일정시간 동안만 작업을 지시하거나 외부에서 작업을 취소할 때 사용

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

  • 작업자에게 전달하면 작업을 지시한 지시자가 원할 때 작업 취소를 알릴 수 있다.
  • 아래와 같이 취소 가능한 커텍스트를 생성한다.
  • conetxt.Background() 라는 기본적인 컨텍스트를 넣을 수 있다.
package main

import (
	"fmt"
	"sync"
	"time"
	"context"
)

var wg sync.WaitGroup

func main() {
	wg.Add(1)
	ctx, cancel := context.WithCancel(context.Background()) // 컨텍스트 생성
	go PrintEverySecond(ctx)
	time.Sleep(5*time.Second)
	cancel() // 취소

	wg.Wait()
}

func PrintEverySecond(ctx context.Context) {
	tick := time.Tick(time.Second)
	for {
		select {
		case <-ctx.Done(): // 취소 확인
			wg.Done()
			return
		case <-tick:
			fmt.Println("Tick")
		}
	}
}

25.2.2 작업 시간을 설정한 ㅋ너텍스트

  • 일정한 시간 동안 작업을 지시할 수 있는 컨텍스트 만들기
  • WithTimeout 함수로 작업 시간을 설정할 수 있다.
ctx, cancel := context.WithTimeout(context.Background()), 3*time.Second)

25.2.3 특정 값을 설정한 컨텍스트

ctx := context.WithValue(context.Background(), "number", 9)
  • 취소도 되면서 값을 설정하려면
  • context.Background()가 아니라 이미 만들어진 컨텍스트 객체를 넣어줄 수도 있다.
ctx, cancel := context.WithCancel(context.Background())
ctx = context.WithValue(ctx, "number", 9)
ctx = context.WithValue(ctx, "keyword", "Lilly")

 

 

 

26.1 해법

  • 사용자로부터 파일 경로와 특정 단어를 입력 받아서 파일에서 해당 단어 검색하기
  • 단어가 발견된 라인과 해당 라인 내용을 출력하고 종료

26.2 사전 지식

26.2.1 와일드카드

  • 와일드 카드를 사용해서 여러 파일을 나타낼 수 있다.
    • 는 0개 이상의 아무 문자
  • ? 는 문자 하나
  • abc*t : abc로 시작하고 그 사이에 0개 이상의 아무문자 그리고 마지막 t
  • ab?d : ab와 d 사이에 아무 문자

26.2.2 os.Args 변수와 실행 인수

  • 터미널 명령시 실행 인수 사용하는 것을
del filename
  • Go에서는
  • os패키지의 Args 변수를 이용, []string 슬라이스에 실행인수가 담긴다.
  • 첫 번째 항목으로는 실행 명령이 들어간다.
var Args []string
find word filepath
  • os.Args[0]에는 find [1]에는 file [2]에는 filepath

26.2.3 파일 핸들링

파일 열기

  • os.Open()으로 파일을 열어 파일 핸들 가져오기
  • *File 타입인 파인 핸들 객체를 반환한다.
  • *File 타입은 io.Reader 인터페이스를 구현하고 있어서 bufio 패키지의 NewScanner() 함수를 통해 스캐너 객체를 만들어 사용할 수 있다.
func Open(name string) (*File, error)

💡 풀어서 설명하면 File 타입의 객체가 io.Reader 인터페이스에 정의된 메서드를 갖고 있어서 File 객체를 사용하여 파일로부터 데이터를 읽을 수 있는 기능이 있음.

</aside>

<aside> 💡 bufio 패키지의 NewScanner() 함수는 io.Reader 인터페이스를 구현하는 어떤 객체도 입력으로 받아들일 수 있고 , io.Reader 객체로부터 데이터를 읽어들이는 스캐너 객체를 생성한다.

</aside>

package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    // 파일 열기
    file, err := os.Open("example.txt")
    if err != nil {
        panic(err)
    }
    defer file.Close()

    // 스캐너 생성
    scanner := bufio.NewScanner(file)

    // 파일 내용을 줄 단위로 읽기
    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }

    if err := scanner.Err(); err != nil {
        panic(err)
    }
}

파일 목록 가져오기

  • path/filepath 패키지의 Glob() 함수를 이용해서 파일 경로에 해당하는 파일 목록을 가져올 수 있다.
func Glob(pattern string) (matches []string, err error)
  • 파일 경로를 넣어주면 해당하는 파일 리스트를 []string 타입으로 반환한다.
filepaths, err := filepath.Glob("*.txt")

파일 내용 한 줄씩 읽기

func NewScanner(r io.Reader) *Scanner
  • io.Reader 인터페이스를 구현한 모든 인스턴스를 인수로 사용 가능하다.
  • *File 객체는 io.Reader 인터페이스를 구현하고 있다.
type Scanner
	func (s *Scanner) Scan() bool
	func (s *Scanner) Text() string
  • Scan() 다음 줄 읽기
  • Text() 읽어온 한 줄을 문자열로 반환하기

26.2.4 단어 포함 여부 검사

  • s안에 substr 이 포함되어 있는지 여부를 반환
func Contains(s, substr string) bool

26.5 파일 검색 프로그램

package main

import (
	"bufio"
	"fmt"
	"os"
	"path/filepath"
	"strings"
)

// 찾은 라인 정보
type LineInfo struct { // 찾은 결과 정보
	lineNo int
	line   string
}

// 파일 내 라인 정보
type FindInfo struct {
	filename string
	lines    []LineInfo
}

func main() {
	if len(os.Args) < 3 {
		fmt.Println("2개 이상의 실행 인수가 필요합니다. ex) ex26.3 word filepath")
		return
	}

	word := os.Args[1] // 찾으려는 단어
	files := os.Args[2:]
	findInfos := []FindInfo{}
	for _, path := range files {
		// 파일 찾기
		findInfos = append(findInfos, FindWordInAllFiles(word, path)...)
	}

	for _, findInfo := range findInfos {
		fmt.Println(findInfo.filename)
		fmt.Println("--------------------------------")
		for _, lineInfo := range findInfo.lines {
			fmt.Println("\\t", lineInfo.lineNo, "\\t", lineInfo.line)
		}
		fmt.Println("--------------------------------")
		fmt.Println()
	}
}

func GetFileList(path string) ([]string, error) {
	return filepath.Glob(path)
}

func FindWordInAllFiles(word, path string) []FindInfo {
	findInfos := []FindInfo{}

	filelist, err := GetFileList(path) // 파일 리스트 가져오기
	if err != nil {
		fmt.Println("파일 경로가 잘못되었습니다. err:", err, "path:", path)
		return findInfos
	}

	for _, filename := range filelist { // 각 파일별로 검색
		findInfos = append(findInfos, FindWordInFile(word, filename))
	}
	return findInfos
}

func FindWordInFile(word, filename string) FindInfo {
	findInfo := FindInfo{filename, []LineInfo{}}
	file, err := os.Open(filename)
	if err != nil {
		fmt.Println("파일을 찾을 수 없습니다. ", filename)
		return findInfo
	}
	defer file.Close()

	lineNo := 1
	scanner := bufio.NewScanner(file) // 스캐너를 만든다.
	for scanner.Scan() {
		line := scanner.Text()
		if strings.Contains(line, word) { // 한 줄씩 읽으면 단어 포함 여부 검색
			findInfo.lines = append(findInfo.lines, LineInfo{lineNo, line})
		}
		lineNo++
	}
	return findInfo
}
반응형