[Go 2주 완성 #06] Day 10~11: 고루틴과 채널 - 동시성 프로그래밍의 혁명

[Go 2주 완성 #06] Day 10~11: 고루틴과 채널 - 동시성 프로그래밍의 혁명

이 글의 핵심

고루틴 생성·채널 송수신·버퍼 채널·select·WaitGroup으로 동시성을 구현합니다. C++ 스레드·뮤텍스와 대비해 Go의 통신 기반 모델을 익힙니다. Day 10~11입니다.

시리즈 안내

📚 Go 2주 완성 시리즈 #06 | 전체 목차 보기

이 글은 C++ 개발자를 위한 2주 완성 Go 언어 커리큘럼Day 10~11 내용입니다.

이전: #05 에러 처리 ← | → 다음: #07 테스팅


들어가며: 경량 동시성의 세계

C++에서 std::thread로 스레드를 만들면 스레드당 1~8MB의 스택이 필요해, 수백 개만 넘어도 부담이 큽니다. Go의 고루틴은 수 KB 수준으로 시작해 필요할 때 스택이 늘어나므로, 경량 작업자를 수만 개까지 띄우기 쉽습니다.

채널은 고루틴 사이에서 값을 주고받는 파이프입니다. 공유 메모리를 직접 만지기보다, 파이프로 데이터를 흘려보내며 동기화하는 방식이 Go의 동시성 모델과 잘 맞습니다.

Rust의 채널·Arc/Mutex와 자주 비교되고, Kotlin 코루틴은 스레드 풀·Channel API로 비슷한 목표를 다른 문법으로 풉니다. 전통적인 OS 스레드 API는 Java Thread 글과 나란히 보면 “무겁다”는 느낌의 기준이 잡힙니다.

이 글에서 배울 내용:

  • 고루틴: 경량 작업자로 동시 작업 나누기
  • 채널: 파이프로 안전하게 통신하기
  • select: 여러 채널 중 준비된 쪽 처리하기
  • 동시성 패턴: 워커 풀, 파이프라인

실무에서의 체감

C++ 위주로 서버를 다루던 환경에서 Go를 도입할 때 흔히 드는 인상은 문법과 동시성 모델이 단순해 보인다는 점입니다. 다만 프로덕션에서 쓰면 그 단순함이 빌드·배포·동시성 코드 가독성으로 이어지는 경우가 많습니다.

자주 언급되는 장점:

  • 개발 속도: 팀·도메인에 따라 다르지만, 동시성·네트워크 코드를 빠르게 완성하기 쉬운 편입니다.
  • 안정성: GC가 있어 수동 할당 해제 부담이 줄어듭니다.
  • 배포: 단일 바이너리로 옮기기 쉬운 구조입니다.

위 내용은 시리즈 전체를 구성할 때 참고한 실무 관점의 정리입니다.


목차

  1. 고루틴: 경량 스레드
  2. 채널: 고루틴 간 통신
  3. 버퍼 채널
  4. select: 다중 채널 제어
  5. 동시성 패턴
  6. 실습 과제

1. 고루틴: 경량 스레드

C++ vs Go: 스레드 생성

// C++: std::thread (무거움)
#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: 고루틴 (가벼움)
package main

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

func worker(id int) {
    fmt.Printf("Worker %d running\n", id)
}

func main() {
    var wg sync.WaitGroup
    
    // 10,000개 고루틴도 가볍게 생성 가능!
    for i := 0; i < 10000; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            worker(id)
        }(i)
    }
    
    // 모든 고루틴 대기
    wg.Wait()
}

핵심 차이점:

  • 생성 비용: 고루틴은 OS 스레드보다 훨씬 저렴
  • 스택 크기: 고루틴은 2KB로 시작, 필요 시 자동 확장
  • 스케줄링: M:N 스케줄링 (M개 고루틴을 N개 OS 스레드에서 실행)
  • 수량: C++는 수백 개 한계, Go는 수만~수십만 개 가능

sync.WaitGroup

// Go: WaitGroup으로 고루틴 대기
package main

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

func task(id int, wg *sync.WaitGroup) {
    defer wg.Done()  // 완료 시 카운터 감소
    
    fmt.Printf("Task %d starting\n", id)
    time.Sleep(time.Second)
    fmt.Printf("Task %d done\n", id)
}

func main() {
    var wg sync.WaitGroup
    
    for i := 1; i <= 5; i++ {
        wg.Add(1)  // 카운터 증가
        go task(i, &wg)
    }
    
    wg.Wait()  // 모든 고루틴 완료 대기
    fmt.Println("All tasks completed")
}

2. 채널: 고루틴 간 통신

C++ vs Go: 데이터 공유

// C++: Mutex로 공유 메모리 보호
#include <thread>
#include <mutex>
#include <vector>

std::mutex mtx;
std::vector<int> results;

void worker(int id) {
    int result = id * 2;
    
    std::lock_guard<std::mutex> lock(mtx);
    results.push_back(result);
}

int main() {
    std::vector<std::thread> threads;
    
    for (int i = 0; i < 10; i++) {
        threads.emplace_back(worker, i);
    }
    
    for (auto& t : threads) {
        t.join();
    }
    
    return 0;
}
// Go: 채널로 통신 (권장 패턴)
package main

import "fmt"

func worker(id int, ch chan int) {
    result := id * 2
    ch <- result  // 채널에 전송
}

func main() {
    ch := make(chan int)
    
    // 10개 고루틴 시작
    for i := 0; i < 10; i++ {
        go worker(i, ch)
    }
    
    // 결과 수신
    results := make([]int, 0, 10)
    for i := 0; i < 10; i++ {
        result := <-ch  // 채널에서 수신
        results = append(results, result)
    }
    
    fmt.Println(results)
}

Go의 철학: “공유 메모리로 통신하지 말고, 통신으로 메모리를 공유하라”

Do not communicate by sharing memory; instead, share memory by communicating.

채널 기본 연산

// Go: 채널 생성과 사용
package main

import "fmt"

func main() {
    // 채널 생성
    ch := make(chan int)
    
    // 송신 (고루틴에서)
    go func() {
        ch <- 42  // 전송 (수신자가 받을 때까지 블록)
    }()
    
    // 수신
    value := <-ch  // 수신 (송신자가 보낼 때까지 블록)
    fmt.Println(value)
    
    // 채널 닫기 (송신자가 더 이상 보낼 것이 없을 때)
    close(ch)
    
    // 닫힌 채널에서 수신 시 제로 값과 false 반환
    v, ok := <-ch
    fmt.Println(v, ok)  // 0 false
}

채널 방향성

// Go: 채널 방향 지정 (타입 안전성)
package main

import "fmt"

// 송신 전용 채널
func sender(ch chan<- int) {
    ch <- 1
    ch <- 2
    ch <- 3
    close(ch)
    // v := <-ch  // ❌ 컴파일 에러: 수신 불가
}

// 수신 전용 채널
func receiver(ch <-chan int) {
    for v := range ch {
        fmt.Println(v)
    }
    // ch <- 4  // ❌ 컴파일 에러: 송신 불가
}

func main() {
    ch := make(chan int)  // 양방향 채널
    
    go sender(ch)    // 송신 전용으로 전달
    receiver(ch)     // 수신 전용으로 전달
}

3. 버퍼 채널

버퍼 없는 채널 vs 버퍼 채널

// Go: 버퍼 없는 채널 (동기)
package main

import "fmt"

func unbuffered() {
    ch := make(chan int)  // 버퍼 없음
    
    // ❌ 데드락: 수신자 없이 송신 시도
    // ch <- 1  // 영원히 블록
    
    // ✅ 고루틴에서 송신
    go func() {
        ch <- 1  // 수신자가 받을 때까지 대기
    }()
    
    fmt.Println(<-ch)  // 1
}

func buffered() {
    ch := make(chan int, 3)  // 버퍼 크기 3
    
    // ✅ 버퍼가 차기 전까지 블록 안 됨
    ch <- 1
    ch <- 2
    ch <- 3
    // ch <- 4  // 버퍼 가득 참, 블록됨
    
    fmt.Println(<-ch)  // 1
    fmt.Println(<-ch)  // 2
    fmt.Println(<-ch)  // 3
}

버퍼 채널 활용:

// Go: 버퍼 채널로 생산자-소비자 패턴
package main

import (
    "fmt"
    "time"
)

func producer(ch chan<- int) {
    for i := 0; i < 10; i++ {
        fmt.Printf("Producing %d\n", i)
        ch <- i
        time.Sleep(100 * time.Millisecond)
    }
    close(ch)
}

func consumer(ch <-chan int) {
    for v := range ch {  // 채널이 닫힐 때까지 수신
        fmt.Printf("Consuming %d\n", v)
        time.Sleep(200 * time.Millisecond)
    }
}

func main() {
    ch := make(chan int, 5)  // 버퍼 크기 5
    
    go producer(ch)
    consumer(ch)
}

4. select: 다중 채널 제어

select 기본 사용법

// Go: select로 여러 채널 대기
package main

import (
    "fmt"
    "time"
)

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)
        }
    }
}

select with timeout

// Go: 타임아웃 처리
package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan string)
    
    go func() {
        time.Sleep(2 * time.Second)
        ch <- "result"
    }()
    
    select {
    case result := <-ch:
        fmt.Println("Received:", result)
    case <-time.After(1 * time.Second):
        fmt.Println("Timeout!")
    }
}

select with default (논블로킹)

// Go: 논블로킹 채널 연산
package main

import "fmt"

func main() {
    ch := make(chan int, 1)
    
    // 논블로킹 송신
    select {
    case ch <- 1:
        fmt.Println("Sent")
    default:
        fmt.Println("Channel full")
    }
    
    // 논블로킹 수신
    select {
    case v := <-ch:
        fmt.Println("Received:", v)
    default:
        fmt.Println("No data")
    }
}

5. 동시성 패턴

패턴 1: 워커 풀 (Worker Pool)

// C++: 스레드 풀 (복잡)
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>

class ThreadPool {
    std::vector<std::thread> workers;
    std::queue<std::function<void()>> tasks;
    std::mutex mtx;
    std::condition_variable cv;
    bool stop = false;
    
public:
    ThreadPool(size_t numThreads) {
        for (size_t i = 0; i < numThreads; i++) {
            workers.emplace_back([this] {
                while (true) {
                    std::function<void()> task;
                    {
                        std::unique_lock<std::mutex> lock(mtx);
                        cv.wait(lock, [this] { return stop || !tasks.empty(); });
                        if (stop && tasks.empty()) return;
                        task = std::move(tasks.front());
                        tasks.pop();
                    }
                    task();
                }
            });
        }
    }
    // ... (생략)
};
// Go: 워커 풀 (간단)
package main

import (
    "fmt"
    "time"
)

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++ {
        result := <-results
        fmt.Println("Result:", result)
    }
}

패턴 2: 파이프라인 (Pipeline)

// Go: 파이프라인 패턴
package main

import "fmt"

// 단계 1: 숫자 생성
func generator(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for _, n := range nums {
            out <- n
        }
    }()
    return out
}

// 단계 2: 제곱
func square(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for n := range in {
            out <- n * n
        }
    }()
    return out
}

// 단계 3: 출력
func printer(in <-chan int) {
    for n := range in {
        fmt.Println(n)
    }
}

func main() {
    // 파이프라인 구성
    nums := generator(1, 2, 3, 4, 5)
    squared := square(nums)
    printer(squared)
}

패턴 3: Fan-out, Fan-in

// Go: Fan-out (하나의 입력을 여러 워커에 분배)
package main

import (
    "fmt"
    "sync"
)

func fanOut(in <-chan int, numWorkers int) []<-chan int {
    outs := make([]<-chan int, numWorkers)
    
    for i := 0; i < numWorkers; i++ {
        out := make(chan int)
        outs[i] = out
        
        go func(ch chan int) {
            defer close(ch)
            for v := range in {
                ch <- v * 2  // 처리
            }
        }(out)
    }
    
    return outs
}

// Fan-in (여러 채널을 하나로 병합)
func fanIn(channels ...<-chan int) <-chan int {
    out := make(chan int)
    var wg sync.WaitGroup
    
    for _, ch := range channels {
        wg.Add(1)
        go func(c <-chan int) {
            defer wg.Done()
            for v := range c {
                out <- v
            }
        }(ch)
    }
    
    go func() {
        wg.Wait()
        close(out)
    }()
    
    return out
}

func main() {
    // 입력 생성
    in := make(chan int)
    go func() {
        defer close(in)
        for i := 1; i <= 10; i++ {
            in <- i
        }
    }()
    
    // Fan-out: 3개 워커에 분배
    workers := fanOut(in, 3)
    
    // Fan-in: 결과 병합
    out := fanIn(workers...)
    
    // 결과 출력
    for result := range out {
        fmt.Println(result)
    }
}

패턴 4: 타임아웃과 취소

// Go: context로 타임아웃과 취소
package main

import (
    "context"
    "fmt"
    "time"
)

func worker(ctx context.Context, id int) {
    for {
        select {
        case <-ctx.Done():  // 취소 신호
            fmt.Printf("Worker %d cancelled\n", id)
            return
        default:
            fmt.Printf("Worker %d working...\n", id)
            time.Sleep(500 * time.Millisecond)
        }
    }
}

func main() {
    // 2초 타임아웃
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()
    
    for i := 1; i <= 3; i++ {
        go worker(ctx, i)
    }
    
    // 타임아웃 대기
    <-ctx.Done()
    fmt.Println("Main: timeout reached")
    
    // 고루틴이 정리될 시간 주기
    time.Sleep(time.Second)
}

6. 실습 과제

과제 1: 병렬 다운로드

// Go: 여러 URL 병렬 다운로드
package main

import (
    "fmt"
    "io"
    "net/http"
    "sync"
)

func download(url string, wg *sync.WaitGroup, results chan<- string) {
    defer wg.Done()
    
    resp, err := http.Get(url)
    if err != nil {
        results <- fmt.Sprintf("%s: error - %v", url, err)
        return
    }
    defer resp.Body.Close()
    
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        results <- fmt.Sprintf("%s: read error - %v", url, err)
        return
    }
    
    results <- fmt.Sprintf("%s: %d bytes", url, len(body))
}

func main() {
    urls := []string{
        "https://golang.org",
        "https://github.com",
        "https://stackoverflow.com",
    }
    
    var wg sync.WaitGroup
    results := make(chan string, len(urls))
    
    for _, url := range urls {
        wg.Add(1)
        go download(url, &wg, results)
    }
    
    // 고루틴 완료 대기 후 채널 닫기
    go func() {
        wg.Wait()
        close(results)
    }()
    
    // 결과 출력
    for result := range results {
        fmt.Println(result)
    }
}

과제 2: 레이트 리미터

// Go: 채널로 레이트 리미터 구현
package main

import (
    "fmt"
    "time"
)

func rateLimiter(requests <-chan int, rate time.Duration) {
    ticker := time.NewTicker(rate)
    defer ticker.Stop()
    
    for req := range requests {
        <-ticker.C  // rate마다 하나씩 처리
        fmt.Printf("Processing request %d at %v\n", req, time.Now())
    }
}

func main() {
    requests := make(chan int, 10)
    
    // 레이트 리미터 시작 (500ms마다 하나씩)
    go rateLimiter(requests, 500*time.Millisecond)
    
    // 요청 전송
    for i := 1; i <= 5; i++ {
        requests <- i
    }
    close(requests)
    
    time.Sleep(3 * time.Second)
}

과제 3: 타임아웃 있는 작업

// Go: 타임아웃 처리
package main

import (
    "fmt"
    "time"
)

func longRunningTask(result chan<- string) {
    time.Sleep(3 * time.Second)
    result <- "Task completed"
}

func main() {
    result := make(chan string)
    
    go longRunningTask(result)
    
    select {
    case res := <-result:
        fmt.Println(res)
    case <-time.After(2 * time.Second):
        fmt.Println("Timeout: task took too long")
    }
}

과제 4: 동시 Map 접근

// Go: sync.Mutex vs 채널
package main

import (
    "fmt"
    "sync"
)

// 방법 1: Mutex 사용
type SafeCounter1 struct {
    mu    sync.Mutex
    count map[string]int
}

func (c *SafeCounter1) Inc(key string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count[key]++
}

func (c *SafeCounter1) Value(key string) int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.count[key]
}

// 방법 2: 채널 사용 (더 Go다운 방식)
type SafeCounter2 struct {
    ops chan func(map[string]int)
}

func NewSafeCounter2() *SafeCounter2 {
    c := &SafeCounter2{
        ops: make(chan func(map[string]int)),
    }
    
    go func() {
        count := make(map[string]int)
        for op := range c.ops {
            op(count)
        }
    }()
    
    return c
}

func (c *SafeCounter2) Inc(key string) {
    c.ops <- func(count map[string]int) {
        count[key]++
    }
}

func (c *SafeCounter2) Value(key string) int {
    result := make(chan int)
    c.ops <- func(count map[string]int) {
        result <- count[key]
    }
    return <-result
}

func main() {
    // Mutex 방식
    counter1 := &SafeCounter1{count: make(map[string]int)}
    
    var wg sync.WaitGroup
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter1.Inc("key")
        }()
    }
    wg.Wait()
    
    fmt.Println("Counter1:", counter1.Value("key"))
    
    // 채널 방식
    counter2 := NewSafeCounter2()
    
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter2.Inc("key")
        }()
    }
    wg.Wait()
    
    fmt.Println("Counter2:", counter2.Value("key"))
}

정리: Day 10~11 학습 체크리스트

완료해야 할 항목

  • go 키워드로 고루틴 생성
  • sync.WaitGroup으로 고루틴 대기
  • 채널 생성 및 송수신 (<-)
  • 버퍼 채널과 버퍼 없는 채널 차이 이해
  • select로 다중 채널 제어
  • 타임아웃과 논블로킹 연산
  • 워커 풀, 파이프라인 패턴 구현
  • 실습 과제 4개 완료

C++에서 Go로 전환 포인트

C++Go비고
std::threadgo 키워드훨씬 가벼움
thread.join()sync.WaitGroup더 유연
std::mutexsync.Mutex 또는 채널채널 우선
std::condition_variable채널 또는 sync.Cond채널이 더 간단
공유 메모리 + 락채널 통신패러다임 전환

동시성 vs 병렬성

graph TD
    A[동시성 Concurrency] --> B[여러 작업을 다루는 구조]
    C[병렬성 Parallelism] --> D[여러 작업을 동시에 실행]
    
    B --> E[고루틴으로 구현]
    D --> F[멀티코어에서 실행]
    
    E --> G[GOMAXPROCS로 제어]
    F --> G

Go의 동시성 모델:

  • 동시성: 여러 작업을 구조적으로 다루는 방법 (고루틴, 채널)
  • 병렬성: 여러 작업을 물리적으로 동시에 실행 (멀티코어)
  • Go는 동시성을 쉽게 만들고, 런타임이 자동으로 병렬성을 처리

다음 단계 예고

Day 10~11에서는 고루틴과 채널을 배웠습니다. 다음 글에서는 의존성 관리와 테스팅을 다룹니다. CMake보다 수백 배 쉬운 Go Modules와 내장 테스트 프레임워크를 배웁니다.


📚 시리즈 네비게이션

이전 글목차다음 글
← #05 에러 처리📑 전체 목차#07 테스팅 →

Go 2주 완성 시리즈: 커리큘럼#01 기본 문법#02 자료구조#03 객체지향#04 인터페이스#05 에러 처리#06 고루틴·채널#07 테스팅#08 REST API#09 context·우아한 종료


한 줄 요약: 고루틴은 가볍고, 채널은 안전하며, select는 강력합니다. Go의 동시성 모델은 C++ 스레드보다 훨씬 우아합니다.

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

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

  • [Go 2주 완성 #05] Day 8~9: 예외 처리의 새로운 접근 - try-catch는 잊어라
  • [Go 심화 #09] context.Context·타임아웃·우아한 종료 — 동시성을 프로덕션 수준으로
  • C++ 개발자를 위한 2주 완성 Go 언어(Golang) 마스터 커리큘럼
  • C++ std::thread 입문 | join 누락·디태치 남용 등 자주 하는 실수 3가지와 해결법

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

Go goroutine, Go channel, select Go, 동시성 프로그래밍, C++ thread 비교, Golang 고루틴, Go 2주 완성, 채널 버퍼 등으로 검색하시면 이 글이 도움이 됩니다.

실전 팁

실무에서 바로 적용할 수 있는 팁입니다.

디버깅 팁

  • 문제가 발생하면 먼저 컴파일러 경고를 확인하세요
  • 간단한 테스트 케이스로 문제를 재현하세요

성능 팁

  • 프로파일링 없이 최적화하지 마세요
  • 측정 가능한 지표를 먼저 설정하세요

코드 리뷰 팁

  • 코드 리뷰에서 자주 지적받는 부분을 미리 체크하세요
  • 팀의 코딩 컨벤션을 따르세요

실전 체크리스트

실무에서 이 개념을 적용할 때 확인해야 할 사항입니다.

코드 작성 전

  • 이 기법이 현재 문제를 해결하는 최선의 방법인가?
  • 팀원들이 이 코드를 이해하고 유지보수할 수 있는가?
  • 성능 요구사항을 만족하는가?

코드 작성 중

  • 컴파일러 경고를 모두 해결했는가?
  • 엣지 케이스를 고려했는가?
  • 에러 처리가 적절한가?

코드 리뷰 시

  • 코드의 의도가 명확한가?
  • 테스트 케이스가 충분한가?
  • 문서화가 되어 있는가?

이 체크리스트를 활용하여 실수를 줄이고 코드 품질을 높이세요.


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. C++ std::thread의 무거움에서 벗어나 수만 개의 고루틴을 가볍게 생성하세요. 채널로 안전하게 통신하고, select로 다중 채널을 제어하는 Go 동시성 프로그래밍의 핵심을 배웁니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


관련 글

  • C++ 개발자를 위한 2주 완성 Go 언어(Golang) 마스터 커리큘럼
  • C++ vs Go | 성능·동시성·선택 가이드 완전 비교 [#47-1]
  • C++ 개발자의 뇌 구조로 이해하는 Go 언어 [#47-2]
  • [Go 2주 완성 #01] Day 1~2: Go 언어의 철학과 기본 문법 - C++ 개발자의 첫인상
  • [Go 2주 완성 #02] Day 3~4: 메모리와 자료구조 - 포인터 연산은 없지만 포인터는 있다