Golang 언어에 대한 포스터를 작성하고자 합니다. ‘Tucker의 Go 언어 프로그래밍’이란 책을 통해 학습했던 내용을 정리하고자 합니다. 정리할 내용은 변수 간 값의 전달, 스택과 힙, 고루틴과 컨텍스트, 채널, 클래스와 상속 등이 있습니다. 또한, Go는 컴파일 언어, 강 타입 언어이면서 기본적으로 라이브러리를 모두 정적 링크합니다. 그래서, DLL의 의존 문제를 회피할 수 있고, 런타임 실행 시간도 빠릅니다. 하지만, 정적 링크하기 때문에 파일 크기가 크고, 라이브러리의 수정이 생기면 다시 컴파일 해줘야 한다는 불편함도 있습니다.



1. 변수 간 값의 전달


Go 언어에서 변수 간 값의 전달은 항상 복사로 이루어집니다. 대입 연산자와 함수 호출 시 인수값 전달도 모두 복사 방식으로 처리됩니다. 복사의 크기는 타입의 크기와 동일하며, 배열의 경우에도 개별 요소를 깊은 복사합니다. 다만, 슬라이스, 맵, 채널, 함수 타입 등의 경우 내부적으로 포인터를 포함하고 있어, 복사 시에도 원본 데이터를 참조하는 것처럼 보일 수 있습니다.

메서드 작성 시 리시버를 값 타입과 포인터 타입 중 어떤 방식으로 정의할 것인지에 따라 동작이 달라집니다. 값 타입 리시버를 사용하면 호출 시 구조체의 복사본이 전달되므로 원본 데이터가 변경되지 않습니다. 반면, 포인터 리시버를 사용하면 원본 주소를 직접 참조하므로 내부 데이터를 수정할 수 있습니다.

인터페이스에도 값 타입과 포인터 타입의 차이가 존재합니다. 구조체가 인터페이스를 구현할 때, 값 타입으로 인터페이스를 대입하면 구조체의 복사본이 인터페이스로 전달됩니다. 반면, 포인터 타입으로 대입하면 원본 구조체의 주소가 인터페이스 내부에 저장됩니다. 따라서, 인터페이스 내부에서 메서드를 호출할 때 값 타입일 경우 내부 상태 변경이 불가능하지만, 포인터 타입일 경우 내부 상태 변경이 가능합니다.


2. 스택과 힙


Go 언어에서는 탈출 분석(escape analysis)을 통해 변수가 스택에 할당될지, 힙에 할당될지를 결정합니다. 만약 특정 변수가 함수 내부에서 선언되었지만 함수가 종료된 이후에도 참조가 필요하다면, 해당 변수는 힙에 할당됩니다. 이렇게 힙에 할당된 변수는 가비지 컬렉터(GC)에 의해 관리됩니다.

즉, Go에서는 명시적인 malloc/free를 사용하지 않아도, 함수 내부에서 구조체를 생성한 후 반환할 경우, 탈출 분석에 의해 해당 구조체가 스택이 아닌 힙에 할당될 가능성이 높아집니다. 이를 통해, Go는 메모리 할당을 자동으로 최적화하며 개발자가 직접 메모리 관리를 하지 않아도 됩니다.


3. 고루틴


고루틴은 Go에서 제공하는 경량 스레드로, 운영체제의 커널 스레드보다 훨씬 가벼운 단위로 동작합니다. 고루틴의 가장 큰 장점은 경량성이며, 컨텍스트 스위치 비용이 감소한다는 점입니다.

운영체제에서 일반적인 스레드를 사용할 경우, 컨텍스트 스위치 시 많은 비용이 발생합니다. 하지만 Go의 고루틴은 Go 런타임 스케줄러가 관리하는 사용자 레벨 스레드로 동작하므로, 컨텍스트 스위치 비용이 최소화됩니다. 또한, 고루틴은 필요할 때만 커널 스레드에 매핑되어 실행되므로, CPU 코어를 효율적으로 활용할 수 있습니다.


4. 채널과 컨텍스트


고루틴 간의 동기화 및 데이터 교환을 위해 Go는 채널(channel)을 제공합니다. 채널을 사용하면 여러 고루틴이 데이터를 안전하게 주고받을 수 있으며, 공유된 자원을 직접 접근하지 않도록 유도하여 동시성 문제를 줄일 수 있습니다.

채널의 동작 방식

  • 버퍼링 없는 채널 (Unbuffered Channel): 송신자와 수신자가 동시에 실행되어야 데이터가 전달됩니다. 이를 통해 자연스럽게 동기화가 이루어집니다.
  • 버퍼링 있는 채널 (Buffered Channel): 지정된 크기만큼 데이터를 임시 저장할 수 있어, 송신과 수신이 즉시 이루어지지 않아도 됩니다.

채널을 활용한 동시성 제어 패턴

  • 워커 풀(Worker Pool) 패턴: 여러 개의 고루틴을 생성하고 작업을 분배하여 처리 속도를 높입니다.
  • select 문을 활용한 다중 채널 처리: 여러 개의 채널을 동시에 모니터링하고, 준비된 채널부터 실행합니다.
  • context와 채널을 조합하여 타임아웃 및 취소 관리: 특정 시간 내에 작업을 완료하지 못하면 자동으로 취소되도록 설정할 수 있습니다.

컨텍스트(Context)의 주요 기능

컨텍스트(Context)는 고루틴의 실행을 제어하는데 사용됩니다. 특히, 타임아웃 설정, 취소 신호 전파, 요청 범위 제한 등의 기능을 제공하여, 긴 실행 시간이 필요한 고루틴을 효과적으로 관리할 수 있습니다.

  • context.WithCancel: 부모 컨텍스트가 취소될 때 모든 자식 고루틴에 취소 신호를 전파합니다.
  • context.WithTimeout: 지정된 시간이 지나면 자동으로 취소되도록 설정할 수 있습니다.
  • context.WithDeadline: 특정 시간까지 실행을 보장하고 이후 자동 취소합니다.
  • context.WithValue: 컨텍스트를 통해 키-값 데이터를 전달할 수 있습니다.


5. 클래스와 상속


Go 언어는 클래스와 상속을 지원하지 않습니다. 대신, 인터페이스를 활용하여 객체지향적인 프로그래밍을 할 수 있습니다. Go의 인터페이스는 덕 타이핑(Duck Typing)을 사용하여 명시적인 선언 없이도 특정 메서드를 구현하면 인터페이스를 만족하게 됩니다.

Go에서는 상속 대신 포함(Composition)을 사용하여 객체 간 관계를 정의합니다. 포함 관계를 활용하면, 코드 재사용성을 높이면서도 상속의 단점을 피할 수 있습니다. 또한, 구체적인 구현체보다는 추상적인 인터페이스에 의존하도록(DIP, Dependency Inversion Principle) 설계할 수 있어 유지보수성이 향상됩니다.


6. Go 환경 변수 정리


  • GOROOT: Go가 설치된 경로 (기본적으로 /usr/local/go). Go 내부 도구가 패키지를 인식하는 데 사용됩니다.
  • GOPATH: 설치된 패키지가 저장되는 위치.
  • PATH: go, gofmt 등의 실행 파일이 있는 디렉토리.

Go는 패키지를 가져올 때 기본적으로 GOROOT 내의 표준 라이브러리와 GOPATH 내의 외부 라이브러리를 참조합니다. 여러 개의 Go 버전을 관리할 경우, GOROOT를 적절히 설정하면 각 버전에 맞는 환경을 쉽게 전환할 수 있습니다.


7. 멀티 모듈 사용


Go 1.18 이후부터는 go.work 파일을 활용하여 여러 개의 모듈을 하나의 작업 공간에서 관리할 수 있습니다.

Go에서 패키지를 찾는 방식은 go.mod 파일에 명시된 모듈 이름을 기준으로 이루어집니다. 즉, 임포트 시 go.mod에서 정의한 모듈 이름과 실제 임포트되는 디렉터리에 있는 go.mod 파일의 module 값이 일치해야 합니다. 기본적으로 Go는 이 값을 사용하여 패키지를 찾으며, 이는 파일 경로와 암묵적으로 연결됩니다. 하지만 replace 지시어나 go.work 파일을 사용하면 명시적으로 특정 경로를 지정할 수도 있습니다. 이를 통해 다중 모듈 환경에서 유연한 패키지 관리를 할 수 있습니다.