[Go 2주 완성 #01] Day 1~2: Go 언어의 철학과 기본 문법 - C++ 개발자의 첫인상
이 글의 핵심
C++에 익숙하면 Go 코드가 너무 단순해 보여 오히려 무엇을 놓치고 있는지 불안해집니다. 이 시리즈 첫 편은 같은 문제를 여러 방식으로 풀 수 있는 C++와 달리 한 가지 관용구를 택하는 Go의 방향을 설명하고, 변수 선언과 반복문, 조건문을 C++ 코드와 나란히 비교해 2주 학습의 출발점을 잡습니다.
시리즈 안내
📚 Go 2주 완성 시리즈 #01 | 전체 목차 보기
이 글은 C++ 개발자를 위한 2주 완성 Go 언어 커리큘럼의 Day 1~2 내용입니다.
이전: 커리큘럼 소개 ← | → 다음: #02 메모리와 자료구조
💡 초보자를 위한 한 줄: Go는 C++보다 훨씬 간단합니다. 헤더 파일 없음,
new/delete없음(GC 자동),while없음(for로 통합), 클래스 상속 없음(합성 사용).:=는 C++의auto처럼 타입 추론하는 변수 선언입니다.go build만 하면 단일 실행 파일이 나옵니다.
들어가며: “C++과는 완전히 다른 언어입니다…”
C++ 개발자로서 Go를 처음 접하면 “이게 정말 프로그래밍 언어인가?” 싶을 정도로 심플합니다.
// C++: 이런 게 필요했죠
#include <iostream>
#include <vector>
#include <memory>
class MyClass {
std::unique_ptr<Data> data_;
public:
MyClass() : data_(std::make_unique<Data>()) {}
~MyClass() { /* cleanup */ }
};
Go에서는:
// Go: 이게 전부입니다
type MyClass struct {
data *Data
}
// GC가 알아서 정리함, 소멸자 필요 없음
헤더 파일도 없고, 템플릿 메타프로그래밍도 없으며, new/delete도 신경 쓸 필요가 없습니다. 이 글에서는 Go 언어의 철학과 기본 문법을 C++ 개발자의 시선으로 살펴봅니다.
이 글에서 배울 내용:
- Go 설치 및 기본 툴체인 (
go build,go run,go fmt) - Go의 철학(단순성·명시성·동시성)과 C++와의 차이(포인터·상속·제네릭)
- 패키지·모듈(
go mod, import,internal) 개요 - 변수 선언:
autovs:=의 차이 - 반복문:
while이 없는 세계 - 가비지 컬렉터(GC)로 메모리 관리 자동화
C++ 개발자 관점: C++ 백그라운드에서 Go로 전환하며 겪은 차이점과 함정을 중심으로 설명합니다. 포인터, 동시성, 메모리 관리 등 핵심 개념을 비교하며 정리했습니다.
실무에서의 체감
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 설치
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
변수 선언: 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
}
반복문: 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(명확)
조건문과 기본 제어 흐름
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 | 불가 | 가능 |
| 여러 값 | 여러 줄 필요 | 쉼표로 한 줄 |
가비지 컬렉터: 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의 장단점:
GC 덕분에 delete를 잊어 생기는 메모리 누수나, 이미 해제된 메모리를 가리키는 댕글링 포인터 같은 버그는 언어 차원에서 사라집니다. 메모리 해제 시점을 코드로 관리할 필요가 없으니 비즈니스 로직에 집중하기도 쉽습니다.
대가도 있습니다. GC가 도는 동안 짧은 일시 정지가 생기고(Go의 GC는 지연 시간을 줄이는 쪽으로 튜닝되어 있지만 0은 아닙니다), 회수 전까지 메모리를 들고 있으므로 같은 일을 하는 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[] | 불필요 |
| 메모리 누수 | 가능 | 거의 불가능 |
| 댕글링 포인터 | 가능 | 불가능 |
| 성능 | 최고 | 약간 낮음 |
| 안전성 | 낮음 | 높음 |
| 개발 속도 | 느림 | 빠름 |
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가 저장할 때 자동으로 실행하므로 코드 리뷰에서 들여쓰기나 괄호 위치를 두고 논쟁할 일이 사라집니다.
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 프로젝트를 보면 모두 같은 스타일입니다!
실습 과제
과제 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의 기본 문법
정리: 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자동
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 통일 | 협업 |
다음 단계
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만 익히면 첫 단계는 끝입니다.
자주 묻는 질문 (FAQ)
Q. var와 := 중 언제 무엇을 쓰나요?
A. 함수 안에서 초기값으로 타입이 분명하면 :=를 쓰고, 패키지 레벨 변수처럼 :=를 쓸 수 없는 곳이나 제로 값으로 시작한다는 의도를 드러내고 싶을 때는 var를 씁니다. x := 0은 항상 int가 되므로 int64나 time.Duration처럼 특정 타입이 필요하면 var x int64 = 0처럼 타입을 명시하는 편이 안전합니다. C++에서 넘어오면 auto처럼 :=만 쓰기 쉬운데, 같은 스코프에서 이미 선언된 변수를 :=로 가리는 섀도잉 실수도 함께 조심해야 합니다.