1. Go언어

1. Go언어

나는 Go언어를 2016년부터 사용해왔다.
중간의 공백을 고려해도 거의 5년 이상을 사용해오면서 남에게 설명할 기회는 많이 없었다.

적어도 내가 Go언어에 대해 잘 알고 있다고 말하려면
다른 사람들에게 설명하는데 문제가 없어야 한다고 생각한다.

그래서 오늘부터 이 블로그에 Go언에 대해 내가 알고 있는 내용들을
글로 정리하며 내가 알고 있는 Go언어에 대한 지식을 점검해보려한다.

If you can’t explain it simply,
you don’t understand it well enough
- Albert Einstein -

간단히 설명할 수 없다면
충분히 이해하지 못한 것이다.
- 알버트 아인슈타인 -

개요

package main

import "fmt"

func main() {
    fmt.Println("Hello, 世界")
}

go.dev/tour에 접속했을 때 가장 먼저 볼 수 있는 예제

Go언어는 2009년 11월에 처음 발표된 후 2012년 3월에 정식 발표된 프로그래밍 언어이다.
개발자 중 켄 톰슨(C언어의 창시자) 이 있어 C의 영향을 많이 받은 흔적이 보인다.

Go언어의 특징

Go언어의 특징을 간단하게 표현하면 다음과 같다.

  1. 컴파일 언어이다.
  2. 정적 타입 언어이다.
  3. 덕 타이핑(Duck typing) 언어이다.
  4. 가비지 컬렉터 (Garbage Collector)를 사용하는 언어이다.
  5. 예약 키워드가 적다.
  6. 병렬 프로그래밍이 쉽다.
  7. C와의 연동성이 좋다.

컴파일 언어 Go

package main

import "fmt"

func main() {
    fmt.Println("Hello, 世界")
}

import 키워드를 통해 패키지를 사용

C, C++의 경우 헤더를 Include 하는 방식으로 라이브러리를 사용할때가 많은데
이는 컴파일 해야하는 소스 코드의 양이 늘어나는 것을 의미한다.
또한, C++ 컴파일러의 경우 더 복잡한 문법과 기능때문에 컴파일러의 속도가 느려졌다.

하지만 Go언어는 헤더가 아닌 미리 컴파일 된 패키지를 사용하며
문법이 간결하도록 설계된 덕에 C, C++ 컴파일러의 비해 컴파일 속도가 매우 빠르다.

다만 Java와 같이 바이트코드를 생성하는 언어와 달리 C언어처럼 바이너리로 컴파일되기 때문에 타겟 머신에 따라 각자 컴파일해야 한다.

정적 타입 언어 Go

타입 검사는 크게 정적(Static Typed) 타입과, 동적(Dynamic Typed) 타입으로 나뉜다.
서로의 우열을 나눌 수는 없지만 나는 컴파일 시점 오류를 미리 방어할 수 있는 정적 타입 언어를 선호한다.

  • 정적 타입 언어는 컴파일 시점에 각 변수의 타입을 결정하는 언어를 말한다.
  • 동적 타입 언어는 실행 시점에 각 변수의 타입을 결정하는 언어를 말한다.

Go는 정적 타입 언어로 컴파일 할 때 각 변수의 타입이 결정된다.
정적 타입 언어는 컴파일 시점에 각 변수의 타입을 결정하므로 타입 에러를 컴파일 시점에 교정할 수 있고, 실행 할 때 타입 점검이 필요 없으므로 실행 속도가 빠르다는 장점이 있다.

또한, 정적 타입 언어인 Go는 각 변수의 타입이 명확하므로 다수의 사람들과 협업을 할 때 더 용이하다.

a = "Hello, World"
print(a)

동적 타입 언어 Python

char a[36] = "Hello, World";
printf("%s\n", a)

정적 타입 언어 C

덕 타이핑 언어 Go

위에서 설명한 정적 타입 말고도 타입과 관련된 Go의 특별한 특징이 있다.
바로 덕 타이핑 (Duck Typing) 이라는 것인데

만약 어떤 새가 오리처럼 걷고, 헤엄치고, 꽥꽥거리는 소리를 낸다면 나는 그 새를 오리라 부르겠다.

덕 타이핑은 Java의 상속과는 다른 개념으로 객체의 타입을 판단한다.
아래의 예제에서 Duck과 Swan은 Bird를 상속받지 않았지만
Fly 메서드가 구현되어 있다는 이유만으로 Bird 타입의 매개변수로 사용할 수 있는 것이다.

package main

import "fmt"

type Bird interface {
    Fly() 
}

type Duck struct{}

func (d Duck) Fly() {
    fmt.Println("duck is flying")
}

type Swan struct{}

func (s Swan) Fly() {
    fmt.Println("swan is flying")
}

func fly(bird Bird) {
    bird.Fly()
}

func main() {
    duck := Duck{}
    swan := Swan{}

    fly(duck)
    fly(swan)
}

덕 타이핑은 상속에 비해 덜 친숙하기 때문에 처음 만나면 어색하고 불편할 수 있지만
복잡한 상속 관계를 단순화 할 수 있기 때문에 코드 작성에 더 집중할 수 있다는 장점도 있다.

덕 타이핑에 대한 것을 모두 설명하기엔 이 글이 너무 길어질 것 같으니
추후에 따로 포스트를 작성하여 더 자세히 다뤄보는것이 좋을 것 같다.

가비지 컬렉터가 있는 Go

가비지 컬렉터는 사용되지 않는 포인터를 정리하여 메모리 누수를 막는 역할을 한다.
C, C++ 같은 언어는 메모리를 할당받아 사용한 후 직접 해제를 해줘야하지만
Go언어, Java, Kotlin과 같이 가비지 컬렉터를 가진 언어는 직접 해제할 필요가 없다.

가비지 컬렉터가 있는 언어 중 가장 익숙한 언어는 Java와 Kotlin일 것이다.
Java와 Kotlin은 바이트 코드로 컴파일 되어 JVM 위에서 동작하며 JVM에 가비지 컬렉터가 내장되어 있다.

하지만 Go는 바이트코드로 컴파일 되는 것이 아닌 바이너리 실행 파일로 컴파일 되기 때문에
소스코드를 컴파일 할때 가비지 컬렉터가 실행 파일에 포함되어 같이 컴파일된다.

가비지 컬렉터가 있다는 것은 메모리 누수걱정 없이 프로그래밍 할 수 있다는 장점을 가지지만
동시에 성능에 어느정도 하락이 생긴다는 것을 의미하기도 한다.
Go언어 자체가 성능이 좋기 때문에 그런 점을 느낄 일이 많진 않지만
정말 조금이라도 성능이 더 좋아야 하는 작업을 하게 된다면 C, C++이 더 좋은 선택지일수도 있겠다.

다만, Go는 GC를 포함하더라도 매우 성능이 좋은 편이며
동시에 GC를 포함하고 있기 때문에 메모리 누수를 프로그래머가 신경쓰지 않아도 되어 더 높은 생산성을 기대할 수 있다.

Go는 Tricolor mark and sweep algorithm 을 사용한다고 한다.
GC에 대한 내용도 추후에 자세히 다룰 기회가 있으면 그때 다루는 것이 좋을 것 같다.

예약 키워드가 적은 Go

Go에는 예약 키워드가 25개가 있다.
C언어는 37개 C++11의 경우 84개에 비하면 매우 적은 수준으로
Go 언어가 익히기 쉬운 이유 중 하나이기도 하다.

break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var

병렬 프로그래밍에 강한 Go

Go에는 Goroutine과 Channel이라는 강력한 무기가 있다.

Goroutine은 쉽게 설명하면 경량화된 쓰레드 라고 할 수 있겠다.
Go언어 자체적으로 관리되는 가상 쓰레드로 OS가 관리하는 쓰레드보다 가볍고 다루기 쉽다.

Channel은 주로 Goroutine간의 통신을 위해 사용되는 파이프라인으로
쉽게 말해서 Message Queue의 역할을 한다고 생각하면 쉽다.

아래 예제는 데이터를 처리하는 고루틴과 처리 결과를 출력하는 고루틴으로 나눠
병렬로 데이터를 처리하는 간단한 예제를 구현해본 것이다.

두 고루틴 사이의 통신을 채널을 통해 쉽게 구현할 수 있다.

package main

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

var wg sync.WaitGroup

func Process(data []int, c chan<- int) {
	defer wg.Done()

	for _, v := range data {
		c <- v * 2
	}
}

func Print(c <-chan int) {
	defer wg.Done()
	for {
		select {
		case v := <-c:
			fmt.Printf("result: %d\n", v)
		case <-time.After(5 * time.Second):
			fmt.Println("timeout")
			return
		}
	}
}

func main() {
	data := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

	c := make(chan int)

	wg.Add(2)
	go Process(data, c)
	go Print(c)
	wg.Wait()
}

C와 매우 친한 Go

Go의 개발자 중 켄 톰슨 은 C언어의 아버지 라고 불린다.
실제로 Go의 많은 부분이 C언어의 영향을 받았으며
CGO를 사용하여 C 코드를 사용할 수 있고 Go로 작성된 프로그램을 C에서 사용할 수도 있다.

다만, Cgo는 별도의 C컴파일러를 세팅했을 때만 사용할 수 있다.

package main

/*
#include <stdio.h>

void hello(char* name) {
	printf("hello, %s\n", name);
}
*/
import "C"

func main() {
	name := "hwangseonu"
	C.hello(C.CString(name))
}

Go로 할 수 있는 것

위의 특징 중 내가 생각하는 가장 Go 스러운 특징은 바로 병렬 프로그래밍 이다.
내가 접했던 모든 언어 중 병렬 프로그래밍이 제일 편하고 강력했다.

특히나 병렬 프로그래밍을 활용하기 용이한 서버 프로그래밍에 제일 유용하게 사용할 수 있다.
당연히 구글도 사용하고 있으며, 드롭박스, 클라우드플레어, 트위치 등에서도 Go언어를 사용하고 있다고 하며
국내에서는 당근마켓, AB180 등의 기업에서도 사용되고 있다고 한다.

그 외에 Docker와 Kubernetes를 작성하는데 사용된 언어로도 유명하다.

마치며

처음에 이 글을 작성하기 시작했을때

10줄은 적을 수 있으려나

고민했는데 막상 적기 시작하니 끝날 줄을 모르고 적게 되었다.
글이 길어지는 것을 막기 위해 설명을 간단하게만 하고 넘어가서 아쉬웠던 부분도 있지만
그런 부분은 다음에 다른 포스팅을 통해 더 자세하게 탐구해보는 시간을 가져보는게
더 나을 것 같다.

results matching ""

    No results matching ""