본문으로 건너뛰기
Previous
Next
C++ 개발자를 위한 2주 완성 Go 언어(Golang) 마스터 커리큘럼

C++ 개발자를 위한 2주 완성 Go 언어(Golang) 마스터 커리큘럼

C++ 개발자를 위한 2주 완성 Go 언어(Golang) 마스터 커리큘럼

이 글의 핵심

C++ 개발자를 위한 2주 완성 Go 커리큘럼 안내와 Day별 요약, 그리고 멘탈 모델 매핑·M:N 스케줄러·채널·인터페이스 내부·프로덕션 마이그레이션까지 연결한 심화 해설을 담았습니다.

시리즈 안내

📚 Go 2주 완성 시리즈 - 커리큘럼 메인 | 전체 목차 보기

이 글은 전체 커리큘럼 개요입니다. 각 Day별 상세 내용은 아래 링크를 참고하세요.

시작하기: #01 기본 문법 →


들어가며: 복잡함에서 심플함으로

C++의 강력함은 그대로 유지하면서, 복잡한 빌드 시스템과 포인터 연산의 피로도에서 벗어나고 싶으신가요? 심플한 문법과 강력한 동시성(Concurrency) 처리로 클라우드 네이티브 시대의 대세가 된 Go 언어. 기존 C/C++ 지식을 레버리지하여 단 2주 만에 Go 언어의 핵심을 마스터할 수 있는 pkglog 독점 커리큘럼을 소개합니다. 이 커리큘럼의 특징:

  • C++과의 직접 비교: 매 단계마다 C++ 코드와 Go 코드를 나란히 비교
  • 실무 중심: 이론보다는 바로 적용 가능한 패턴과 예제
  • 14일 집중 코스: 하루 2–3시간 투자로 완성하는 체계적 학습 경로
  • 최종 프로젝트: REST API 서버 구축으로 모든 개념 통합 관련 글: C++ 개발자의 뇌 구조로 이해하는 Go 언어, C++ vs Go 성능·동시성.

실무에서 겪은 문제

C++로는 빌드·링크·플랫폼별 분기까지 신경 쓰다 보면, “작은 서비스 하나”에도 설정 파일과 스크립트가 불어납니다. Go로 옮기면 단일 정적 바이너리, 일관된 포맷(gofmt), 동시성 기본기(고루틴·채널) 덕분에 배포·운영 단계가 단순해지는 경우가 많습니다. 반대로 저수준 제어·템플릿 메타프로그래밍이 필요하면 여전히 C++가 맞는 영역입니다. 전환 시 흔한 시행착오:

  • 에러 처리: if err != nil을 빠뜨려 조용히 실패하는 코드 — #05에서 패턴을 익히면 줄일 수 있습니다.
  • 동시성: 뮤텍스만 익숙하면 채널·select를 과소평가하기 쉽습니다 — #06을 권장합니다.
  • 종료 처리: HTTP 서버를 끌 때 Shutdown 없이 프로세스만 죽이면 연결이 끊깁니다 — #09에서 정리합니다. 아래 커리큘럼을 순서대로 밟으면 위 함정을 줄이는 데 도움이 됩니다.

심화: C++ 관점에서 보는 Go 내부 구조와 실무 전환

문법을 넘어서 런타임이 고루틴·채널·인터페이스를 어떻게 구현하는지를 C++의 직관(스레드, 가상 함수 테이블, 뮤텍스)과 맞춰 보면, 이후 성능 튜닝이나 대규모 마이그레이션에서 “왜 이런 현상이 생기는가”를 설명할 수 있습니다. 아래는 커리큘럼의 개념 지도를 내부 구현과 실무 패턴까지 연결한 심화 요약입니다.

1) C++에서 Go로의 멘탈 모델 매핑

C++에서 익숙한 개념Go에서의 대응한 줄 직관
std::unique_ptr / 소유권GC + 필요 시 포인터·슬라이스 공유수명은 런타임이 추적하고, 공유 의도는 타입 설계로 드러냄
RAII, 소멸자defer, Close, sync.Pool(재사용)종료·해제는 호출 경로가 보이는 쪽에 둠(암시적 소멸자 없음)
const/constexpr컴파일 타임 상수 vs 런타임 불변(관용)“불변 보장”은 언어가 강제하지 않고 API·리시버로 표현
예외 스택 풀어헤치기error 값 + fmt.Errorf 래핑제어 흐름이 호출자에게 명시적으로 전달됨
가상 함수·상속임베딩 + 인터페이스(암시적 구현)“is-a”보다 작은 계약(인터페이스) 조합
std::thread고루틴 + 스케줄러OS 스레드 수와 고루틴 수를 직결하지 않음
템플릿·Concepts제네릭(1.18~) + 작은 인터페이스컴파일 타임 다형성과 런타임 다형성을 분리해서 선택

실무적으로 중요한 전환점은 다음과 같습니다. 첫째, 성능 최적화의 레버가 “인라인·템플릿 특화”에서 할당 줄이기·락 경합 줄이기·GC 압력 완화로 이동한다는 점입니다. 둘째, 동시성 버그는 데이터 레이스 탐지기(go test -race)와 스케줄러 특성(아래 M:N)을 함께 봐야 재현이 쉬워집니다.

2) 고루틴 스케줄러 내부(M:N) — G, M, P

Go 런타임은 수많은 고루틴 G를 소수의 OS 스레드 M에 올려 실행하되, 그 사이에 논리 CPU 단위 P(프로세서 슬롯)를 둡니다. 요약하면 G는 실행 단위, M은 실제로 CPU를 붙잡는 스레드, P는 “지금 이 M이 실행할 수 있는 G의 큐와 자원 슬롯”에 가깝습니다.

  • M:N 매핑: 많은 G가 적은 M에 스케줄되며, GOMAXPROCS(기본은 논리 CPU 수)가 동시에 실행될 P의 개수를 조절합니다. C++에서 스레드 풀 크기를 정하는 것과 비슷하지만, 고루틴 생성 비용은 풀에 넣기 전 단계에서 이미 매우 낮습니다.
  • 런 큐와 워크 스틸링(work stealing): 각 P는 로컬 큐를 가지고, 한쪽이 바쁘고 다른 쪽이 한가하면 다른 P의 큐에서 G를 가져옵니다. 이는 std::deque를 둘러싼 수동 부하 분산을 떠올리게 하지만, Go에서는 런타임이 이동·균형을 담당합니다.
  • 선점(preemption): 오래 CPU만 점유하는 G는 Go 1.14 이후 비협조적 선점으로 다른 G에게 양보할 수 있습니다. “협조적 스케줄링만 있다”는 옛날 설명은 이제 참고용으로만 두는 것이 좋습니다.
  • 시스템 콜·블로킹: M이 커널에서 오래 막히면 P는 다른 M에 붙거나 G가 다른 실행 경로로 옮겨질 수 있습니다. 많은 블로킹 시스템 콜은 고루틴을 “가볍게” 만들어 주는 이점을 일부 상쇄합니다.
  • 네트워크 폴러: net의 다수 소켓 I/O는 커널의 이벤트 알림과 결합되어, 수만 개의 대기 고루틴을 OS 스레드 한 개씩 붙잡아 두지 않도록 설계되어 있습니다. C++의 epoll/IOCP 루프를 직접 짜던 경험이 있다면 “런타임이 그 루프를 숨긴다”고 이해하면 됩니다.

프로파일링 힌트: CPU 프로파일은 pprof로, 뮤텍스·블로킹은 런타임 trace로 보면 G가 어디서 멈췄는지(락, 시스템 콜, GC STW 등)가 분리되어 보입니다.

3) 채널 구현을 한 번에 이해하기

채널은 언어 문법처럼 보이지만, 런타임에서는 락·대기 큐·버퍼 링으로 구현된 hchan에 가깝습니다. C++ 개발자에게는 “조건 변수 + 큐 + (필요 시) 링 버퍼를 하나로 묶은 동기화 프리미티브”로 비유할 수 있습니다.

  • 무버퍼 채널: 송신 G와 수신 G가 서로를 만날 때까지 대기합니다. 단순히 “빠르다/느리다”가 아니라 동기화 지점이 명확해져서, 레이스를 줄이는 데 유리합니다.
  • 버퍼 채널: 정해진 슬롯이 차기 전까지 송신이 블로킹되지 않을 수 있습니다. 백프레셔(backpressure)를 코드로 표현하기 좋습니다.
  • 대기열(대기 고루틴): 채널 연산에 막힌 G는 런타임이 관리하는 대기 구조로 들어갑니다. select는 여러 채널의 준비 상태를 동시에 검사하고, 여러 케이스가 동시에 준비되면 무작위 선택으로 공정성을 확보하는 방식이 대표적입니다(세부는 버전마다 다르지만 “공정성·기아 방지”가 목표).
  • close의 의미: 더 이상 송신할 값이 없음을 알립니다. 수신은 남은 값을 소진한 뒤 제로 값과 false를 받습니다. 닫힌 채널에 송신하면 패닉에 가깝게 다루어지므로, “누가 닫는가”를 팀 규약으로 정하는 것이 안전합니다.

실무 규칙: “채널은 소유자가 닫는다”, “에러는 별도 채널/값으로 전달한다”, “버퍼 크기는 부하 실험으로 정한다”를 #06의 예제와 함께 몸에 익히면, 내부 구현을 몰라도 올바른 패턴에 수렴합니다.

4) 인터페이스 타입 시스템 vs C++ 다형성·Concepts

C++의 가상 함수는 컴파일 시점에 상속 관계가 선언되고, 객체 안에 vptr이 있습니다. Go 인터페이스는 메서드 집합을 만족하면 자동으로 해당 인터페이스가 되며, 런타임에 이타블(itab)과 원본 값/포인터가 짝을 이룹니다.

  • 정적 타입 vs 동적 만족: 구체 타입 T가 컴파일 타임에 Stringer인지 “표기”하지 않아도, 메서드가 있으면 됩니다. C++20 Concepts의 “요구 사항을 만족하는 타입”과 정서가 비슷하지만, Go는 암시적입니다.
  • 값 vs 포인터 수신자: 포인터 수신자 메서드가 있는 타입은 값 T를 인터페이스에 넣을 때 주소가 취해지는 규칙이 있습니다. “왜 갑자기 힙으로 간다” 같은 이슈를 볼 때 이 규칙이 단서가 됩니다.
  • 빈 인터페이스 any: 모든 타입을 담을 수 있지만, C++의 void*처럼 타입 정보가 사라지는 것은 아니고, 런타임에 타입·값 쌍으로 보관됩니다. 단언(.(T)), switch type으로 되돌립니다.
  • 작은 인터페이스: 표준 라이브러리의 io.Reader처럼 메서드 하나인 계약이 많습니다. 이는 C++에서 거대한 추상 기본 클래스를 두는 스타일과 대비됩니다.

인터페이스는 “문법상 편의”가 아니라 의존성 역전을 저비용으로 만드는 장치입니다. 테스트에서 fake를 주입하기 쉬운 이유도 여기에 있습니다.

5) 프로덕션 마이그레이션 패턴(C++ → Go)

한 번에 전부 교체하는 빅뱅 포팅은 리스크가 큽니다. 아래는 현장에서 자주 쓰는 접합 전략입니다.

  1. 스트랭글러 피그(Strangler Fig): 새 기능·새 경로만 Go로 열고, 기존 C++ 바이너리는 점진적으로 줄입니다. API 게이트웨이나 라우터 뒤에서 트래픽 비율을 조절합니다.
  2. 프로세스 경계 분리: gRPC/HTTP + Protobuf/JSON 스키마로 계약을 고정합니다. C++와 Go가 같은 IDL을 공유하면 팀 간 충돌이 줄어듭니다.
  3. 사이드카·에이전트: 데이터 플레인은 C++, 제어·운영·관측은 Go로 두는 식의 역할 분담도 흔합니다. FFI로 한 프로세스 안에서 붙이기보다 네트워크 경계가 테스트·배포를 단순하게 만드는 경우가 많습니다.
  4. 관측 가능성 먼저: 구조화 로깅, 메트릭, 트레이싱을 양쪽 스택에 동일한 스키마로 넣어야, 포팅 후 지연·에러가 회귀인지 개선인지를 판별할 수 있습니다.
  5. 테스트 동등성: 골든 파일, 프로퍼티 기반 테스트, 부하 테스트를 동일 입력·동일 SLO로 맞춥니다. GC나 스케줄링 차이로 지연 분포가 달라질 수 있음을 전제합니다.
  6. 팀 운영: 린트(staticcheck 등), go test -race, CI 캐시, 모듈 미러 정책을 레포 초기에 고정합니다. C++의 복잡한 툴체인 대신 관행이 곧 품질이 됩니다.

이 심화 내용은 앞선 Day별 글을 읽은 뒤 “왜 이렇게 설계되었나?”를 연결하는 용도로 두었습니다. 세부 구현은 Go 버전에 따라 달라질 수 있으므로, 문제가 생기면 해당 버전의 런타임 소스·릴리스 노트를 함께 확인하는 습관을 권합니다.


시리즈 전체 글 목록

1주 차:

  • [Go 2주 완성 #01] Day 1~2: Go 언어의 철학과 기본 문법
  • [Go 2주 완성 #02] Day 3~4: 메모리와 자료구조
  • [Go 2주 완성 #03] Day 5~6: 클래스 없는 객체지향
  • [Go 2주 완성 #04] Day 7: 다형성의 재해석, 인터페이스 2주 차:
  • [Go 2주 완성 #05] Day 8~9: 예외 처리의 새로운 접근
  • [Go 2주 완성 #06] Day 10~11: 고루틴과 채널
  • [Go 2주 완성 #07] Day 12~13: 의존성 관리와 테스팅
  • [Go 2주 완성 #08] Day 14: 실전 미니 프로젝트 실무 심화(2주 이후 권장):
  • [Go 심화 #09] context.Context·타임아웃·우아한 종료

📌 1주 차: 패러다임의 전환과 기본기 다지기

C++의 잔재(상속, 예외 처리 등)를 덜어내고, Go 언어 특유의 심플함에 적응하는 주간입니다.

flowchart LR
    A["Day 1-2br/기본 문법"] --> B["Day 3-4br/메모리·자료구조"]
    B --> C["Day 5-6br/객체지향"]
    C --> D["Day 7br/인터페이스"]
    style A fill:#e1f5ff
    style B fill:#e1f5ff
    style C fill:#e1f5ff
    style D fill:#e1f5ff

Day 1~2: Go 언어의 철학과 기본 문법

📖 상세 글 보기: [Go 2주 완성 #01] Day 1~2: Go 언어의 철학과 기본 문법 주제: C++ 개발자의 시선에서 본 Go 언어의 첫인상 핵심 내용:

  • Go 설치 및 툴체인(go build, go run, go fmt) 소개
  • 변수 선언의 차이 (auto vs :=)
  • C++의 while을 대체하는 강력한 for
  • 가비지 컬렉터(GC)의 도입: newdelete로부터의 해방 C++ vs Go 비교: 변수 선언
// C++: 변수 선언
int x = 10;
auto y = 20;  // 타입 추론
const int MAX = 100;
std::vector<int> vec = {1, 2, 3};
// Go: 변수 선언
var x int = 10
y := 20  // 짧은 선언 (타입 추론)
const MAX = 100
// 여러 변수 동시 선언
var (
    name string = "Go"
    version int = 1
)
slice := []int{1, 2, 3}

C++ vs Go 비교: 반복문

// C++: 다양한 반복문
for (int i = 0; i < 10; i++) {
    std::cout << i << "\n";
}
while (condition) {
    // ...
}
for (const auto& item : container) {
    // ...
}
// Go: for 하나로 모든 반복 처리
for i := 0; i < 10; i++ {
    fmt.Println(i)
}
// while 대신 for (조건만)
for condition {
    // ...
}
// range로 컨테이너 순회
for i, item := range slice {
    fmt.Println(i, item)
}
// 무한 루프
for {
    // break로 탈출
}

학습 포인트:

  • Go는 while, do-while이 없습니다. for만으로 모든 반복을 처리합니다.
  • :=는 함수 내에서만 사용 가능하며, 타입을 자동 추론합니다.
  • go fmt는 코드 포맷을 자동으로 맞춰주므로, 팀 내 코딩 스타일 논쟁이 사라집니다.

Day 3~4: 메모리와 자료구조 (포인터, 배열, 슬라이스)

📖 상세 글 보기: [Go 2주 완성 #02] Day 3~4: 메모리와 자료구조 주제: 포인터 연산은 없지만 포인터는 있다? 핵심 내용:

  • Go의 포인터(*, &)와 Call by Value / Call by Reference
  • C++ std::vector와 Go Slice의 결정적 차이 (Capacity와 Length의 이해)
  • Map 구조의 활용 C++ vs Go 비교: 포인터
// C++: 포인터와 참조
void increment(int* p) {
    (*p)++;
}
void increment_ref(int& r) {
    r++;
}
int main() {
    int x = 10;
    increment(&x);      // 포인터
    increment_ref(x);   // 참조
    
    int* p = new int(42);
    delete p;  // 수동 해제
}
// Go: 포인터 (연산 없음, GC 자동 해제)
// 함수 정의 및 구현
func increment(p *int) {
    *p++  // 포인터 연산 없음, 역참조만 가능
}
func main() {
    x := 10
    increment(&x)
    
    p := new(int)  // GC가 자동 해제
    *p = 42
    // delete 불필요
}

핵심 차이점:

  • Go는 포인터 연산(p++, p + 1)이 불가능합니다. 안전성을 위해 제거되었습니다.
  • 참조(&)는 없고, 포인터만 있습니다. 함수 인자로 수정이 필요하면 포인터를 전달합니다.
  • new로 할당해도 delete 불필요. GC가 자동으로 수거합니다. C++ vs Go 비교: 동적 배열
// C++: std::vector
#include <vector>
#include <iostream>
int main() {
    std::vector<int> vec;
    vec.push_back(1);
    vec.push_back(2);
    vec.push_back(3);
    
    std::cout << "Size: " << vec.size() << "\n";
    std::cout << "Capacity: " << vec.capacity() << "\n";
    
    // 범위 기반 for
    for (const auto& v : vec) {
        std::cout << v << " ";
    }
}
// Go: Slice (동적 배열)
// 함수 정의 및 구현
func main() {
    var slice []int  // nil 슬라이스
    slice = append(slice, 1)
    slice = append(slice, 2)
    slice = append(slice, 3)
    
    fmt.Println("Length:", len(slice))
    fmt.Println("Capacity:", cap(slice))
    
    // range로 순회
    for i, v := range slice {
        fmt.Println(i, v)
    }
    
    // 슬라이싱
    sub := slice[1:3]  // [2, 3]
}

Slice의 핵심 개념:

  • Length: 현재 원소 개수 (len(slice))
  • Capacity: 재할당 없이 담을 수 있는 최대 개수 (cap(slice))
  • append로 추가 시 capacity를 초과하면 자동으로 재할당(보통 2배)
  • 슬라이싱(slice[1:3])은 원본 배열을 공유하므로 주의 필요 C++ vs Go 비교: Map
// C++: std::map / std::unordered_map
#include <unordered_map>
int main() {
    std::unordered_map<std::string, int> m;
    m[key1] = 100;
    m[key2] = 200;
    
    // 키 존재 확인
    if (m.find("key1") != m.end()) {
        std::cout << "Found: " << m[key1] << "\n";
    }
}
// Go: map
func main() {
    m := make(map[string]int)
    m[key1] = 100
    m[key2] = 200
    
    // 키 존재 확인 (두 번째 반환값)
    if v, ok := m[key1]; ok {
        fmt.Println("Found:", v)
    }
    
    // 키 삭제
    delete(m, "key1")
}

Day 5~6: 클래스(Class) 없는 객체지향 프로그래밍

📖 상세 글 보기: [Go 2주 완성 #03] Day 5~6: 클래스 없는 객체지향 주제: 상속(Inheritance)을 버리고 합성(Composition)을 취하다 핵심 내용:

  • struct의 정의와 활용
  • 메서드(Method)와 리시버(Receiver)의 개념 (포인터 리시버 vs 값 리시버)
  • 객체의 합성(Embedding)을 통한 코드 재사용 패턴 C++ vs Go 비교: 클래스와 메서드
// C++: 클래스 기반 객체지향
class Counter {
private:
    int count;
    
public:
    Counter() : count(0) {}
    
    void Increment() {
        count++;
    }
    
    int GetCount() const {
        return count;
    }
    
    void Reset() {
        count = 0;
    }
};
// 사용
Counter c;
c.Increment();
std::cout << c.GetCount() << "\n";
// Go: 구조체 + 메서드
// 타입 정의
type Counter struct {
    count int  // 소문자 = private (패키지 외부 접근 불가)
}
// 생성자 관례 (NewXxx 함수)
func NewCounter() *Counter {
    return &Counter{count: 0}
}
// 포인터 리시버 - 필드 수정
func (c *Counter) Increment() {
    c.count++
}
// 값 리시버 - 읽기 전용
func (c Counter) GetCount() int {
    return c.count
}
// 포인터 리시버 - 필드 수정
func (c *Counter) Reset() {
    c.count = 0
}
// 사용
c := NewCounter()
c.Increment()
fmt.Println(c.GetCount())

리시버 선택 가이드:

  • 포인터 리시버 (c *Counter): 필드를 수정하거나, 구조체가 큰 경우
  • 값 리시버 (c Counter): 읽기 전용이고, 구조체가 작은 경우
  • 일관성: 한 타입의 메서드는 모두 포인터 또는 모두 값 리시버로 통일하는 것이 관례 C++ vs Go 비교: 상속 vs 합성
// C++: 상속 기반 재사용
class Animal {
public:
    virtual void Speak() {
        std::cout << "Some sound\n";
    }
};
class Dog : public Animal {
public:
    void Speak() override {
        std::cout << "Woof!\n";
    }
    
    void Fetch() {
        std::cout << "Fetching...\n";
    }
};
// 사용
Dog d;
d.Speak();   // "Woof!"
d.Fetch();
// Go: 합성(Embedding) 기반 재사용
// 타입 정의
type Animal struct {
    Name string
}
func (a Animal) Speak() {
    fmt.Println("Some sound")
}
type Dog struct {
    Animal  // 임베딩 - Animal의 메서드가 Dog에 포함됨
    Breed string
}
// Dog의 고유 메서드
func (d Dog) Fetch() {
    fmt.Println("Fetching...")
}
// Animal.Speak 오버라이드
func (d Dog) Speak() {
    fmt.Println("Woof!")
}
// 사용
d := Dog{
    Animal: Animal{Name: "Buddy"},
    Breed:  "Golden Retriever",
}
d.Speak()  // "Woof!" (오버라이드됨)
d.Fetch()

핵심 차이점:

  • Go는 상속이 없습니다. 대신 구조체 임베딩으로 메서드를 재사용합니다.
  • 임베딩된 타입의 메서드가 자동으로 외부 타입에 “승격”됩니다.
  • 같은 이름의 메서드를 정의하면 오버라이드 효과를 낼 수 있습니다.

Day 7: 다형성의 재해석, 인터페이스(Interface)

📖 상세 글 보기: [Go 2주 완성 #04] Day 7: 다형성의 재해석, 인터페이스 주제: 가상 함수(Virtual Function) 없이 다형성 구현하기 핵심 내용:

  • implements 키워드가 없는 암시적 인터페이스 (Duck Typing)
  • 빈 인터페이스 interface{}와 타입 단언(Type Assertion)
  • Go 라이브러리에서 흔히 쓰이는 소형 인터페이스(예: io.Reader, io.Writer) 설계 패턴 C++ vs Go 비교: 다형성
// C++: 가상 함수로 다형성
class Shape {
public:
    virtual double Area() const = 0;
    virtual ~Shape() = default;
};
class Circle : public Shape {
    double radius;
public:
    Circle(double r) : radius(r) {}
    
    double Area() const override {
        return 3.14159 * radius * radius;
    }
};
class Rectangle : public Shape {
    double width, height;
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    
    double Area() const override {
        return width * height;
    }
};
// 다형성 사용
void printArea(const Shape& s) {
    std::cout << "Area: " << s.Area() << "\n";
}
int main() {
    Circle c(5.0);
    Rectangle r(4.0, 6.0);
    printArea(c);
    printArea(r);
}
// Go: 인터페이스로 다형성 (명시적 상속 불필요)
// 타입 정의
type Shape interface {
    Area() float64
}
type Circle struct {
    Radius float64
}
// Circle은 Area()를 구현하므로 자동으로 Shape 인터페이스 만족
func (c Circle) Area() float64 {
    return 3.14159 * c.Radius * c.Radius
}
type Rectangle struct {
    Width, Height float64
}
// Rectangle도 Area()를 구현하므로 Shape 인터페이스 만족
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}
// 다형성 사용
func printArea(s Shape) {
    fmt.Printf("Area: %.2f\n", s.Area())
}
func main() {
    c := Circle{Radius: 5.0}
    r := Rectangle{Width: 4.0, Height: 6.0}
    printArea(c)
    printArea(r)
}

핵심 차이점:

  • Go는 명시적 상속 선언이 없습니다. 메서드만 구현하면 자동으로 인터페이스를 만족합니다.
  • “Duck Typing”: “오리처럼 걷고 오리처럼 운다면 오리다”
  • 인터페이스는 작게 만드는 것이 Go의 철학입니다. (예: io.ReaderRead 메서드 하나만) 표준 라이브러리 인터페이스 예시
// Go: io.Reader 인터페이스 (메서드 하나)
type Reader interface {
    Read(p []byte) (n int, err error)
}
// *os.File, *bytes.Buffer, *strings.Reader 등이 모두 io.Reader 만족
func processData(r io.Reader) {
    data, err := io.ReadAll(r)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(string(data))
}
// 사용
f, _ := os.Open("file.txt")
defer f.Close()
processData(f)  // *os.File은 io.Reader
buf := bytes.NewBufferString("hello")
processData(buf)  // *bytes.Buffer도 io.Reader

빈 인터페이스와 타입 단언

// Go: interface{} (모든 타입 수용, C++의 void*와 유사하지만 타입 안전)
func printAny(v interface{}) {
    // 타입 단언
    if s, ok := v.(string); ok {
        fmt.Println("String:", s)
    } else if i, ok := v.(int); ok {
        fmt.Println("Int:", i)
    }
}
// 타입 스위치
func describe(v interface{}) {
    switch t := v.(type) {
    case string:
        fmt.Printf("String: %s\n", t)
    case int:
        fmt.Printf("Int: %d\n", t)
    default:
        fmt.Printf("Unknown type: %T\n", t)
    }
}

📌 2주 차: Go 언어의 꽃, 동시성

일상 비유로 이해하기: 동시성은 주방에서 여러 요리를 동시에 하는 것과 비슷합니다. 한 명의 요리사(싱글 스레드)가 국을 끓이다가 불을 줄이고, 그 사이에 야채를 썰고, 다시 국을 확인하는 식이죠. 반면 병렬성은 요리사 여러 명(멀티 스레드)이 각자 다른 요리를 동시에 만드는 겁니다.

2주 차에서는 동시성과 실전 에코시스템을 다룹니다. OS 스레드나 Mutex로 고통받던 과거를 뒤로하고, 우아한 동시성 제어와 실무 적용법을 배웁니다.

flowchart LR
    A["Day 8-9br/에러 처리"] --> B["Day 10-11br/고루틴·채널"]
    B --> C["Day 12-13br/의존성·테스팅"]
    C --> D["Day 14br/실전 프로젝트"]
    style A fill:#fff4e1
    style B fill:#fff4e1
    style C fill:#fff4e1
    style D fill:#ffcccc

Day 8~9: 예외(Exception) 처리의 새로운 접근

📖 상세 글 보기: [Go 2주 완성 #05] Day 8~9: 예외 처리의 새로운 접근 주제: try-catch는 잊어라, Go의 명시적 에러 핸들링 핵심 내용:

  • 다중 반환값(Multiple Return Values)을 활용한 에러 전달
  • if err != nil 패턴의 철학
  • 자원 해제를 보장하는 마법의 키워드 defer (C++의 RAII 패턴 대체)
  • panicrecover (왜 일반적인 예외 처리로 쓰면 안 되는가?) C++ vs Go 비교: 예외 처리
// C++: try-catch 예외 처리
#include <stdexcept>
#include <fstream>
void processFile(const std::string& path) {
    std::ifstream file(path);
    if (!file) {
        throw std::runtime_error("Failed to open file");
    }
    
    std::string line;
    while (std::getline(file, line)) {
        if (line.empty()) {
            throw std::invalid_argument("Empty line");
        }
        // 처리...
    }
    // RAII로 자동 닫힘
}
int main() {
    try {
        processFile("data.txt");
    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << "\n";
        return 1;
    }
    return 0;
}
// Go: error 반환과 명시적 처리
func processFile(path string) error {
    file, err := os.Open(path)
    if err != nil {
        return fmt.Errorf("failed to open file: %w", err)
    }
    defer file.Close()  // defer로 자원 해제 보장
    
    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        line := scanner.Text()
        if line == "" {
            return fmt.Errorf("empty line found")
        }
        // 처리...
    }
    
    if err := scanner.Err(); err != nil {
        return fmt.Errorf("scan error: %w", err)
    }
    return nil
}
func main() {
    if err := processFile("data.txt"); err != nil {
        fmt.Fprintf(os.Stderr, "Error: %v\n", err)
        os.Exit(1)
    }
}

defer의 활용: RAII 대체

// C++: RAII로 자동 정리
void criticalSection() {
    std::lock_guard<std::mutex> lock(mtx);  // 생성자에서 락
    // 작업...
    // 소멸자에서 자동 언락
}
void fileOperation() {
    std::ifstream f("file.txt");
    // 작업...
    // 소멸자에서 자동 close
}
// Go: defer로 명시적 정리
func criticalSection() {
    mu.Lock()
    defer mu.Unlock()  // 함수 종료 시 자동 실행
    // 작업...
}
func fileOperation() error {
    f, err := os.Open("file.txt")
    if err != nil {
        return err
    }
    defer f.Close()  // return, panic 모두에서 실행
    // 작업...
    return nil
}

defer의 실행 순서 (LIFO)

// Go: defer는 LIFO (Last In First Out)
func example() {
    defer fmt.Println("1")
    defer fmt.Println("2")
    defer fmt.Println("3")
    fmt.Println("함수 본문")
}
// 출력:
// 함수 본문
// 3
// 2
// 1

panic과 recover (제한적 사용)

// Go: panic/recover - 복구 가능한 심각한 오류에만 사용
func safeDivide(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("panic recovered: %v", r)
        }
    }()
    
    if b == 0 {
        panic("division by zero")  // 일반적으로는 error 반환 권장
    }
    
    return a / b, nil
}
func main() {
    result, err := safeDivide(10, 0)
    if err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Println("Result:", result)
    }
}

Go의 에러 처리 철학:

  • 명시성: 에러가 발생할 수 있는 모든 곳에서 명시적으로 처리
  • 투명성: 호출 스택을 따라 에러가 전파되는 경로가 코드에 명확히 드러남
  • panic은 예외적 상황: 복구 불가능한 프로그래밍 오류에만 사용

Day 10~11: 고루틴(Goroutine)과 채널(Channel)

📖 상세 글 보기: [Go 2주 완성 #06] Day 10~11: 고루틴과 채널 주제: C++ 개발자가 Go에 열광하는 진짜 이유, 동시성 프로그래밍 핵심 내용:

  • OS 스레드(std::thread)와 고루틴의 무게 차이 (수만 개의 고루틴 띄워보기)
  • “공유 메모리로 통신하지 말고, 통신으로 메모리를 공유하라”
  • Channel을 이용한 고루틴 간의 안전한 데이터 동기화
  • select 문을 활용한 다중 채널 제어 C++ vs Go 비교: 스레드 생성
// C++: std::thread (OS 스레드, 무거움)
#include <thread>
#include <iostream>
#include <vector>
void worker(int id) {
    std::cout << "Worker " << id << " running\n";
}
int main() {
    std::vector<std::thread> threads;
    
    // 10개 스레드 생성 (각 1~8MB 스택)
    for (int i = 0; i < 10; i++) {
        threads.emplace_back(worker, i);
    }
    
    // 모든 스레드 대기
    for (auto& t : threads) {
        t.join();
    }
    
    return 0;
}
// Go: 고루틴 (경량 스레드, 수 KB 스택)
// 함수 정의 및 구현
func worker(id int) {
    fmt.Printf("Worker %d running\n", id)
}
func main() {
    // 10,000개 고루틴도 가볍게 생성 가능
    for i := 0; i < 10000; i++ {
        go worker(i)  // go 키워드로 고루틴 생성
    }
    
    // 고루틴 완료 대기 (간단한 예시)
    time.Sleep(time.Second)
}

핵심 차이점:

  • 고루틴: 수 KB 스택으로 시작, 필요 시 자동 확장. M:N 스케줄링으로 OS 스레드보다 훨씬 가볍습니다.
  • 생성 비용: std::thread는 OS 스레드 생성 비용이 큽니다. 고루틴은 거의 무료입니다.
  • 수량: C++에서는 수백 개 스레드가 한계. Go는 수만~수십만 고루틴도 가능합니다. C++ vs Go 비교: 동기화
// C++: Mutex로 공유 메모리 보호
#include <mutex>
#include <thread>
std::mutex mtx;
int counter = 0;
void increment(int n) {
    for (int i = 0; i < n; i++) {
        std::lock_guard<std::mutex> lock(mtx);
        counter++;
    }
}
int main() {
    std::thread t1(increment, 1000);
    std::thread t2(increment, 1000);
    t1.join();
    t2.join();
    std::cout << "Counter: " << counter << "\n";
}
// Go: 채널로 통신 (권장 패턴)
func increment(ch chan int, n int) {
    for i := 0; i < n; i++ {
        ch <- 1  // 채널에 값 전송
    }
}
func main() {
    ch := make(chan int)
    counter := 0
    
    // 고루틴 2개 시작
    go increment(ch, 1000)
    go increment(ch, 1000)
    
    // 결과 수신
    for i := 0; i < 2000; i++ {
        counter += <-ch  // 채널에서 값 수신
    }
    
    fmt.Println("Counter:", counter)
}

채널의 기본 개념

// Go: 채널 생성과 사용
func main() {
    // 버퍼 없는 채널 (동기)
    ch := make(chan int)
    
    go func() {
        ch <- 42  // 전송 (수신자가 받을 때까지 블록)
    }()
    
    value := <-ch  // 수신
    fmt.Println(value)
    
    // 버퍼 있는 채널 (비동기)
    buffered := make(chan int, 3)
    buffered <- 1  // 버퍼가 차기 전까지 블록 안 됨
    buffered <- 2
    buffered <- 3
    
    fmt.Println(<-buffered)  // 1
    fmt.Println(<-buffered)  // 2
}

select 문: 다중 채널 제어

// Go: select로 여러 채널 동시 대기
func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)
    
    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "from ch1"
    }()
    
    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- "from ch2"
    }()
    
    // 먼저 준비된 채널에서 수신
    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-ch1:
            fmt.Println(msg1)
        case msg2 := <-ch2:
            fmt.Println(msg2)
        case <-time.After(3 * time.Second):
            fmt.Println("timeout")
            return
        }
    }
}

실전 패턴: 워커 풀

// Go: 워커 풀 패턴
func worker(id int, jobs <-chan int, results chan<- int) {
    for job := range jobs {
        fmt.Printf("Worker %d processing job %d\n", id, job)
        time.Sleep(time.Second)  // 작업 시뮬레이션
        results <- job * 2
    }
}
func main() {
    numJobs := 10
    jobs := make(chan int, numJobs)
    results := make(chan int, numJobs)
    
    // 3개 워커 시작
    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }
    
    // 작업 전송
    for j := 1; j <= numJobs; j++ {
        jobs <- j
    }
    close(jobs)
    
    // 결과 수집
    for a := 1; a <= numJobs; a++ {
        <-results
    }
}

Day 12~13: 의존성 관리와 테스팅

📖 상세 글 보기: [Go 2주 완성 #07] Day 12~13: 의존성 관리와 테스팅 주제: CMake와 vcpkg보다 수백 배 쉬운 패키지 관리 핵심 내용:

  • Go Modules (go.mod, go.sum) 기초 사용법
  • C++에서는 복잡했던 외부 라이브러리 가져오기 (go get)
  • 서드파티 프레임워크 없이 go test로 끝내는 유닛 테스트(Unit Test) 작성법 C++ vs Go 비교: 의존성 관리
# C++: CMake + vcpkg/Conan
# CMakeLists.txt 작성
# find_package() 설정
# vcpkg install 또는 conan install
# 빌드 시스템 설정...
# Go: 모듈 초기화 및 의존성 추가
go mod init myproject
go get github.com/gin-gonic/gin@latest
go mod tidy  # 불필요한 의존성 정리

go.mod 파일 예시

// go.mod
module myproject
go 1.21
require (
    github.com/gin-gonic/gin v1.9.1
    github.com/stretchr/testify v1.8.4
)

핵심 차이점:

  • Go는 빌드 시스템이 내장되어 있습니다. CMake, Make 등 불필요.
  • go get으로 의존성을 추가하면 go.mod에 자동 기록됩니다.
  • go.sum은 체크섬으로 의존성 무결성을 보장합니다. C++ vs Go 비교: 유닛 테스트
// C++: Google Test 사용 예시
#include <gtest/gtest.h>
int Add(int a, int b) {
    return a + b;
}
TEST(MathTest, AddPositive) {
    EXPECT_EQ(Add(2, 3), 5);
}
TEST(MathTest, AddNegative) {
    EXPECT_EQ(Add(-2, -3), -5);
}
int main(int argc, char **argv) {
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}
// Go: 내장 testing 패키지 (외부 프레임워크 불필요)
// math.go
package math
func Add(a, b int) int {
    return a + b
}
// math_test.go (같은 패키지, _test.go 접미사)
package math
import "testing"
func TestAddPositive(t *testing.T) {
    result := Add(2, 3)
    expected := 5
    if result != expected {
        t.Errorf("Add(2, 3) = %d; want %d", result, expected)
    }
}
func TestAddNegative(t *testing.T) {
    result := Add(-2, -3)
    expected := -5
    if result != expected {
        t.Errorf("Add(-2, -3) = %d; want %d", result, expected)
    }
}

테스트 실행

# Go: 테스트 실행 (매우 간단)
go test                    # 현재 패키지 테스트
go test ./....             # 모든 하위 패키지 테스트
go test -v                 # 상세 출력
go test -cover             # 커버리지 측정
go test -bench=.           # 벤치마크 실행

테이블 주도 테스트 (Table-Driven Test)

// Go: 테이블 주도 테스트 패턴
func TestAdd(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"positive", 2, 3, 5},
        {"negative", -2, -3, -5},
        {"zero", 0, 0, 0},
        {"mixed", -5, 10, 5},
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := Add(tt.a, tt.b)
            if result != tt.expected {
                t.Errorf("Add(%d, %d) = %d; want %d", 
                    tt.a, tt.b, result, tt.expected)
            }
        })
    }
}

벤치마크

// Go: 벤치마크 (함수명 BenchmarkXxx)
func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add(2, 3)
    }
}
// 실행: go test -bench=.
// 출력: BenchmarkAdd-8   1000000000   0.25 ns/op

Day 14: 실전 미니 프로젝트 (REST API 서버 구축)

📖 상세 글 보기: [Go 2주 완성 #08] Day 14: 실전 미니 프로젝트 주제: 배운 것을 하나로 엮는 실전 프로젝트 핵심 내용:

  • net/http 표준 라이브러리만을 이용한 초간단 웹 서버 띄우기
  • JSON 데이터 직렬화/역직렬화 (encoding/json)
  • 동시성(Goroutine)을 활용한 백그라운드 작업 처리 실습 프로젝트: 간단한 TODO API 서버
// main.go
package main
import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "sync"
)
// Todo 구조체
type Todo struct {
    ID        int    `json:"id"`
    Title     string `json:"title"`
    Completed bool   `json:"completed"`
}
// 인메모리 저장소 (실전에서는 DB 사용)
type TodoStore struct {
    mu    sync.RWMutex
    todos map[int]*Todo
    nextID int
}
func NewTodoStore() *TodoStore {
    return &TodoStore{
        todos: make(map[int]*Todo),
        nextID: 1,
    }
}
func (s *TodoStore) Create(title string) *Todo {
    s.mu.Lock()
    defer s.mu.Unlock()
    
    todo := &Todo{
        ID:        s.nextID,
        Title:     title,
        Completed: false,
    }
    s.todos[s.nextID] = todo
    s.nextID++
    return todo
}
func (s *TodoStore) GetAll() []*Todo {
    s.mu.RLock()
    defer s.mu.RUnlock()
    
    result := make([]*Todo, 0, len(s.todos))
    for _, todo := range s.todos {
        result = append(result, todo)
    }
    return result
}
func (s *TodoStore) Update(id int, completed bool) error {
    s.mu.Lock()
    defer s.mu.Unlock()
    
    todo, ok := s.todos[id]
    if !ok {
        return fmt.Errorf("todo not found")
    }
    todo.Completed = completed
    return nil
}
// HTTP 핸들러
type TodoHandler struct {
    store *TodoStore
}
func NewTodoHandler(store *TodoStore) *TodoHandler {
    return &TodoHandler{store: store}
}
// GET /todos - 모든 TODO 조회
func (h *TodoHandler) GetTodos(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodGet {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }
    
    todos := h.store.GetAll()
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(todos)
}
// POST /todos - 새 TODO 생성
func (h *TodoHandler) CreateTodo(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }
    
    var req struct {
        Title string `json:"title"`
    }
    
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, "Invalid JSON", http.StatusBadRequest)
        return
    }
    
    if req.Title == "" {
        http.Error(w, "Title required", http.StatusBadRequest)
        return
    }
    
    todo := h.store.Create(req.Title)
    
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(todo)
}
func main() {
    store := NewTodoStore()
    handler := NewTodoHandler(store)
    
    // 라우팅 설정
    http.HandleFunc("/todos", func(w http.ResponseWriter, r *http.Request) {
        switch r.Method {
        case http.MethodGet:
            handler.GetTodos(w, r)
        case http.MethodPost:
            handler.CreateTodo(w, r)
        default:
            http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        }
    })
    
    // 백그라운드 작업 예시 (고루틴 활용)
    go func() {
        ticker := time.NewTicker(10 * time.Second)
        defer ticker.Stop()
        
        for range ticker.C {
            todos := store.GetAll()
            log.Printf("Current todos count: %d", len(todos))
        }
    }()
    
    // 서버 시작
    addr := ":8080"
    log.Printf("Server starting on %s", addr)
    if err := http.ListenAndServe(addr, nil); err != nil {
        log.Fatal(err)
    }
}

테스트 코드

// main_test.go
package main
import (
    "bytes"
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "testing"
)
func TestCreateTodo(t *testing.T) {
    store := NewTodoStore()
    handler := NewTodoHandler(store)
    
    // 요청 생성
    reqBody := `{"title":"Test Todo"}`
    req := httptest.NewRequest(http.MethodPost, "/todos", 
        bytes.NewBufferString(reqBody))
    req.Header.Set("Content-Type", "application/json")
    
    // 응답 기록
    w := httptest.NewRecorder()
    handler.CreateTodo(w, req)
    
    // 검증
    if w.Code != http.StatusCreated {
        t.Errorf("Expected status %d, got %d", http.StatusCreated, w.Code)
    }
    
    var todo Todo
    if err := json.NewDecoder(w.Body).Decode(&todo); err != nil {
        t.Fatal(err)
    }
    
    if todo.Title != "Test Todo" {
        t.Errorf("Expected title 'Test Todo', got '%s'", todo.Title)
    }
}

실행 방법

# 서버 실행
go run main.go
# 다른 터미널에서 테스트
curl http://localhost:8080/todos
curl -X POST http://localhost:8080/todos \
  -H "Content-Type: application/json" \
  -d '{"title":"Learn Go"}'
# 테스트 실행
go test -v

C++와 비교한 장점:

  • 빌드 속도: C++의 긴 컴파일 시간 vs Go의 초고속 빌드
  • 의존성: 복잡한 CMake 설정 vs 한 줄 go get
  • 동시성: 복잡한 스레드 풀 vs 간단한 go 키워드
  • 배포: 단일 바이너리로 크로스 컴파일 가능

학습 팁과 추천 자료

효과적인 학습 전략

  1. C++ 사고방식 매핑: Go를 배울 때 “C++에서는 이렇게 했는데”를 항상 떠올리세요. 심화: 내부 구조와 실무 전환의 매핑 표를 함께 보면 개념이 빨리 고정됩니다.
  2. 작은 프로그램 많이 작성: 매일 10~20줄 짜리 프로그램을 5개 이상 작성하세요.
  3. 표준 라이브러리 탐색: fmt, os, io, net/http, encoding/json 등을 직접 사용해보세요.
  4. 에러 처리 습관화: if err != nil 패턴을 자연스럽게 받아들이세요.
  5. 고루틴 실험: 수백, 수천 개의 고루틴을 띄워보며 가벼움을 체감하세요.

추천 학습 자료

공식 자료:

  • Tour of Go - 대화형 Go 튜토리얼
  • Effective Go - Go 스타일 가이드
  • Go by Example - 예제 중심 학습 스타일 가이드:
  • Uber Go Style Guide
  • Google Go Style Guide 실전 프로젝트 아이디어:
  • CLI 도구 (파일 처리, 로그 분석기)
  • HTTP 클라이언트 (API 테스터)
  • 간단한 웹 크롤러
  • 채팅 서버 (WebSocket + 고루틴)
  • 데이터베이스 마이그레이션 도구

C++ 개발자가 Go에서 주의할 점

flowchart TD
    A[C++ 습관] --> B{Go에서 문제?}
    B -->|Yes| C[수정 필요]
    B -->|No| D[그대로 사용]
    
    C --> E[RAII → defer]
    C --> F[예외 → error 반환]
    C --> G[상속 → 합성]
    C --> H[템플릿 → 인터페이스/제네릭]
    
    D --> I[포인터 개념]
    D --> J[반복문 로직]
    D --> K[조건문 구조]

자주 하는 실수:

  1. defer를 루프 안에서 사용: 함수 종료 시에만 실행되므로 리소스 누수 발생
  2. 에러 무시: _로 에러를 무시하면 디버깅 어려움
  3. 루프 변수 클로저: 고루틴에서 루프 변수를 캡처할 때 주의
  4. 채널 닫기 누락: 수신자가 영원히 대기할 수 있음
  5. 포인터 vs 값 혼동: 메서드가 필드를 수정하는지에 따라 리시버 타입 선택

정리: 2주 후 당신이 할 수 있는 것

습득 가능한 핵심 스킬

이 커리큘럼을 완료하면 다음을 할 수 있습니다:

  • Go 문법 완전 이해: 변수, 함수, 구조체, 인터페이스
  • 메모리 관리: GC 환경에서의 효율적인 메모리 사용
  • 에러 처리: if err != nil 패턴과 에러 래핑
  • 동시성 프로그래밍: 고루틴과 채널을 활용한 병렬 처리
  • 테스트 작성: 유닛 테스트와 벤치마크
  • REST API 서버: 실무에 바로 적용 가능한 웹 서버 구축
  • 의존성 관리: Go Modules로 라이브러리 관리

C++에서 Go로 전환 체크리스트

메모리·리소스:

  • GC가 메모리를 관리한다는 사실 수용
  • 파일·락 등은 defer로 정리
  • 루프 안에서는 명시적 Close 호출 타입·다형성:
  • 인터페이스는 메서드 집합일 뿐
  • 명시적 implements 없이 덕 타이핑
  • 제네릭보다 인터페이스 우선 고려 에러 처리:
  • 예외 대신 error 반환
  • fmt.Errorf로 에러 래핑
  • panic은 복구 불가능한 경우만 동시성:
  • 채널 우선, Mutex는 필요 시
  • 루프 변수는 인자로 전달
  • 채널 사용 후 close 호출

다음 단계

2주 커리큘럼을 완료했다면:

  1. 실전 프로젝트: 기존 C++ 프로젝트의 일부를 Go로 재작성
  2. 고급 패턴: Context, 미들웨어, 의존성 주입
  3. 성능 최적화: 프로파일링, 메모리 최적화
  4. 프레임워크: Gin, Echo 등 웹 프레임워크 학습
  5. 클라우드: Docker, Kubernetes와 Go 통합

마무리

C++의 복잡함에서 Go의 심플함으로 전환하는 것은 단순히 새로운 언어를 배우는 것 이상입니다. “적게 쓰고 많이 얻는다”는 Go의 철학을 체득하면, 더 빠르고 안전한 코드를 작성할 수 있습니다. CMake 설정에 시간을 쏟는 대신 비즈니스 로직에 집중하고, 복잡한 스레드 동기화 대신 채널로 우아하게 통신하세요. 2주 후, 당신은 클라우드 네이티브 시대의 필수 언어를 마스터한 개발자가 되어 있을 것입니다. 다음 읽을 글: C++ 개발자의 뇌 구조로 이해하는 Go 언어에서 더 깊이 있는 개념 매핑을 확인하세요. 시리즈 시작하기: [Go 2주 완성 #01] Day 1~2: Go 언어의 철학과 기본 문법부터 시작하세요! 시리즈 전체 목차: Go 2주 완성 시리즈 인덱스에서 전체 글 목록과 추천 학습 경로를 확인하세요.

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


자주 묻는 질문 (FAQ)

Q. C++ 경험이 많으면 Go를 더 빨리 배울 수 있나요?

A. 네. 포인터, 메모리 관리, 동시성 개념에 익숙하다면 Go의 개념을 빠르게 이해할 수 있습니다. 다만 C++의 일부 습관(예외 처리, 상속)은 의도적으로 버려야 합니다.

Q. Go 제네릭이 없다면 타입 안전성은 어떻게 보장하나요?

A. Go 1.18부터 제네릭이 추가되었습니다. 그 전에는 인터페이스와 타입 단언으로 처리했습니다. 현재는 [T any] 같은 타입 파라미터를 사용할 수 있습니다.

Q. 실무에서 Go는 어떤 분야에 많이 쓰이나요?

A. 클라우드 인프라(Docker, Kubernetes), 마이크로서비스, API 서버, CLI 도구, DevOps 도구에 널리 사용됩니다. Google, Uber, Netflix 등에서 대규모로 활용 중입니다.

Q. C++보다 Go가 느리지 않나요?

A. 단일 스레드 성능은 C++이 우수하지만, 동시성 처리가 필요한 서버 애플리케이션에서는 Go가 더 효율적일 수 있습니다. GC 오버헤드는 있지만, 개발 생산성과 안전성을 고려하면 충분히 합리적인 트레이드오프입니다.

관련 글

심화 부록: 구현·운영 관점

이 부록은 앞선 본문에서 다룬 주제(「C++ 개발자를 위한 2주 완성 Go 언어(Golang) 마스터 커리큘럼」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.

내부 동작과 핵심 메커니즘

flowchart TD
  A[입력·요청·이벤트] --> B[파싱·검증·디코딩]
  B --> C[핵심 연산·상태 전이]
  C --> D[부작용: I/O·네트워크·동시성]
  D --> E[결과·관측·저장]
sequenceDiagram
  participant C as 클라이언트/호출자
  participant B as 경계(런타임·게이트웨이·프로세스)
  participant D as 의존성(API·DB·큐·파일)
  C->>B: 요청/이벤트
  B->>D: 조회·쓰기·RPC
  D-->>B: 지연·부분 실패·재시도 가능
  B-->>C: 응답 또는 오류(코드·상관 ID)
  • 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
  • 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
  • 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
  • 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.

프로덕션 운영 패턴

영역운영 관점 질문
관측성요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가
안전성입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가
신뢰성재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가
성능캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가
배포롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가
용량피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가

스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.

확장 예시: 엔드투엔드 미니 시나리오

앞선 본문 주제(「C++ 개발자를 위한 2주 완성 Go 언어(Golang) 마스터 커리큘럼」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  1. 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
  2. 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
  3. 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
  4. 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
  5. 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
  ctx = newCorrelationId()
  validated = validateSchema(request)
  authorize(validated, ctx)
  result = domainCore(validated)
  persistOrEmit(result, idempotentKey)
  recordMetrics(ctx, latency, outcome)
  return result

문제 해결(Troubleshooting)

증상가능 원인조치
간헐적 실패레이스, 타임아웃, 외부 의존성, DNS최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검
성능 저하N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거
메모리 증가캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납상한·TTL·힙/FD 스냅샷 비교
빌드·배포만 실패환경 변수, 권한, 플랫폼 차이, lockfileCI 로그와 로컬 diff, 런타임·이미지 버전 핀
설정 불일치프로필·시크릿·기본값, 리전스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화
데이터 불일치비멱등 재시도, 부분 쓰기, 캐시 무효화 누락멱등 키·아웃박스·트랜잭션 경계 재검토

권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.

배포 전에는 git addgit commitgit pushnpm run deploy 순서를 권장합니다.


이 글에서 다루는 키워드 (관련 검색어)

C++, Go, Golang, 커리큘럼, 학습가이드, 동시성, goroutine 등으로 검색하시면 이 글이 도움이 됩니다.