책 '하룻밤에 읽는 Go 언어 이야기'를 읽고

shinychan95·2021년 6월 30일
0

go-with-Go

목록 보기
4/4
post-thumbnail

💡 Go 언어에 대해서 책을 읽고 정리한 것이다. 등장 배경, 문법, 특장점, 동시성, 예제 기반 등을 담고 있다.

글 쓰는 목적

정리 링크

 

참고 자료

 

시대 요구

  1. 개발 속도 (하드웨어 개발 vs 소프트웨어 개발)
    • 하드웨어 관련 법칙 중에 가장 많이 인용되는 것이 바로 무어의 법칙이다. 하지만 정작 이 하드웨어의 성능을 제대로 발휘하게 하는 소프트웨어의 발전 속도는 더디기만 했다.
    • Go언어는 멀티코어 환경을 고려하여 설계했기 때문에 기존의 개발 언어보다 현재의 하드웨어에 맞게 설계된 편이다.
  2. 모듈화를 위한 의존성
    • C 언어에서 컴파일 시간이 많이 소요되는 이유는 실제 빌드에 필요하지 않은 파일까지 접근해서 읽기 때문이다.
    • C 언어의 컴파일에서 나타나는 의존성은 모듈화를 어렵게 하는 요소다.
    • 의존성 가정 : a.go → b.go → c.go (A → B : A는 B에 의존성을 가진다.)
    • 빌드 순서 : c.go, b.go, a.go
    • Go의 의존성 : a.go를 빌드하는 경우 b.go만 참조한다?
  3. 동적 타입 언어의 속성을 가진 언어
    • Go 언어는 정적 타입 언어이지만 동적 타입 언어 속성을 지원하도록 설계되었다. 커파일 시점에 컴파일러에 의해 지원되는 것이 특징이다.
  4. 가비지 컬렉션
    • Java의 경우 VM에서 가비지 컬렉션을 제공하는 것과 달리 Go 언어는 빌드되어 나온 결과 실행파일에서 이를 지원한다.
  5. 병렬처리
    • 나중에 어떤 방식으로 편리하게 병렬 처리하는지 코드 레벨로 확인

 

구글이 만든 이유

  • 구글에서 제공하는 서비스는 대부분 서버 기반으로 규모가 상당히 큽니다. C나 C++로 작성된 코드의 경우 코드를 수정하고 빌드하려면 수 시간에 걸쳐, 어떤 때에는 10시간이 넘게 전체 빌드하는 경우도 있다고 한다.
    ("리눅스 커널 소스를 직접 빌드하면 3시간이나 걸렸다.")
  • 물리적으로 환경은 구성되었지만 여러 개의 코어를 사용하여 프로그램을 스케줄링하는 것은 별개의 문제입니다. 동시성을 지원하도록 프로그램을 작성해야 하는데, 동시성을 지원하면서 버그가 발생하지 않게 작성하는 것이 어렵기 때문입니다.
    • Go 언어 개발팀은 기본 언어에 라이브러리 차원에서 동시성을 지원하는 경우 한계가 있다고 말합니다.

 

시스템 프로그래밍 언어

  • 어플리케이션은 사용자에게 제공하는 서비스다. 여기에 반해 시스템은 하드웨어나 운영체제에 제공하는 서비스다. 현재 우리가 접할 수 있고 사용해 볼 수 있는 시스템 프로그래밍 언어는 C/C++이다.
  • 이러한 시스템 프로그래밍 언어를 만들기 위해서는 기본적으로 운영체제에 대한 경험이 절대적으로 필요하다.
  • 시스템 프로그래밍 언어는 만들기도 쉽지 않지만 일단 뿌리를 내리면 상당히 긴 시간 동안 유지되는 특성이 있다. 따라서 Go 언어가 시스템 프로그래밍 분야에서 독보적인 개발 언어가 될 가능성이 크다. 하드웨어로는 현재 가장 널리 사용되는 ARM과 x86을 완벽히 지원하고 운영체제로는 윈도우, 리눅스, 맥을 지원한다.
  • 특히 Go 언어의 http 라이브러리는 구글 서버에서도 사용하니 개발자 입장에서 안심하고 사용할 수 있습니다. 적은 코드로 안전한 웹 서버를 구현할 수 있다는 것은 큰 장점이다.
  • 또한 Go 언어가 기술적으로 가장 신뢰를 얻는 부분은 역시 동시성 지원입니다. 혹시 소프트웨어가 하드웨어의 발전을 따라가지 못하다는 말을 들어본 적 있으신가요?
  • 개발 언어마다 라이브러리 형태로 동시성을 지원하는 노력을 기울이고 있습니다. 그러나 해당 언아가 만들어질 당시에는 동시성에 대한 고려가 없었고 버전이 올라가면서 동시성 관련된 기능이 추가된 것으로 언어 자체가 복잡해지고 버그를 동반하기도 합니다.

 

Go 언어의 특징

다른 언어와의 비교

factorio thumbnail 그림 출처 - 하룻밤에 읽는 Go 언어 이야기 (신제용 지음)

  • 주목할 만한 것은, 클래스나 오버로딩이 없다는 것 그리고 포인터는 지원하지만 포인터 연산은 지원하지 않는다는 것이다.
  • 또한 시스템 프로그래밍에서 가장 많은 코딩 부분을 차지하는 것 중에 하나가 메모리 관리다. 이에 있어서 Go 언어는 가비지 컬렉션을 지원한다.
  • Go 언어는 객체지향언어일까? 타입과 메서드로 개체지향 스타일의 프로그래밍을 할 수 있지만 상속은 지원하지 않고 인터페이스라는 개념으로 다른 접근방식을 지원한다. 임베드 타입을 이용하면 서브클래스와 유사한 기능을 제공할 수 있다. 따라서 기존의 객체지향을 완벽하게 지원하지는 않지만 비슷한 효과를 낼 수는 있다.
  • 추가로, 아래와 같이 변수 선언 방식이 바뀐 이유는 영어 그대로 "Variable a is integer".
// C
int a;
int b[10];

// Go
var a int
var b [10]int
  • 클래스 상속에 관련해서는 제임스 고슬링이 "Java를 다시 설계한다면 무엇을 없애고 싶습니까?"라는 질문에 서브클래스를 없애고 싶다는 말을 했었다. 클래스 상속이 문제가 많으니 인터페이스를 이용해야 한다는 뜻이다. 부모 클래스에 의존성이 커져서 확장에 문제가 되며 원하지 않는 속성이나 메서드까지 상속받아야 하는 문제가 발생한다.

 

코드 레벨 Go 언어 특징

  • 세미콜론 → 없음
  • while → for loop
  • 접근 제한자(public, private) → 대소문자
  • 변수 선언 초기화 → Zero Value
  • 인터페이스 → (이전 게시물 코드 기반으로 자세한 정리)
  • switch → (이전 게시물 코드 기반으로 자세한 정리)
  • 함수 → (이전 게시물 코드 기반으로 자세한 정리)
  • Swap → i, j = j, i
  • Slice → (이전 게시물 코드 기반으로 자세한 정리)
  • Defer → (이전 게시물 코드 기반으로 자세한 정리)
  • 선언 후 사용하지 않는 변수 → 에러 발생
  • 암시적 형변환
  • 단위 테스트 → (이전 게시물 코드 기반으로 자세한 정리)

 

병렬성과 동시성

Go 언어는 뼛속까지 동시성 지원을 위한 언어

병렬성,

문제를 여러 연산으로 나누고 그것을 여러 프로세서나 코어 혹은 분산환경에서 동시에 실행하는 형태를 뜻한다. 연산이 일어나는 레벨을 기준으로 한다면 비트, 명령어, 데이터, 마지막으로 태스크 레벨로 분류할 수 있다.

동시성,

여러 연산이 동시에 수행되고 이 연산들이 상호작용이 발생할 수 있는 시스템의 특성을 말합니다. 연산들은 단일 프로세서나 코어에서 시분할 방법으로 동시에 실행될 수도 있고 여러 프로세서나 코어 또는 분산환경에서 동시에 실행될 수도 있습니다.

즉, 동시성은 CPU를 지정하여 연산을 동시에 수행하는 것이 아니라, 동시에 일어나는 일들에 대한 정의와 어떤 리소스를 공유할지에 대한 내용을 기술하는 방식이다.

(스레드 → 동시성 프로그래밍)

프로세스가 여러 개라면 어떤 일이 생길까?

  • 세마포어 (Dijkstra, 1965)
  • 모니터 (Hoare, 1974)
  • 뮤텍스와 락
  • 메시지 전달 (Lauer & Needham, 1979)

 

Go 언어의 동시성

Go 언어에는 동시성을 지원하기 위해 goroutine이 있다. 비동기적으로 함수를 실행하기 위해서 사용하는데, 다른 언어에서는 어떻게 goroutine과 유사한 일을 실행하는지 알아보자.

// Java
class SimpleThread extends Thread {
    public void doSomething() {
        // ...
    }
    public void run() {
        doSomething();
    }
}

public class ThreadTest {
    public static void main(String[] args) {
        new SimpleThread().start();
    }
}

- > Thread를 상속하여 run 메서드를 오버라이드 한다.실제로 호출하는 쪽에서는 해당 스레드 인스턴스를 생성하여 start 메서드를 호출하면 새로 생성한 스레드로 함수 실행할 수 있다.

// C
void * doSomething() {
    // ...
}

int main() {
    pthread_t thread_t;

    if (pthread_create( & thread_t, NULL, doSomething, NULL) < 0) {
        perror( << thread create error >> );
        exit(0);
    }

    return 1;
}

- > pthread를 선언하고 pthread_create를 이용해 실행하는 방법이다.함수의 인자로 실행할 함수 포인터를 전달한다.

// Go
func doSomething() {
    // ...
}

func main() {
    do doSomething()
}

- > 간단하다.
  • goroutine은 위 둘과 유사하지만 다른 속성을 가진다. 동일한 주소 공간에서 다른 goroutine들과 함께 동시에 실행되고, 스레드보다 가벼우며 스택 주소 공간의 할당이 적다.

  • C 프로그램이 실행될 때 주소 공간은 (Stack → Free Memory ← Heap, Static Data, Code) 로 이루어져 있다. C에서 스택은 연속적인 메모리 블록으로 구성되어 있어 새로운 스레드가 생성되면 스레드가 최대한 사용할 만큼의 메모리 블록을 잡게 되는데, 일반적으로 1MB의 스택 영역을 할당받는다. 실제로 몇 KB만 있으면 충분한 태스크도 있지만, 1MB를 받게 된다.

  • Go 언어의 경우 스택을 링크드 리스트로 관리한다. 이는 할당받은 공간이 충반하지 않으면 추가로 스택을 늘려 달라고 요청해 늘리는 방식이다. 또한 goroutine은 매번 커널 스레드를 생성하여 수행하지 않고, 일부 커널 스레드로 멀티플렉싱되어 사용되므로 효율적이다.

멀티플렉싱? (인터럽트로 인한 잠시 다른 공부)

goroutine의 multiplexing 이해 과정

  • Go 언어의 중요 개념인 채널
    • C 스레드에서 사용했던 pthread_join()과 같이 스레드 생성 후 프로세스가 종료되면 스레드가 정상적으로 동작하는지 확인할 수 없다. 이와 관련하여 Go는 채널을 통해 goroutine과 main 함수 상호 간 통신을 가능하게 만든다.
    • 스레드가 공유 메모리 및 lock을 통해 데이터 동기화를 하는 것과 같이 Go 언어는 CSP(Communicating Sequential Process) 개념을 이용한 채널로 두 개의 goroutine 간의 메모리를 공유할 수 있다.
func main() {
    c: = make(chan int)

        go func() {
        // ...
        c < -1 // 1을 채널 c로 보낸다.
    }()

        < -c // 채널 c에서 값을 받아 사용하지 않고 버린다.
    fmt.Printf("main end")
}

Go 언어의 핵심 포인트 중 하나인 channel에 대한 좋은 글을 첨부한다.

Golang 채널 중심 프로그래밍

 

또 중요한 메모리 모델이라는 개념,

  • 메모리 모델이란 스레드와 메모리 간에 어떻게 상호작용이 이루어지는지를 설명하는 것으로, 컴파일러가 어떤 식으로 메모리 관리와 관련된 코드를 생성해 내는지에 대한 일종의 스펙과도 같습니다. Go 언어의 메모리 모델은 두 개의 goroutine이 동일한 메모리에 접근할 때의 상호 작용에 대해 명시하고 있습니다.
  • 메모리 모델이 필요한 이유는 컴파일러가 컴파일하는 동안 성능 최적화를 위해 작성한 코드의 처리 순서를 조정할 때, 논리적 코드 결과와 다른 수행결과를 얻을 수 있기 때문이다.
  • Go 언어에서 제공하는 메모리 모델 관련 자료

 

동시성 예제

책에서 검색 시스템을 예시로 동시성을 설명한다.

factorio thumbnail 그림 출처 - 하룻밤에 읽는 Go 언어 이야기 (신제용 지음)

  • 웹 브라우저에서 검색을 하면, 해당 키워드를 기반으로 Web, Images, Videos에서 검색 결과를 받아 수합하여 사용자에게 전달한다.
  • 이 과정에서 동시성을 코드 기반으로 알아보자.
var {
    Web = fakeSearch("web")
    Image = fakeSearch("image")
    Video = fakeSearch("video")
}

type Search func(query string) Result

func fakeSearch(kind string) Search {
    return func(query string) Result {
        time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)
        return Result(fmt.Sprintf("%s result for %q\n", kind, query))
    }
}

// Google 함수의 순차적 검색 방식
func Google(query string)(results[] Result) {
    results = append(results, Web(query))
    results = append(results, Image(query))
    results = append(results, Video(query))
    return
}

// goroutine을 이용한 검색 방식
func Google(query string)(results[] Result) {
    c: = make(chan Result)
    go func() {
        c < -Web(query)
    }()
    go func() {
        c < -Image(query)
    }()
    go func() {
        c < -Video(query)
    }()

    for i: = 0;i < 3;i++{
        result: = < -c
        results = append(results, result)
    }

    return
}

func main() {
    results: = Google("동시성")
    fmt.Println(results)
}
  • 둘의 차이를 살펴보면, 순차적인 검색 방식은 하나의 항목에 대한 검색이 끝나고 결과가 추가되고 다시 다른 항목에 대한 검색을 하는 방식이다. 따라서 전체 검색에 드는 시간은 각 검색에 드는 시간의 합이 됩니다.
  • goroutine을 사용한 동시성 코드는 goroutine을 이용해 각 함수를 호출합니다. 그리고 결과를 채널 c로 받아서 취합한다. 이때 3번 반복되는 반복문이 실행되게 되는데, 결과적으로 먼저 채널에 입력된 결과부터 더해진다. 즉, 가장 빨리 실행을 완료한 것부터 3번 채널에서 결과를 받아 취합하는 과정이다.

 

추가로 아래와 같이 channel 타입에만 사용되는 switch 구문인 select와 timeout을 더한 예시도 참고하면 좋다.

c := make(chan Result)
go func() { c <- Web(query) } ()
go func() { c <- Image(query) } ()
go func() { c <- Video(query) } ()
timeout := time.After(80 * time.Millisecond)
for i:=0; i<3; i++ {
	select {
		case result := <-c:
			results = append(results, result)
		case <-timeout:
			fmt.Println("time out")
			return
	}
}
return

 

🤦🏻‍♂️하룻밤은 무슨... (아직 멀었구나)

profile
개발자로 일하는 김찬영입니다.

0개의 댓글