C++ 개발자를 위한 2주 완성 Go 언어(Golang) 마스터 커리큘럼
이 글의 핵심
C++ 개발자를 위한 2주 완성 Go 언어(Golang) 마스터 커리큘럼에 대한 실전 가이드입니다.
시리즈 안내
📚 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++ 프로젝트를 진행하며 이 패턴/기법의 중요성을 체감했습니다. 책에서 배운 이론과 실제 코드는 많이 달랐습니다.
실전 경험:
- 문제: 처음에는 이 개념을 제대로 이해하지 못해 비효율적인 코드를 작성했습니다
- 해결: 코드 리뷰와 프로파일링을 통해 문제를 발견하고 개선했습니다
- 교훈: 이론만으로는 부족하고, 실제로 부딪혀보며 배워야 합니다
이 글이 여러분의 시행착오를 줄여주길 바랍니다.
목차
시리즈 전체 글 목록
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주 이후 권장):
📌 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) 소개 - 변수 선언의 차이 (
autovs:=) - C++의
while을 대체하는 강력한for문 - 가비지 컬렉터(GC)의 도입:
new와delete로부터의 해방
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.Reader는Read메서드 하나만)
표준 라이브러리 인터페이스 예시
// 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 언어의 꽃, 동시성과 실전 에코시스템
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 패턴 대체) panic과recover(왜 일반적인 예외 처리로 쓰면 안 되는가?)
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키워드 - 배포: 단일 바이너리로 크로스 컴파일 가능
학습 팁과 추천 자료
효과적인 학습 전략
- C++ 사고방식 매핑: Go를 배울 때 "C++에서는 이렇게 했는데"를 항상 떠올리세요.
- 작은 프로그램 많이 작성: 매일 10~20줄 짜리 프로그램을 5개 이상 작성하세요.
- 표준 라이브러리 탐색:
fmt,os,io,net/http,encoding/json등을 직접 사용해보세요. - 에러 처리 습관화:
if err != nil패턴을 자연스럽게 받아들이세요. - 고루틴 실험: 수백, 수천 개의 고루틴을 띄워보며 가벼움을 체감하세요.
추천 학습 자료
공식 자료:
- Tour of Go - 대화형 Go 튜토리얼
- Effective Go - Go 스타일 가이드
- Go by Example - 예제 중심 학습
스타일 가이드:
실전 프로젝트 아이디어:
- 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[조건문 구조]
자주 하는 실수:
- defer를 루프 안에서 사용: 함수 종료 시에만 실행되므로 리소스 누수 발생
- 에러 무시:
_로 에러를 무시하면 디버깅 어려움 - 루프 변수 클로저: 고루틴에서 루프 변수를 캡처할 때 주의
- 채널 닫기 누락: 수신자가 영원히 대기할 수 있음
- 포인터 vs 값 혼동: 메서드가 필드를 수정하는지에 따라 리시버 타입 선택
정리: 2주 후 당신이 할 수 있는 것
습득 가능한 핵심 스킬
이 커리큘럼을 완료하면 다음을 할 수 있습니다:
- ✅ Go 문법 완전 이해: 변수, 함수, 구조체, 인터페이스
- ✅ 메모리 관리: GC 환경에서의 효율적인 메모리 사용
- ✅ 에러 처리:
if err != nil패턴과 에러 래핑 - ✅ 동시성 프로그래밍: 고루틴과 채널을 활용한 병렬 처리
- ✅ 테스트 작성: 유닛 테스트와 벤치마크
- ✅ REST API 서버: 실무에 바로 적용 가능한 웹 서버 구축
- ✅ 의존성 관리: Go Modules로 라이브러리 관리
C++에서 Go로 전환 체크리스트
메모리·리소스:
- GC가 메모리를 관리한다는 사실 수용
- 파일·락 등은
defer로 정리 - 루프 안에서는 명시적
Close호출
타입·다형성:
- 인터페이스는 메서드 집합일 뿐
- 명시적
implements없이 덕 타이핑 - 제네릭보다 인터페이스 우선 고려
에러 처리:
- 예외 대신
error반환 -
fmt.Errorf로 에러 래핑 -
panic은 복구 불가능한 경우만
동시성:
- 채널 우선, Mutex는 필요 시
- 루프 변수는 인자로 전달
- 채널 사용 후
close호출
다음 단계
2주 커리큘럼을 완료했다면:
- 실전 프로젝트: 기존 C++ 프로젝트의 일부를 Go로 재작성
- 고급 패턴: Context, 미들웨어, 의존성 주입
- 성능 최적화: 프로파일링, 메모리 최적화
- 프레임워크: Gin, Echo 등 웹 프레임워크 학습
- 클라우드: Docker, Kubernetes와 Go 통합
마무리
C++의 복잡함에서 Go의 심플함으로 전환하는 것은 단순히 새로운 언어를 배우는 것 이상입니다. **"적게 쓰고 많이 얻는다"**는 Go의 철학을 체득하면, 더 빠르고 안전한 코드를 작성할 수 있습니다.
CMake 설정에 시간을 쏟는 대신 비즈니스 로직에 집중하고, 복잡한 스레드 동기화 대신 채널로 우아하게 통신하세요. 2주 후, 당신은 클라우드 네이티브 시대의 필수 언어를 마스터한 개발자가 되어 있을 것입니다.
다음 읽을 글: C++ 개발자의 뇌 구조로 이해하는 Go 언어에서 더 깊이 있는 개념 매핑을 확인하세요.
시리즈 시작하기: [Go 2주 완성 #01] Day 1~2: Go 언어의 철학과 기본 문법부터 시작하세요!
시리즈 전체 목차: Go 2주 완성 시리즈 인덱스에서 전체 글 목록과 추천 학습 경로를 확인하세요.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 개발자의 뇌 구조로 이해하는 Go 언어 [#47-2]
- C++ vs Go | 성능·동시성·선택 가이드 완전 비교 [#47-1]
- C++ std::thread 입문 | join 누락·디태치 남용 등 자주 하는 실수 3가지와 해결법
자주 묻는 질문 (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 오버헤드는 있지만, 개발 생산성과 안전성을 고려하면 충분히 합리적인 트레이드오프입니다.