[Go 2주 완성 #01] Day 1~2: Go 언어의 철학과 기본 문법 - C++ 개발자의 첫인상
이 글의 핵심
C++ 경험자를 위한 Go 첫걸음입니다. 설치부터 :=·var·for·range·가비지 컬렉터·go fmt까지 C++ 코드와 나란히 비교하며 익힙니다. Go 2주 완성 시리즈 Day 1~2입니다.
시리즈 안내
📚 Go 2주 완성 시리즈 #01 | 전체 목차 보기
이 글은 C++ 개발자를 위한 2주 완성 Go 언어 커리큘럼의 Day 1~2 내용입니다.
이전: 커리큘럼 소개 ← | → 다음: #02 메모리와 자료구조
들어가며: Go 언어와의 첫 만남
C++ 개발자로서 Go를 처음 접하면 “이게 정말 프로그래밍 언어인가?” 싶을 정도로 심플합니다. 헤더 파일도 없고, 템플릿 메타프로그래밍도 없고, new/delete도 신경 쓸 필요가 없습니다. 이 글에서는 Go 언어의 철학과 기본 문법을 C++ 개발자의 시선으로 살펴봅니다.
이 글에서 배울 내용:
- Go 설치 및 기본 툴체인 (
go build,go run,go fmt) - Go의 철학(단순성·명시성·동시성)과 C++와의 차이(포인터·상속·제네릭)
- 패키지·모듈(
go mod, import,internal) 개요 - 변수 선언:
autovs:=의 차이 - 반복문:
while이 없는 세계 - 가비지 컬렉터(GC)로 메모리 관리 자동화
실무에서의 체감
C++ 위주로 서버를 다루던 환경에서 Go를 도입할 때 흔히 드는 인상은 문법과 툴체인이 단순해 보인다는 점입니다. 프로덕션에서는 그 단순함이 빌드·배포·동시성 코드 가독성으로 이어지는 경우가 많습니다.
자주 언급되는 장점:
- 개발 속도: 팀·도메인에 따라 다르지만, 네트워크·CLI 코드를 빠르게 완성하기 쉬운 편입니다.
- 안정성: GC가 있어 수동 할당 해제 부담이 줄어듭니다.
- 배포: 단일 바이너리로 옮기기 쉬운 구조입니다.
Go의 철학: 단순성, 명시성, 동시성
Go는 읽기 쉬운 코드와 예측 가능한 동작을 최우선으로 둡니다. C++에서 흔한 “같은 문제를 여러 방식으로 풀 수 있다”는 유연함보다, 한 가지 명확한 관용구를 택하는 쪽에 가깝습니다.
| 축 | 의미 | 실무에서의 체감 |
|---|---|---|
| 단순성 | 문법·키워드 수를 줄이고, 도구(go fmt, go test)로 일관성 확보 | 리뷰 시 스타일·빌드 스크립트 논쟁이 줄어듦 |
| 명시성 | 암시적 변환·숨은 제어 흐름을 줄임 (if err != nil, 명시적 go로 동시 실행) | “이 함수가 실패할 수 있는가?”가 시그니처에 드러남 |
| 동시성 | 동시 실행은 고루틴 + 채널이라는 고정된 도구로 표현 (이 시리즈 후반에서 다룸) | 스레드·뮤텍스만 던지지 않고, 통신으로 동기화하는 습관을 강제하기 쉬움 |
C++에서 온 개발자에게는 “표현력이 부족해 보인다”는 인상이 들 수 있지만, 팀 단위로 유지보수할 때 비용이 낮아지는 방향으로 설계되어 있다고 보면 됩니다.
C++와의 차이: 포인터, 상속, 제네릭
이미 본문 여기저기에서 다루지만, 첫날 관점에서만 핵심만 정리합니다.
포인터
- C++:
T*, 참조T&, 스마트 포인터, 포인터 연산까지 한 언어 안에 공존. - Go: 포인터는
*T이지만 포인터 연산은 없음.nil역참조는 런타임(프로그램이 실행 중일 때) 패닉. 대부분의 “소유권”은 GC가 담당합니다. - 실무: “주소만 넘겨서 복사 비용 줄이기”는 비슷하지만, 댕글링 포인터로 잘못된 메모리 접근하는 패턴은 설계상 막히는 편입니다.
상속
- C++:
public상속, 다중 상속, 가상 함수로 다형성. - Go: 클래스 상속 없음. 구조체 임베딩과 인터페이스(메서드 집합)로 대체합니다(#03, #04).
- 실무: “기본 클래스에 몰아넣기” 대신 작은 인터페이스 + 합성이 기본 패턴입니다.
제네릭
- C++: 템플릿으로 컴파일 타임 다형성 (강력하지만 에러 메시지·빌드 비용 이슈).
- Go: Go 1.18+에서 타입 매개변수가 들어왔습니다. 모든 코드가 제네릭일 필요는 없고,
map, 슬라이스, 채널 등이 이미 제네릭 느낌으로 쓰입니다. - 실무: 입문 단계에서는 인터페이스와 구체 타입을 먼저 익히고, 반복되는 알고리즘·컨테이너에만 제네릭을 도입하면 됩니다.
패키지와 모듈 시스템
패키지 한 줄 요약
- 모든
.go파일은package선언으로 묶입니다. 실행 파일은package main+func main(). - 같은 디렉터리의 파일들은 같은 패키지 이름을 가져야 합니다.
- 대문자로 시작하는 이름만 패키지 밖에서 보입니다(
Exported). 소문자는 패키지 내부 전용입니다. C++의namespace+private에 가깝게 이해하면 됩니다.
모듈(go mod)과 의존성
- 모듈 루트에
go.mod가 있으면, 모듈 경로(예:github.com/org/project)가 import 경로의 접두사가 됩니다. go mod init,go mod tidy로 의존성을 정리하고, 버전은go.mod/go.sum에 고정됩니다.- 표준 라이브러리는 import 문자열만으로 사용 (
"fmt","net/http"). 서드파티는 원격 모듈 경로 또는replace/vendor로 관리합니다.
실무에서 자주 쓰는 흐름
mkdir myapp && cd myapp
go mod init github.com/you/myapp
# 코드 작성 후
go build ./...
go test ./...
internal 디렉터리: myproject/internal/foo 패키지는 부모 트리 밖에서는 import할 수 없게 막아, 공개 API를 좁히는 데 쓰입니다. 팀 프로젝트에서 경계를 나눌 때 유용합니다.
실전: 함수·제어 문법 빠른 참조
아래는 본문에서 세부적으로 다루는 내용을 함수 관점에서만 압축한 것입니다.
함수
package example
import "fmt"
// 여러 반환값 (에러는 마지막에 두는 것이 관례)
func divide(a, b int) (int, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
}
// 이름 있는 반환값 (defer와 함께 쓸 때 가끔 사용)
func stats() (sum int, n int) {
// ...
return // naked return — 남용하지 말 것
}
제어
- 조건:
if,for,switch만 사용 (while없음). - 이름 충돌 줄이기:
if err := f(); err != nil { ... }처럼 짧은 스코프를 쓰는 패턴이 표준입니다.
이미 변수·반복문·조건문 절에서 같은 내용을 예제와 함께 다루었으므로, 막혔을 때 이 절로 돌아와 머릿속을 정리하면 됩니다.
목차
- Go 설치와 Hello World
- Go의 철학: 단순성, 명시성, 동시성
- C++와의 차이: 포인터, 상속, 제네릭
- 패키지와 모듈 시스템
- 실전: 함수·제어 문법 빠른 참조
- 변수 선언: auto vs :=
- 반복문: for 하나로 모든 것을
- 조건문과 기본 제어 흐름
- 가비지 컬렉터: new/delete로부터의 해방
- go fmt: 코딩 스타일 논쟁 종결자
- 실습 과제
1. Go 설치와 Hello World
Go 설치
Go 설치 과정 상세 가이드:
# Windows (PowerShell)
# 1. https://go.dev/dl/ 접속
# 2. "Microsoft Windows" 섹션에서 .msi 파일 다운로드
# 3. 설치 파일 실행 (기본 경로: C:\Go)
# 4. 설치 완료 후 PowerShell 재시작
# 설치 확인
go version
# 출력: go version go1.21.0 windows/amd64
# GOPATH 확인 (Go 작업 공간)
go env GOPATH
# 출력: C:\Users\YourName\go
# macOS (Homebrew)
brew install go
# 설치 확인
go version
# 출력: go version go1.21.0 darwin/arm64
# Linux (Ubuntu/Debian)
# 방법 1: 공식 바이너리 (권장)
wget https://go.dev/dl/go1.21.0.linux-amd64.tar.gz
sudo rm -rf /usr/local/go # 기존 설치 제거
sudo tar -C /usr/local -xzf go1.21.0.linux-amd64.tar.gz
# PATH 추가 (~/.bashrc 또는 ~/.zshrc)
export PATH=$PATH:/usr/local/go/bin
# 설정 적용
source ~/.bashrc
# 방법 2: apt (버전이 오래될 수 있음)
sudo apt update
sudo apt install golang-go
# 설치 확인
go version
# 출력: go version go1.21.0 linux/amd64
명령 정리: go version으로 툴체인이 맞는지 확인하고, go env·GOPATH는 모듈(go mod) 시대에도 여전히 바이너리 캐치 경로 등에 쓰입니다. 팀과 CI에서는 동일한 Go 마이너 버전을 맞춰 두면 재현성 있는 빌드가 쉬워집니다.
Go 환경 변수:
# Go 환경 변수 전체 확인
go env
# 주요 환경 변수:
# GOROOT: Go 설치 경로
# GOPATH: Go 작업 공간 (패키지, 바이너리 저장)
# GOBIN: 실행 파일 설치 경로
# GOOS: 운영체제 (windows, darwin, linux)
# GOARCH: CPU 아키텍처 (amd64, arm64)
# 특정 변수 확인
go env GOROOT
go env GOPATH
환경 변수 읽기: GOOS/GOARCH는 크로스 컴파일할 때 특히 중요합니다. 예를 들어 macOS에서 linux/amd64 바이너리를 만들 때 GOOS=linux를 앞에 붙이는 식으로 씁니다. 컨테이너 이미지를 여러 아키텍처로 배포할 때도 같은 개념이 반복됩니다.
C++ vs Go: Hello World 상세 비교
C++ Hello World:
// C++: Hello World
#include <iostream> // 헤더 파일 포함 (전처리기)
int main() { // 반환 타입 명시 필수
std::cout << "Hello, World!" << std::endl;
return 0; // 명시적 반환 (생략 가능하지만 권장)
}
// 컴파일 및 실행 (2단계)
// 1. 컴파일: g++ hello.cpp -o hello
// - 전처리 → 컴파일 → 어셈블 → 링크
// - 실행 파일 생성: hello (또는 hello.exe)
// 2. 실행: ./hello (또는 hello.exe)
// - 출력: Hello, World!
Go Hello World:
// Go: Hello World
package main // 패키지 선언 (필수, 실행 파일은 main)
import "fmt" // 표준 라이브러리 import (헤더 파일 없음)
func main() { // 반환 타입 없음 (항상 void)
fmt.Println("Hello, World!")
// return 불필요 (자동 종료)
}
// 실행 방법 1: 즉시 실행 (개발 중)
// go run hello.go
// 동작: 임시 컴파일 → 실행 → 정리
// 출력: Hello, World!
// 실행 방법 2: 빌드 후 실행 (배포용)
// go build hello.go
// 실행 파일 생성: hello (또는 hello.exe)
// ./hello
// 출력: Hello, World!
// 실행 방법 3: 설치 (GOPATH/bin에 복사)
// go install hello.go
// hello (어디서든 실행 가능)
핵심 차이점 상세 설명:
-
패키지 선언:
- C++: 없음 (네임스페이스는 선택)
- Go:
package main필수 (실행 파일은 main, 라이브러리는 다른 이름)
-
import vs include:
- C++:
#include <iostream>(헤더 파일 텍스트 복사) - Go:
import "fmt"(패키지 참조, 순환 import 불가)
- C++:
-
main 함수:
- C++:
int main()(반환 타입 필수, return 0) - Go:
func main()(반환 타입 없음, return 불필요)
- C++:
-
빌드 과정:
- C++: 컴파일러 명령 + 링커 플래그 + Makefile/CMake
- Go:
go build하나로 끝 (의존성 자동 해결)
-
실행 파일:
- C++: 동적 링크 (libstdc++.so 등 필요)
- Go: 정적 링크 (단일 바이너리, 의존성 없음)
실전 예제 - 명령줄 인자 받기:
// C++: 명령줄 인자
#include <iostream>
int main(int argc, char* argv[]) {
std::cout << "인자 개수: " << argc << '\n';
for (int i = 0; i < argc; ++i) {
std::cout << "argv[" << i << "]: " << argv[i] << '\n';
}
return 0;
}
// 실행: ./hello arg1 arg2
// 출력:
// 인자 개수: 3
// argv[0]: ./hello
// argv[1]: arg1
// argv[2]: arg2
// Go: 명령줄 인자
package main
import (
"fmt"
"os"
)
func main() {
args := os.Args // []string 슬라이스
fmt.Println("인자 개수:", len(args))
for i, arg := range args {
fmt.Printf("args[%d]: %s\n", i, arg)
}
}
// 실행: go run hello.go arg1 arg2
// 출력:
// 인자 개수: 3
// args[0]: /tmp/go-build.../exe/hello
// args[1]: arg1
// args[2]: arg2
Go 툴체인 소개
Go는 통합 툴체인을 제공하여 별도의 빌드 도구가 필요 없습니다.
주요 Go 명령어 상세 설명:
# 1. go run - 즉시 실행 (개발 중 사용)
go run main.go
# 동작: 임시 디렉토리에 컴파일 → 실행 → 삭제
# 장점: 빠른 테스트, 스크립트처럼 사용
# 단점: 매번 컴파일 (배포용 아님)
# 2. go build - 실행 파일 생성
go build
# 현재 디렉토리의 모든 .go 파일을 컴파일
# 출력: 현재 디렉토리 이름으로 실행 파일 생성
go build -o myapp
# 출력 파일명 지정
go build -o myapp main.go
# 특정 파일만 컴파일
# 크로스 컴파일 (다른 OS용 빌드)
GOOS=linux GOARCH=amd64 go build -o myapp_linux
GOOS=windows GOARCH=amd64 go build -o myapp.exe
GOOS=darwin GOARCH=arm64 go build -o myapp_mac_m1
# 3. go fmt - 코드 포맷팅 (자동)
go fmt main.go # 단일 파일
go fmt ./... # 모든 하위 디렉토리
# 코드 스타일을 자동으로 통일 (들여쓰기, 공백 등)
# 4. go vet - 정적 분석 (버그 탐지)
go vet main.go
# 잠재적 버그 검사:
# - 포맷 문자열 오류 (Printf)
# - 사용하지 않는 변수
# - 잘못된 메서드 시그니처
# 5. go test - 테스트 실행
go test # 현재 패키지 테스트
go test ./... # 모든 하위 패키지 테스트
go test -v # 상세 출력
go test -cover # 커버리지 확인
# 6. go mod - 모듈 관리 (의존성)
go mod init myproject # go.mod 파일 생성
go mod tidy # 사용하지 않는 의존성 제거
go mod download # 의존성 다운로드
go mod vendor # vendor 폴더에 의존성 복사
# 7. go get - 패키지 다운로드
go get github.com/gin-gonic/gin
# 패키지 다운로드 및 go.mod에 추가
# 8. go install - 바이너리 설치
go install github.com/user/tool@latest
# $GOPATH/bin에 실행 파일 설치
C++과의 비교:
| 작업 | C++ | Go |
|---|---|---|
| 컴파일 | g++ main.cpp -o main | go build |
| 실행 | ./main | go run main.go |
| 포맷팅 | clang-format (별도 설치) | go fmt (내장) |
| 테스트 | Google Test (별도 설치) | go test (내장) |
| 의존성 | vcpkg, Conan (별도 도구) | go mod (내장) |
| 빌드 시스템 | CMake, Make, Ninja | 불필요 (go 명령어) |
실전 워크플로우:
# 1. 새 프로젝트 시작
mkdir myproject
cd myproject
go mod init github.com/username/myproject
# 2. 코드 작성 (main.go)
# 3. 개발 중 테스트
go run main.go
# 4. 코드 포맷팅 (저장 시 자동화 권장)
go fmt ./...
# 5. 정적 분석
go vet ./...
# 6. 테스트 실행
go test ./...
# 7. 빌드 (배포용)
go build -o myapp
# 8. 실행
./myapp
2. 변수 선언: auto vs :=
C++ vs Go: 변수 선언 철학
C++: 다양한 선택지 제공 (유연성 중시)
Go: 단순하고 명확한 방법 (일관성 중시)
C++ 변수 선언 (다양한 방식)
// C++: 여러 가지 변수 선언 방식
int x = 10; // 명시적 타입 (전통적)
auto y = 20; // 타입 추론 (C++11)
int z(30); // 생성자 스타일
int w{40}; // 균일 초기화 (C++11)
const int MAX = 100; // 런타임 상수
constexpr int SIZE = 50; // 컴파일 타임 상수
// 초기화하지 않으면 쓰레기 값 (위험!)
int uninitialized; // 예측 불가능한 값
std::cout << uninitialized << std::endl; // 쓰레기 값 출력
// 여러 변수 동시 선언
int a = 1, b = 2, c = 3;
// auto 사용 시 주의
auto d = 1, e = 2; // ✅ 같은 타입
// auto f = 1, g = 2.0; // ❌ 다른 타입 (에러)
// 타입 추론 예시
auto str = "Hello"; // const char* (C 스타일 문자열)
auto str2 = std::string("Hello"); // std::string
Go 변수 선언 (단순하고 명확)
// Go: 3가지 변수 선언 방식
var x int = 10 // 명시적 타입
var y = 20 // 타입 추론
z := 30 // 짧은 선언 (함수 내부만)
const MAX = 100 // 상수 (타입 추론)
const SIZE int = 50 // 상수 (명시적 타입)
// 초기화하지 않으면 제로 값 (안전!)
var uninitialized int // 0으로 자동 초기화
fmt.Println(uninitialized) // 0 출력 (예측 가능)
// 여러 변수 동시 선언
a, b, c := 1, 2, 3
// 블록 선언 (패키지 레벨)
var (
name string = "Go"
version int = 1
stable bool = true
)
// 타입 추론 예시
pi := 3.14 // float64
count := 100 // int
message := "Hello" // string
제로 값 (Zero Value) - Go의 안전 장치
Go는 초기화하지 않은 변수를 제로 값으로 자동 초기화합니다. C++의 쓰레기 값 문제가 완전히 사라집니다.
// Go: 제로 값 (타입별)
var i int // 0
var f float64 // 0.0
var b bool // false
var s string // "" (빈 문자열)
var p *int // nil (null 포인터)
var slice []int // nil (빈 슬라이스)
var m map[string]int // nil (빈 맵)
// 제로 값 활용 예시
var counter int
counter++ // 0에서 시작하여 1이 됨
fmt.Println(counter) // 1
// C++과 비교
// C++: int counter; counter++; // 쓰레기 값 + 1 (위험!)
// Go: var counter int; counter++ // 0 + 1 = 1 (안전!)
제로 값의 장점:
// 1. 안전한 초기화
func processData() {
var sum int // 0으로 초기화 (명시적 초기화 불필요)
for _, v := range []int{1, 2, 3} {
sum += v
}
fmt.Println(sum) // 6
}
// 2. 간결한 코드
type User struct {
Name string
Age int
Email string
}
var user User // 모든 필드가 제로 값으로 초기화
fmt.Println(user) // { 0 } (Name="", Age=0, Email="")
// 3. nil 체크 간소화
var data []int // nil
if data == nil {
data = []int{1, 2, 3}
}
짧은 선언 (:=)의 규칙과 활용
기본 규칙:
func example() {
// ✅ 함수 내부에서만 사용 가능
x := 10
// ✅ 여러 변수 동시 선언
name, age := "Alice", 30
// ✅ 재선언: 최소 하나는 새 변수여야 함
name, email := "Bob", "[email protected]" // email은 새 변수
// name은 재할당, email은 새로 선언
// ❌ 모두 기존 변수면 에러
// name, age := "Charlie", 40 // 에러: no new variables
// ✅ 타입 자동 추론
pi := 3.14 // float64
count := 100 // int
message := "Hi" // string
items := []int{1, 2, 3} // []int (슬라이스)
}
// ❌ 패키지 레벨에서는 := 사용 불가
// x := 10 // 에러: syntax error: non-declaration statement outside function body
// ✅ 패키지 레벨은 var 사용
var globalVar = 10
실전 활용:
// 1. 에러 처리와 함께
file, err := os.Open("data.txt")
if err != nil {
log.Fatal(err)
}
defer file.Close()
// 2. 타입 변환
str := "123"
num, err := strconv.Atoi(str)
if err != nil {
fmt.Println("변환 실패")
}
// 3. 함수 여러 반환값 받기
result, ok := myMap["key"]
if !ok {
fmt.Println("키가 없습니다")
}
// 4. 짧은 if 문과 결합
if value, exists := cache["key"]; exists {
fmt.Println("캐시 히트:", value)
} else {
fmt.Println("캐시 미스")
}
var vs := 선택 가이드
| 상황 | 사용 | 이유 |
|---|---|---|
| 함수 내부, 타입 명확 | := | 간결 |
| 패키지 레벨 | var | := 불가 |
| 제로 값 초기화 | var | 명시적 의도 |
| 명시적 타입 필요 | var | 타입 지정 |
// 예시
func process() {
// ✅ := 사용 (타입 명확)
count := 0
name := "Alice"
// ✅ var 사용 (제로 값 의도)
var buffer []byte // nil로 초기화 의도
// ✅ var 사용 (명시적 타입)
var timeout time.Duration = 5 * time.Second
// ❌ := 사용 (타입 불명확)
// x := 0 // int? int32? int64?
// ✅ var 사용 (타입 명확)
var x int64 = 0
}
3. 반복문: for 하나로 모든 것을
Go의 반복문 철학
Go는 for 하나로 모든 반복을 처리합니다. while, do-while, foreach 같은 키워드가 없습니다.
왜 하나만?
- 단순함: 배울 것이 적음
- 일관성: 모든 반복이 같은 패턴
- 충분함: for만으로 모든 경우 커버
패턴 1: 전통적 for 루프
// C++: 전통적인 for 루프
for (int i = 0; i < 10; i++) {
std::cout << i << "\n";
}
// Go: 동일한 for 루프 (괄호 없음)
for i := 0; i < 10; i++ {
fmt.Println(i)
}
// 세미콜론 생략 가능 (단일 조건)
for i := 0; i < 10; {
fmt.Println(i)
i++
}
차이점:
- Go는 괄호
()없음 (더 깔끔) - Go는 중괄호
{}필수 (스타일 통일)
패턴 2: while 루프 (조건만)
// C++: while 루프
int i = 0;
while (i < 10) {
std::cout << i << "\n";
i++;
}
// do-while
int j = 0;
do {
std::cout << j << "\n";
j++;
} while (j < 10);
// Go: for로 while 대체 (초기화, 증감 생략)
i := 0
for i < 10 { // while (i < 10)과 동일
fmt.Println(i)
i++
}
// do-while 패턴 (조건을 끝에서 체크)
j := 0
for {
fmt.Println(j)
j++
if j >= 10 {
break
}
}
// 또는 첫 실행 보장
j := 0
for ok := true; ok; ok = j < 10 {
fmt.Println(j)
j++
}
패턴 3: 무한 루프
// C++: 무한 루프
while (true) {
// ...
if (condition) break;
}
// 또는
for (;;) {
// ...
if (condition) break;
}
// Go: 무한 루프 (조건 생략)
for {
// ...
if condition {
break
}
}
// 실전 예시: 서버 루프
for {
conn, err := listener.Accept()
if err != nil {
log.Println(err)
continue
}
go handleConnection(conn) // 고루틴으로 처리
}
패턴 4: range 루프 (컬렉션 순회)
// C++: 범위 기반 for (C++11)
std::vector<int> numbers = {1, 2, 3, 4, 5};
// 값만
for (int num : numbers) {
std::cout << num << "\n";
}
// 인덱스 + 값 (수동)
for (size_t i = 0; i < numbers.size(); i++) {
std::cout << i << ": " << numbers[i] << "\n";
}
// 맵 순회
std::map<std::string, int> ages = {{"Alice", 30}, {"Bob", 25}};
for (const auto& [name, age] : ages) { // C++17
std::cout << name << ": " << age << "\n";
}
// Go: range로 간단하게
numbers := []int{1, 2, 3, 4, 5}
// 인덱스 + 값
for i, num := range numbers {
fmt.Printf("%d: %d\n", i, num)
}
// 값만 (인덱스 무시)
for _, num := range numbers {
fmt.Println(num)
}
// 인덱스만 (값 무시)
for i := range numbers {
fmt.Println(i)
}
// 맵 순회
ages := map[string]int{"Alice": 30, "Bob": 25}
for name, age := range ages {
fmt.Printf("%s: %d\n", name, age)
}
// 문자열 순회 (rune 단위)
for i, char := range "Hello" {
fmt.Printf("%d: %c\n", i, char)
}
// 0: H
// 1: e
// 2: l
// 3: l
// 4: o
range의 특징:
- 슬라이스/배열: 인덱스, 값 반환
- 맵: 키, 값 반환
- 문자열: 인덱스, rune(유니코드 문자) 반환
- 채널: 값만 반환 (인덱스 없음)
C++ vs Go: 범위 기반 for (Range-based for)
// C++: 범위 기반 for (C++11)
#include <vector>
std::vector<int> vec = {1, 2, 3, 4, 5};
// 값 복사
for (auto v : vec) {
std::cout << v << " ";
}
// 참조 (수정 가능)
for (auto& v : vec) {
v *= 2;
}
// const 참조 (읽기 전용, 복사 방지)
for (const auto& v : vec) {
std::cout << v << " ";
}
// Go: range로 컨테이너 순회
slice := []int{1, 2, 3, 4, 5}
// 인덱스와 값 모두 받기
for i, v := range slice {
fmt.Println(i, v)
}
// 값만 받기 (인덱스 무시)
for _, v := range slice {
fmt.Println(v)
}
// 인덱스만 받기
for i := range slice {
fmt.Println(i)
}
// 값 수정 (인덱스로 접근)
for i := range slice {
slice[i] *= 2
}
for 루프 제어: break, continue, 레이블
// C++: break와 continue
for (int i = 0; i < 10; i++) {
if (i == 5) continue; // 5 건너뛰기
if (i == 8) break; // 8에서 종료
std::cout << i << " ";
}
// 출력: 0 1 2 3 4 6 7
// 중첩 루프 탈출 (goto 사용)
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 5; j++) {
if (i * j > 10) {
goto end; // 외부 루프까지 탈출
}
}
}
end:
std::cout << "Done\n";
// Go: break와 continue
for i := 0; i < 10; i++ {
if i == 5 {
continue // 5 건너뛰기
}
if i == 8 {
break // 8에서 종료
}
fmt.Print(i, " ")
}
// 출력: 0 1 2 3 4 6 7
// 레이블과 break (중첩 루프 탈출)
outer:
for i := 0; i < 5; i++ {
for j := 0; j < 5; j++ {
if i*j > 10 {
break outer // 외부 루프까지 탈출
}
fmt.Printf("(%d,%d) ", i, j)
}
}
fmt.Println("\nDone")
// 레이블과 continue (외부 루프 계속)
outer:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if j == 1 {
continue outer // 외부 루프의 다음 반복으로
}
fmt.Printf("(%d,%d) ", i, j)
}
}
// 출력: (0,0) (1,0) (2,0)
핵심 차이:
- C++:
goto사용 (가독성 낮음) - Go: 레이블 +
break/continue(명확)
4. 조건문과 기본 제어 흐름
if 문: 괄호 없는 조건문
// C++: if 문 (괄호 필수)
int x = 10;
if (x > 5) {
std::cout << "x is greater than 5\n";
} else if (x > 0) {
std::cout << "x is positive\n";
} else {
std::cout << "x is non-positive\n";
}
// 한 줄 if (중괄호 선택)
if (x > 5)
std::cout << "Greater\n"; // 중괄호 없어도 됨 (위험!)
// C++17: 초기화 구문
if (auto it = map.find(key); it != map.end()) {
std::cout << "Found: " << it->second << "\n";
}
// Go: if 문 (괄호 없음, 중괄호 필수)
x := 10
if x > 5 {
fmt.Println("x is greater than 5")
} else if x > 0 {
fmt.Println("x is positive")
} else {
fmt.Println("x is non-positive")
}
// 한 줄이어도 중괄호 필수 (안전)
if x > 5 {
fmt.Println("Greater") // 중괄호 필수!
}
// 초기화 구문 (변수 스코프 제한)
if v, ok := m[key]; ok {
fmt.Println("Found:", v)
// v는 if 블록 내에서만 사용 가능
}
// fmt.Println(v) // 에러: v는 여기서 사용 불가
// Go의 일반적인 에러 체크 패턴
if err := doSomething(); err != nil {
fmt.Println("Error:", err)
return
}
// err는 if 블록 내에서만 존재
// 여러 조건 체크
if file, err := os.Open("data.txt"); err != nil {
log.Fatal(err)
} else {
defer file.Close()
// file 사용...
}
핵심 차이점:
| 항목 | C++ | Go |
|---|---|---|
| 조건식 괄호 | 필수 if (x > 5) | 없음 if x > 5 |
| 중괄호 | 선택 (한 줄이면) | 필수 (항상) |
| 초기화 구문 | C++17부터 | 처음부터 지원 |
| 변수 스코프 | 블록 전체 | if 블록만 |
if 초기화 구문의 장점
// ❌ 나쁜 예: 변수 스코프가 넓음
v, ok := m[key]
if ok {
fmt.Println(v)
}
// v와 ok가 여기서도 사용 가능 (불필요)
// ✅ 좋은 예: 변수 스코프 제한
if v, ok := m[key]; ok {
fmt.Println(v)
}
// v와 ok는 if 블록 내에서만 존재 (깔끔)
// 실전 예시: 파일 처리
if data, err := os.ReadFile("config.json"); err != nil {
log.Fatal(err)
} else {
// data 사용
fmt.Println(string(data))
}
// 에러 체크 패턴 (가장 일반적)
if err := saveToDatabase(user); err != nil {
return fmt.Errorf("데이터베이스 저장 실패: %w", err)
}
switch 문: 자동 break
// C++: switch 문 (break 필수)
int day = 3;
switch (day) {
case 1:
std::cout << "Monday\n";
break; // break 빠뜨리면 다음 case로 떨어짐 (fallthrough)
case 2:
std::cout << "Tuesday\n";
break;
case 3:
case 4: // 여러 case 묶기
std::cout << "Mid-week\n";
break;
default:
std::cout << "Other day\n";
}
// break 빠뜨린 경우 (버그!)
switch (day) {
case 1:
std::cout << "Monday\n";
// break 없음 → case 2로 떨어짐!
case 2:
std::cout << "Tuesday\n";
break;
}
// Go: switch 문 (break 자동)
day := 3
switch day {
case 1:
fmt.Println("Monday")
// break 자동 (fallthrough 없음)
case 2:
fmt.Println("Tuesday")
case 3, 4: // 여러 값 동시 처리 (쉼표로 구분)
fmt.Println("Mid-week")
default:
fmt.Println("Other day")
}
// fallthrough 명시적 사용 (드물게)
switch day {
case 1:
fmt.Println("Monday")
fallthrough // 다음 case로 강제 이동
case 2:
fmt.Println("Weekday")
}
// day=1일 때 출력:
// Monday
// Weekday
조건식 없는 switch (if-else 대체)
// 복잡한 if-else 체인
x := 15
if x < 0 {
fmt.Println("Negative")
} else if x < 10 {
fmt.Println("Small")
} else if x < 100 {
fmt.Println("Medium")
} else {
fmt.Println("Large")
}
// switch로 더 깔끔하게 (조건식 생략)
switch {
case x < 0:
fmt.Println("Negative")
case x < 10:
fmt.Println("Small")
case x < 100:
fmt.Println("Medium")
default:
fmt.Println("Large")
}
// 실전 예시: 타입 체크
var value interface{} = 42
switch v := value.(type) {
case int:
fmt.Printf("정수: %d\n", v)
case string:
fmt.Printf("문자열: %s\n", v)
case bool:
fmt.Printf("불린: %t\n", v)
default:
fmt.Printf("알 수 없는 타입: %T\n", v)
}
switch 초기화 구문
// switch에도 초기화 구문 사용 가능
switch value := getValue(); value {
case 1:
fmt.Println("One")
case 2:
fmt.Println("Two")
default:
fmt.Println("Other")
}
// value는 switch 블록 내에서만 존재
// 실전 예시: 에러 타입 체크
switch err := doSomething(); err.(type) {
case *os.PathError:
fmt.Println("경로 에러")
case *os.SyscallError:
fmt.Println("시스템 호출 에러")
default:
fmt.Println("기타 에러:", err)
}
핵심 차이점:
| 항목 | C++ | Go |
|---|---|---|
| break | 필수 (빠뜨리면 버그) | 자동 (안전) |
| fallthrough | 기본 동작 | 명시적 키워드 |
| 조건식 없는 switch | 불가 | 가능 (if-else 대체) |
| 타입 switch | 불가 | 가능 |
| 여러 값 | 여러 줄 필요 | 쉼표로 한 줄 |
5. 가비지 컬렉터: new/delete로부터의 해방
메모리 관리 철학
C++: 개발자가 직접 관리 (성능 최적화 가능, 버그 위험)
Go: 가비지 컬렉터 자동 관리 (안전, 생산성 향상)
C++ 메모리 관리 (수동)
// C++: 수동 메모리 관리
int* p = new int(42);
delete p; // 수동 해제 필수
// 배열
int* arr = new int[100];
delete[] arr; // [] 필수 (delete만 쓰면 버그!)
// 흔한 실수들
int* p1 = new int(10);
// delete p1; // 해제 안 하면 메모리 누수!
int* p2 = new int(20);
delete p2;
// *p2 = 30; // 댕글링 포인터 (이미 해제된 메모리 접근)
int* arr2 = new int[100];
delete arr2; // ❌ delete[] 아님 (정의되지 않은 동작)
// 스마트 포인터 (C++11, RAII)
#include <memory>
auto ptr = std::make_unique<int>(42);
// 스코프 벗어나면 자동 해제
// 공유 소유권
auto shared1 = std::make_shared<int>(42);
auto shared2 = shared1; // 참조 카운트 증가
// 모든 shared_ptr이 소멸되면 메모리 해제
Go 메모리 관리 (자동)
// Go: 가비지 컬렉터 자동 관리
p := new(int)
*p = 42
// delete 불필요 - GC가 자동 수거
// 구조체 포인터
type Person struct {
Name string
Age int
}
person := &Person{Name: "Alice", Age: 30}
// GC가 자동 수거 (참조가 없어지면)
// 슬라이스 (동적 배열)
arr := make([]int, 100)
// GC가 자동 수거
// 맵
m := make(map[string]int)
m["key"] = 100
// GC가 자동 수거
GC의 장단점:
장점:
- ✅ 메모리 누수 방지: delete 잊어버릴 일 없음
- ✅ 댕글링 포인터 불가능: 이미 해제된 메모리 접근 불가
- ✅ 개발 생산성: 메모리 관리 신경 안 써도 됨
- ✅ 안전성: 메모리 관련 버그 대부분 제거
단점:
- ⚠️ GC 일시 정지: 수 ms (Go는 최적화되어 있음)
- ⚠️ 메모리 사용량: C++보다 높을 수 있음
- ⚠️ 예측 불가능: GC 실행 시점 제어 불가
- ⚠️ 실시간 시스템: 부적합할 수 있음 (항공, 의료기기 등)
스택 vs 힙: Escape Analysis
Go 컴파일러는 Escape Analysis로 변수를 스택/힙에 자동 배치합니다.
// 예제 1: 스택 할당
func createLocal() int {
x := 10 // 스택 할당 (함수 내부에서만 사용)
return x // 값 복사로 반환
}
// 예제 2: 힙 할당
func createHeap() *int {
x := 10 // 힙 할당 (포인터가 반환되어 함수 밖에서 사용)
return &x // 포인터 반환
}
// C++에서는 두 번째 함수가 댕글링 포인터 오류!
// Go는 컴파일러가 자동으로 힙에 할당하여 안전
// 예제 3: 복잡한 경우
type Data struct {
Value int
}
func process() *Data {
d := Data{Value: 100} // 힙 할당 (포인터 반환)
return &d
}
func main() {
data := process()
fmt.Println(data.Value) // 100 (안전하게 접근 가능)
}
Escape Analysis 확인:
# 컴파일러가 어디에 할당하는지 확인
go build -gcflags="-m" main.go
# 출력 예시:
# ./main.go:10:2: moved to heap: x
# ./main.go:15:2: d escapes to heap
C++ vs Go 메모리 관리 비교
| 항목 | C++ | Go |
|---|---|---|
| 할당 | new | new() 또는 & |
| 해제 | delete (수동) | GC (자동) |
| 배열 해제 | delete[] | 불필요 |
| 메모리 누수 | 가능 | 거의 불가능 |
| 댕글링 포인터 | 가능 | 불가능 |
| 성능 | 최고 | 약간 낮음 |
| 안전성 | 낮음 | 높음 |
| 개발 속도 | 느림 | 빠름 |
6. go fmt: 코딩 스타일 논쟁 종결자
C++의 스타일 전쟁
C++에는 통일된 코드 스타일이 없습니다. 팀마다, 회사마다 다른 스타일 가이드를 사용합니다.
// C++: 팀마다 다른 스타일
// 1. K&R 스타일 (Linux 커널)
if (condition) {
doSomething();
}
// 2. Allman 스타일 (BSD)
if (condition)
{
doSomething();
}
// 3. GNU 스타일
if (condition)
{
doSomething();
}
// 4. Stroustrup 스타일
if (condition) {
doSomething();
}
else {
doSomethingElse();
}
// 논쟁거리들:
// - 탭 vs 스페이스?
// - 들여쓰기 2칸 vs 4칸?
// - 중괄호 위치는?
// - 포인터 선언: int* p vs int *p?
// - 함수 이름: camelCase vs snake_case?
C++ 포맷터 도구 (별도 설치 필요):
clang-format: LLVM 프로젝트astyle: Artistic Style- 각 도구마다 설정 파일 필요 (
.clang-format,.astylerc)
Go의 해결책: go fmt
Go는 하나의 공식 스타일만 존재합니다. go fmt가 자동으로 포맷팅합니다.
// Go: go fmt가 자동으로 포맷팅
// 저장 전 (엉망인 코드)
func example(){
if x>0{
fmt.Println("positive")
}else{
fmt.Println("non-positive")
}
}
// go fmt 실행 후 (자동 정리)
func example() {
if x > 0 {
fmt.Println("positive")
} else {
fmt.Println("non-positive")
}
}
// 연산자 주변 공백
x:=1+2*3 // 저장 전
x := 1 + 2*3 // go fmt 후
// 불필요한 괄호 제거
if (x > 0) { // 저장 전
if x > 0 { // go fmt 후
// 들여쓰기는 항상 탭 (스페이스 아님)
go fmt의 철학:
- ✅ 하나의 스타일: Go 커뮤니티 전체가 동일한 스타일
- ✅ 자동화: IDE에 통합하여 저장 시 자동 실행
- ✅ 논쟁 종결: 코드 리뷰에서 스타일 논쟁 제거
- ✅ 설정 불필요:
.gofmt같은 설정 파일 없음
go fmt 사용법
# 단일 파일 포맷팅
go fmt main.go
# 현재 패키지 포맷팅
go fmt
# 모든 하위 디렉토리 포맷팅
go fmt ./...
# gofmt 직접 사용 (더 많은 옵션)
gofmt -w main.go # 파일에 직접 쓰기
gofmt -d main.go # 변경 사항만 출력 (diff)
gofmt -s -w main.go # 코드 단순화 + 쓰기
VS Code에서 자동 포맷팅 설정
// settings.json
{
"go.formatTool": "gofmt",
"editor.formatOnSave": true,
"[go]": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true
}
}
}
저장 시 자동 실행:
go fmt- 코드 포맷팅goimports- import 정리 (사용하지 않는 import 제거, 필요한 import 추가)
go fmt의 실제 효과
// 포맷팅 전
package main
import "fmt"
func main(){
var x=10
if x>5{fmt.Println("big")}else{
fmt.Println("small")}}
// go fmt 실행 후
package main
import "fmt"
func main() {
var x = 10
if x > 5 {
fmt.Println("big")
} else {
fmt.Println("small")
}
}
결과: 모든 Go 코드가 동일한 스타일로 작성됩니다. GitHub의 Go 프로젝트를 보면 모두 같은 스타일입니다!
7. 실습 과제
과제 1: FizzBuzz
C++과 Go로 각각 구현하고 차이를 비교해보세요.
// C++: FizzBuzz
#include <iostream>
int main() {
for (int i = 1; i <= 100; i++) {
if (i % 15 == 0) {
std::cout << "FizzBuzz\n";
} else if (i % 3 == 0) {
std::cout << "Fizz\n";
} else if (i % 5 == 0) {
std::cout << "Buzz\n";
} else {
std::cout << i << "\n";
}
}
return 0;
}
// Go: FizzBuzz
package main
import "fmt"
func main() {
for i := 1; i <= 100; i++ {
switch {
case i%15 == 0:
fmt.Println("FizzBuzz")
case i%3 == 0:
fmt.Println("Fizz")
case i%5 == 0:
fmt.Println("Buzz")
default:
fmt.Println(i)
}
}
}
과제 2: 구구단 출력
// Go: 구구단 (중첩 루프)
package main
import "fmt"
func main() {
for i := 2; i <= 9; i++ {
for j := 1; j <= 9; j++ {
fmt.Printf("%d x %d = %d\n", i, j, i*j)
}
fmt.Println()
}
}
과제 3: 슬라이스 합계 계산 (range 활용)
C++ 구현:
// C++: 벡터 합계
#include <iostream>
#include <vector>
int sum(const std::vector<int>& numbers) {
int total = 0;
for (int num : numbers) { // 범위 기반 for
total += num;
}
return total;
}
int main() {
std::vector<int> nums = {1, 2, 3, 4, 5};
std::cout << "Sum: " << sum(nums) << std::endl; // 15
return 0;
}
Go 구현:
// Go: 슬라이스 합계
package main
import "fmt"
func sum(numbers []int) int {
total := 0
for _, num := range numbers { // range 루프
total += num
}
return total
}
func main() {
nums := []int{1, 2, 3, 4, 5}
result := sum(nums)
fmt.Printf("Sum: %d\n", result) // Sum: 15
}
비교:
- C++:
const std::vector<int>&(참조 전달) - Go:
[]int(슬라이스는 참조 타입) - Go가 더 간결
과제 4: 짝수만 필터링 (슬라이스 조작)
C++ 구현:
// C++: 짝수 필터링
#include <iostream>
#include <vector>
std::vector<int> filterEven(const std::vector<int>& numbers) {
std::vector<int> result;
result.reserve(numbers.size()); // 메모리 예약
for (int num : numbers) {
if (num % 2 == 0) {
result.push_back(num);
}
}
return result;
}
int main() {
std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::vector<int> evens = filterEven(nums);
for (int num : evens) {
std::cout << num << " ";
}
std::cout << std::endl; // 2 4 6 8 10
return 0;
}
Go 구현:
// Go: 짝수 필터링
package main
import "fmt"
func filterEven(numbers []int) []int {
result := make([]int, 0, len(numbers)) // 용량 예약
for _, num := range numbers {
if num%2 == 0 {
result = append(result, num)
}
}
return result
}
func main() {
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
evens := filterEven(nums)
fmt.Println(evens) // [2 4 6 8 10]
}
비교:
- C++:
reserve(),push_back() - Go:
make(),append() - 개념은 유사하지만 Go가 더 간결
과제 5: 맵 사용 (단어 빈도 계산)
C++ 구현:
// C++: 단어 빈도
#include <iostream>
#include <map>
#include <string>
#include <vector>
int main() {
std::vector<std::string> words = {
"apple", "banana", "apple", "cherry", "banana", "apple"
};
std::map<std::string, int> freq;
for (const auto& word : words) {
freq[word]++; // 없으면 0으로 초기화 후 증가
}
for (const auto& [word, count] : freq) { // C++17 구조화 바인딩
std::cout << word << ": " << count << "\n";
}
return 0;
}
Go 구현:
// Go: 단어 빈도
package main
import "fmt"
func main() {
words := []string{"apple", "banana", "apple", "cherry", "banana", "apple"}
freq := make(map[string]int)
for _, word := range words {
freq[word]++ // 제로 값(0)에서 시작
}
for word, count := range freq {
fmt.Printf("%s: %d\n", word, count)
}
}
// 출력 (순서는 무작위):
// apple: 3
// banana: 2
// cherry: 1
비교:
- 문법이 거의 동일
- Go의 제로 값 덕분에 초기화 걱정 없음
- C++17 구조화 바인딩 vs Go의 기본 문법
8. 정리: Day 1~2 학습 체크리스트
완료해야 할 항목
- Go 설치 및
go version확인 - Hello World 프로그램 작성 및 실행 (
go run,go build) -
:=와var의 차이 이해 - 제로 값 개념 숙지 (int → 0, string → "", bool → false)
-
for하나로 모든 반복 처리하기 (while, 무한 루프, range) -
if초기화 구문 활용 (변수 스코프 제한) -
switch자동 break 이해 -
go fmt로 코드 포맷팅 자동화 -
go mod·패키지 경로·Exported 이름 규칙 이해 - 실습 과제 5개 완료
C++에서 버려야 할 것
- ❌
while,do-while문법 →for하나로 통일 - ❌ 변수 초기화 걱정 → 제로 값 자동
- ❌
new/delete수동 관리 → GC 자동 - ❌
switch에서break필수 → 자동 break - ❌ 코드 스타일 논쟁 →
go fmt자동 - ❌
new/delete수동 관리 - ❌ 코딩 스타일 논쟁
Go에서 새로 익혀야 할 것
- ✅
:=짧은 선언 (함수 내부 전용) - ✅
for하나로 모든 반복 (while, 무한 루프, range) - ✅
range로 컨테이너 순회 (인덱스, 값 동시 접근) - ✅
if,switch초기화 구문 (변수 스코프 제한) - ✅
go fmt자동 포맷팅 (스타일 통일) - ✅ GC 신뢰하기 (메모리 관리 자동)
C++ vs Go 핵심 비교표
| 개념 | C++ | Go | Go의 장점 |
|---|---|---|---|
| 변수 선언 | auto, int, const, constexpr | var, :=, const | 단순함 |
| 초기화 | 쓰레기 값 가능 | 제로 값 자동 | 안전 |
| 반복문 | for, while, do-while | for 하나 | 일관성 |
| 조건문 | 괄호 필수 | 괄호 없음 | 간결 |
| switch | break 필수 | 자동 break | 안전 |
| 메모리 관리 | 수동 (new/delete) | GC 자동 | 생산성 |
| 코드 스타일 | 팀마다 다름 | go fmt 통일 | 협업 |
9. 다음 단계
Day 1~2에서는 Go의 기본 문법과 철학을 배웠습니다.
다음 글에서 배울 내용:
- 포인터와 메모리 (C++의
*,&와 비교) - 배열 vs 슬라이스 (동적 배열)
- 맵 (Map) - 해시 테이블
- 구조체 (Struct) - 사용자 정의 타입
📚 시리즈 네비게이션
| 이전 글 | 목차 | 다음 글 |
|---|---|---|
| ← 커리큘럼 소개 | 📑 전체 목차 | #02 자료구조 → |
Go 2주 완성 시리즈: 커리큘럼 • #01 기본 문법 • #02 자료구조 • #03 객체지향 • #04 인터페이스 • #05 에러 처리 • #06 고루틴·채널 • #07 테스팅 • #08 REST API • #09 context·우아한 종료
한 줄 요약: Go는 C++의 복잡함을 덜어내고 심플함에 집중한 언어입니다. :=, for, go fmt만 익히면 첫 단계는 끝입니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- Go 2주 완성 시리즈 전체 목차
- C++ 개발자를 위한 2주 완성 Go 언어(Golang) 마스터 커리큘럼
- C++ 개발자의 뇌 구조로 이해하는 Go 언어 [#47-2]
- C++ vs Go | 성능·동시성·선택 가이드 완전 비교 [#47-1]
이 글에서 다루는 키워드 (관련 검색어)
Go 입문, Golang 설치, := 짧은 선언, Go for문, range, 가비지 컬렉터, go fmt, C++ Go 비교, Go 2주 완성, Golang 기본 문법 등으로 검색하시면 이 글이 도움이 됩니다.
실전 팁
실무에서 바로 적용할 수 있는 팁입니다.
디버깅 팁
- 문제가 발생하면 먼저 컴파일러 경고를 확인하세요
- 간단한 테스트 케이스로 문제를 재현하세요
성능 팁
- 프로파일링 없이 최적화하지 마세요
- 측정 가능한 지표를 먼저 설정하세요
코드 리뷰 팁
- 코드 리뷰에서 자주 지적받는 부분을 미리 체크하세요
- 팀의 코딩 컨벤션을 따르세요
실전 체크리스트
실무에서 이 개념을 적용할 때 확인해야 할 사항입니다.
코드 작성 전
- 이 기법이 현재 문제를 해결하는 최선의 방법인가?
- 팀원들이 이 코드를 이해하고 유지보수할 수 있는가?
- 성능 요구사항을 만족하는가?
코드 작성 중
- 컴파일러 경고를 모두 해결했는가?
- 엣지 케이스를 고려했는가?
- 에러 처리가 적절한가?
코드 리뷰 시
- 코드의 의도가 명확한가?
- 테스트 케이스가 충분한가?
- 문서화가 되어 있는가?
이 체크리스트를 활용하여 실수를 줄이고 코드 품질을 높이세요.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. C++의 auto와 while을 Go의 := 와 for로 전환하기. 가비지 컬렉터 도입으로 new/delete에서 해방되는 첫 단계. 변수 선언, 반복문, 조건문을 C++ 코드와 직접 비교하며 배웁니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
관련 글
- C++ 개발자를 위한 2주 완성 Go 언어(Golang) 마스터 커리큘럼
- C++ vs Go | 성능·동시성·선택 가이드 완전 비교 [#47-1]
- C++ 개발자의 뇌 구조로 이해하는 Go 언어 [#47-2]
- [Go 2주 완성 #02] Day 3~4: 메모리와 자료구조 - 포인터 연산은 없지만 포인터는 있다
- [Go 2주 완성 #03] Day 5~6: 클래스 없는 객체지향 - 상속을 버리고 합성을 취하다