Go in Action -2 (search)

김민진·2022년 4월 10일
0

go

목록 보기
2/6

프로그램 아키텍처

깃허브의 코드를 참고한다

프로그램의 진입점은 main.go 코드 파일에 작성되어 있다.

코드의 내용은 아래와 같다.

package main

import (
    "log"
    "os"

    _ "github.com/webgenie/go-in-action/chapter2/sample/matchers"
    "github.com/webgenie/go-in-action/chapter2/sample/search"
)

// init 함수는 main 함수보다 먼저 호출된다.
func init() {
    // 표준 출력으로 로그를 출력하도록 변경한다.
    log.SetOutput(os.Stdout)
}

// main 함수는 프로그램의 진입점이다.
func main() {
    // 지정된 검색어로 검색을 수행한다.
    search.Run("Sherlock Holmes")
}

모든 Go 프로그램은 두 가지 독특한 기능을 가진 실행 파일을 만들어낸다.

func main() {
    // 지정된 검색어로 검색을 수행한다.
    search.Run("Sherlock Holmes")
}

이 부분을 통해서 메인 함수를 선언하며 프로그램의 진입점 역할을 수행하기도 한다.

package main

main함수는 main이라는 이름의 패키지에 구현되어 있다.

go의 모든 코드 파일은 패키지에 종속되어야 하며 main.go 파일도 예외는 아니다.

import (
    "log"
    "os"

    _ "github.com/webgenie/go-in-action/chapter2/sample/matchers"
    "github.com/webgenie/go-in-action/chapter2/sample/search"
)

해당 코드는 외부 코드를 가져오는 코드이다.

가져오기는 말 그대로 외부의 코드를 가져와서 그 코드에 정의된 타입,함수,상수 및 인터페이스 같은 식별자들에 접근할 수 있게 해주는 기능이다.

예제의 경우 main.go 파일은 이제 search패키지의 Run 함수를 참조할 수 있게 된다.

"log"
"os"

를 통해서는 표준 라이브러리로부터 log와 os의 패키지를 가져오는 코드다.

    _ "github.com/webgenie/go-in-action/chapter2/sample/matchers"

위의 방법을 통해 패키지를 가져오는 코드 앞에 빈 식별자를 사용한 것을 알 수 있다.
이 방법은 가져온 패키지에 정의된 식별자를 직접 사용하지 않더라도 가져오기 선언을 유지하기 위한 방법이다.

Go 컴파일러는 코드의 가독성을 위해 패키지를 가져왔지만 실제로 그 패키지에 정의된 식별자를 사용하지 않으면 컴파일 오류를 발생한다.

// init 함수는 main 함수보다 먼저 호출된다.
func init() {
    // 표준 출력으로 로그를 출력하도록 변경한다.
    log.SetOutput(os.Stdout)
}

프로그램을 구성하는 코드 파일에 정의된 모든 init 함수는 main 함수가 호출되기 전에 먼저 호출된다. 이 init 함수는 표준 라이브러리로부터 로그 출력기를 가져와 표준 출력(stdout,standard out 의 약자) 장치로 로그를 출력하도록 설정한다.

    search.Run("Sherlock Holmes")

이 코드는 search패키지에 선언된 Run 함수를 호출한다. Run 함수가 리턴되면 프로그램이 종료된다.

Search 패키지

프로그름을 위한 프레임워크와 비지니스 로직을 구현한 패키지다.

package search

import (
    "log"
	"sync"
)

// 검색을 처리할 검색기의 매핑 정보를 저장할 맵(map)
var matchers = make(map[string]Matcher)

제일 첫 번째 줄에는 package라는 키워드와 함께 패키지 이름이 지정되어 있다.
search폴더에 저장된 각각의 코드 파일은 search라는 패키지 이름을 사용한다.

외부 라이브러리 코드를 가져올 때와는 다르게 표준 라이브러리로부터 코드를 가져올 때는 패키지의 이름만 지정하면 된다. 그러면 컴파일러는 GOROOT 및 GOPATH 환경변수 에 정의된 위치를 기준으로 가졍로 패키지를 탐색한다.

GOROOT="/Users/me/go"
GoPATH="/Users/me/spaces/go/projects"

log는 표준 출력,표준 오류 또는 사용자 정의 장치를 통해 로깅 메시지를 출력하는 기능을 제공
sync 패키지는 우리 프로그램이 필요로 하는 고루틴 사이의 동기화를 지원한다.

var matchers = make(map[string]Matcher)

이 변수는 앞으로 구현할 함수들의 외부에 선언된 변수이기 때문에 패키지 수준의 변수로 인식된다.

변수의 이름이 matchers 소문자로 시작한다.

Go에서의 식벽자들은 패키지 외부로 노출이 되는것과 노출이 되지 않는 것으로 구분할 수 있다.
외부로 노출되는 식별자들은 다른 패키지가 해당 패키지를 가져오면 곧바로 접근이 가능 한 식별자들이다.
이런 식별자들의 이름은 대문자로 시작한다. 반면 노출되지 않는 식별자들의 이름은 소문자로 시작하며 이 경우 다른 패키지의 코드가 직접 접근할 수 없다.

또한 이 변수의 선언문을 통해 대입 연산자와 특수한 내장 함수인 make 함수를 이용해 변수를 초기화 하는 방법도 알 수 있다.

public = 대문자 private = 소문자?

초기화

Go 에서는 모든 변수가 제로 값으로 초기화된다.

숫자 타입의 초기화는 0
문자열은 빈 문자열로 초기화
boolean은 false 로 초기화
포인터의 경우 제로 값으로 nil이 사용
참조 타입의 경우 각 기반 타입의 제로값으로 초기화된 데이터 구조 사용
참조 타입으로 선언된 변수의 제로값은 nil

함수 선언 방법

Go 에서는 함수를 선언할 때 ,func 키워드를 사용한 후 함수의 이름과 함수의 매개변수 그리고 리턴값을 차례대로 지정하면 된다.

func Run(searchTerm string) 

Run 함수의 경우 searchTerm이라는 string 타입의 매개변수 하나만을 정의하고 있다.

feeds, err :=RetrieveFeeds()
if err != nul{
	log.Fatal(err)
 }
RetrieveFeeds

함수를 보면 두개의 값을 리턴한다.
첫번째 리턴 값은 feed 타입의 슬라이스다. 슬라이스는 동적 배열을 구현한 참조 타입이며 Go 에서 데이터의 목록을 처리할 때 사용한다.
두번째 리턴 값은 오류다

Go의 함수는 여러 개의 리턴 값을 가질 수 있다.

그래서 Go는 RetrieveFeeds 함수처럼 어떤 값과 오류 값을 함께 리턴하도록 함수를 작성하는 것이 일반적이다.

만일 오류가 발생하면 함수가 리턴한 다른 값들은 절대 믿어서는 안 된다.

feeds, err := RetrieveFeeds()

이 코드에서는 단축 버전의 변수 선언 연산자(:=)를 사용하고 있다.

이 연산자는 변수의 선언과 초기화를 동시에 수행한다.

각각의 변수 타입은 컴파일러가 함수의 리턴 값을 확인한 후에 결정된다.

이 연산자를 통해 선언된 변수는 var 키워드를 통해 선언된 변수와 아무런 차이가 없다.

results := make(chan *Result)

make 내장 함수를 이용해 버퍼가 없는(unbuffered)채널을 생성하고 있다. 이때 앞에서와 마찬가지로 단축 변수 선언 연산자를 통해 make 함수를 호출한 결과를 바탕으로 변수를 선언과 동시에 초기화 하고 있다.

변수를 선언할 때 적용되는 규칙은 다음과 같다
1. 제로 값으로 초기화될 변수를 선언할 때는 var 키워드를 이용하고
2. 함수를 호출이나 다른 초기화로직을 통해 변수를 초기화하는 경우에는 단축 변수 선언 연산자를 사용한다.

Go의 채널은 맵이나 슬라이스와 마찬가지로 참조 타입이지만 다른 타입들과 달리 채널은 고루틴 사이에 데이터 동신에 사용될 특정 타입의 값들을 위한 큐를 구현하고 있다.

var waitGruop sync.WaitGroup

waitGroup.Add(len(feeds))

Go 에서는 메인 함수가 리턴되면 프로그램 자체가 종료된디.

sync 패키지의 WaitGroup을 이용하여 앞으로 실행하게 도리 모든 고루틴들을 추적한다.

WaitGroup은 특정 고루틴이 작업을 완료했는지를 추적할 수 있는 편리한 기능을 제공한다.

WaitGroup은 카운팅 세마포어(couting semaphore)여서 고루틴의 실행이 종료될 때마다 전체 개수를 하나씩 줄여간다.

// 각기 다른 종류의 피드를 처리할 고루틴을 실행한다.
	for _, feed := range feeds {
		// 검색을 위해 검색기를 조회한다.
		matcher, exists := matchers[feed.Type]
		if !exists {
			matcher = matchers["default"]
		}

		// 검색을 실행하기 위해 고루틴을 실행힌다.
		go func(matcher Matcher, feed *Feed) {
			Match(matcher, feed, searchTerm, results)
			waitGroup.Done()
		}(matcher, feed)
	}

range 키워드는 배열,문자열,슬라이스,맵 및 채널과 함께 사용할 숭 ㅣㅆ다.
for range 구문을 이용해 슬라이스를 탐색하면 슬라이스 내의 각 요소마다 두 개의 값을 돌려받는다.
첫번째 값은 현재 탐색 중인 요소의 인덱스
두 번째 값은 탐색 중인 요소의 복사본이다.

for _, feed := range feeds {

를 보면 빈 식별자를 사용하고 있음을 알 수 있다. 정의된 식별자를 사용하지 않으면 컴파일 오류가 발생하지만 빈 식별자를 사용하면 컴파일러는 패키지를 가져온 후 패키지 내의 다른 코드 파일에서 init 함수를 찾아 호출한다.

이번에는 range 키워드를 호출할 때 인덱스 값이 대입될 변수를 대체하기 위해 사용되었다.

여러개의 리턴 값을 가지는 함수를 호출할 때 그중 필요하지 않은 리턴 값이 있으면 빈 식별자를 이용해 특정 리턴 값을 무시할 수 있다.

// 각기 다른 종류의 피드를 처리할 고루틴을 실행한다.
	for _, feed := range feeds {
		// 검색을 위해 검색기를 조회한다.
		matcher, exists := matchers[feed.Type]
		if !exists {
			matcher = matchers["default"]
		}
for _, feed := range feeds {

이 부분을 통해 맵에서 피드 타입에 일치하는 키가 존재하는지 확인한다.
맵에서 키를 조회할 때는 리턴값을 대입할 변수를 하나만 선언해도 되고 두 개를 선언해도 된다.

이 경우 첫 번째 리턴 값(matcher)은 검색된 키에 해당하는 값이며 두 번째 리턴 값(exists)은 키가 존재하는지를 표현하는 불리언 값이다.

// 검색을 실행하기 위해 고루틴을 실행힌다.
		go func(matcher Matcher, feed *Feed) {
			Match(matcher, feed, searchTerm, results)
			waitGroup.Done()
		}(matcher, feed)

고루틴은 프로그램 내의 함수와는 독립적으로 실행되는 함수 이다.

고루틴을 실행하고 동시적 실행을 위한 스케줄링을 시도할 때는 go 키워드를 사용하면 된다.

go func(matcher Matcher, feed *Feed) {

이 부분을 보면 go 키워드를 이용해 익명함수 를 고루틴으로서 실행하고 있다.

포인터 변수를 통하면 함수 간에 변수를 쉽게 공유할 수 있다. 즉 여러 함수들이 다른 함수나 다른 고루틴에 선언된 변수의 상태에 접근하거나 변경하는 것이 가능하다는 뜻이다.

Match(matcher, feed, searchTerm, results)
			waitGroup.Done()

이 고루틴이 수행하는 첫 번째 작업은 match.go 파일에서 선언한 Match 함수를 호출하는 것이다.
Match 함수 호출이 완료되면 waitGroup.Done() 메서드를 호출하면 우리 프로그램은 각각의 피드가 처리되고 있음을 알 수 있다.

Done 메서드는 WaitGroup값이 실제로 익명 함수에 매개변수로 전달된 적이 없음에도 불구하고 익명함수 내에서 사용되고 있다는 점이다.

이는 Go가 클로저를 지원하기 때문에 가능한 일이다.

profile
dart,c#,java 개발자! 잡다하게 해서 문제될게 없다!

0개의 댓글