Go에서 변수를 선언하는 방법은 두 가지입니다. 어디서나 쓸 수 있는 var, 그리고 함수 안에서만 쓸 수 있고 타입을 추론하는 짧은 형태 :=입니다.
모든 변수는 컴파일 타임에 정해지는 고정된 타입을 가집니다. 값을 주면 Go가 그 값에서 타입을 추론하므로, 타입까지 적는 것(var city string = "Lisbon")은 문법상 맞지만 중복입니다.
var와 :=
var | := | |
|---|---|---|
| 위치 | 패키지 수준과 함수 안 | 함수 안에서만 |
| 타입 | 선택, 값이 없으면 필수 | 항상 추론 |
| 초기값 | 선택(없으면 제로 값) | 필수 |
| 재선언 | 불가 | 왼쪽 변수 중 하나 이상이 새 변수면 가능 |
함수 안에서의 관용적인 선택:
- 시작 값이 있으면
x := value. - 제로 값을 원하면
var x T. "일부러 비워 둔 것"이라는 신호가 됩니다:var buf bytes.Buffer,var total int. - 추론되는 타입이 원하는 타입이 아니면
var x T = value. 예를 들어var ratio float64 = 1(그냥ratio := 1이라고 쓰면int가 됩니다).
함수 밖의 :=는 문법 오류입니다:
./main.go:5:1: syntax error: non-declaration statement outside function body
제로 값
값 없이 선언된 변수에 쓰레기 값이 들어 있는 일은 없습니다. 그 타입의 제로 값을 가집니다:
| 타입 | 제로 값 |
|---|---|
| 정수, 실수 | 0 |
bool | false |
string | "" |
| 포인터, 슬라이스, 맵, 채널, 함수, 인터페이스 | nil |
| 배열, 구조체 | 모든 요소나 필드가 각자의 제로 값 |
좋은 Go API는 제로 값이 쓸모 있도록 설계됩니다. 제로 값 sync.Mutex는 잠기지 않은 상태이고, 제로 값 bytes.Buffer는 비어 있으며 바로 쓸 수 있고, len, range, append는 모두 nil 슬라이스에서 동작합니다. 주의할 것은 맵입니다. nil 맵을 읽으면 제로 값이 나오지만, 쓰면 패닉이 납니다. 값을 저장하기 전에 make나 리터럴로 맵을 만드세요.
변수 여러 개 선언하기
var ( ... ) 블록은 관련된 패키지 수준 변수를 묶습니다. x, y = y, x 같은 다중 대입에서는 대입하기 전에 오른쪽 표현식을 모두 평가하므로 값 교환이 동작합니다.
이름이 여럿인 :=는 왼쪽 이름 중 하나 이상이 새 이름이면 문제없이 동작합니다. 같은 스코프에 이미 있는 이름에는 그냥 대입됩니다. 그래서 err를 재사용할 수 있습니다:
f, err := os.Open("a.txt") // declares f and err
g, err := os.Open("b.txt") // declares g, assigns to the existing err
새 이름이 하나도 없으면 오류입니다:
./main.go:7:4: no new variables on left side of :=
이미 있는 변수에 대입할 때는 그냥 =를 쓰세요.
사용하지 않는 변수는 오류다
선언만 하고 한 번도 읽지 않는 지역 변수가 있으면 빌드가 멈춥니다:
./main.go:9:2: declared and not used: x
끌 수 있는 경고가 아닙니다. 변수를 지우거나, 사용하거나, 디버깅 중이라면 빈 식별자로 조용히 만드세요: _ = x. 빈 식별자는 필요 없는 반환 값을 버릴 때도 씁니다: _, err := fmt.Println("hi").
패키지 수준 변수와 함수 매개변수는 이 규칙에서 예외입니다.
스코프와 섀도잉
변수는 선언된 지점부터 감싸는 블록({ ... })의 끝까지 존재합니다. 안쪽 블록의 :=는 바깥에 같은 이름의 변수가 있어도 새 변수를 만듭니다. 바깥 변수는 바뀌지 않고 가려집니다:
이 프로그램은 inside: 42를 출력한 다음 outside: 0을 출력합니다. if 안의 :=는 count와 err를 모두 안쪽 블록의 새 변수로 선언했습니다. 둘 다 그 블록에는 아직 없었기 때문입니다. 깔끔하게 컴파일되며, "내 값이 사라졌다" 버그의 전형적인 원인입니다.
해결책은 err를 먼저 선언하고 =로 대입하는 것입니다:
var err error
count, err = strconv.Atoi(input)
go vet은 기본적으로 섀도잉을 보고하지 않습니다. 별도의 shadow 분석기가 보고하며, golangci-lint 같은 린터에서 켤 수 있습니다.
이름 짓기
camelCase를 씁니다:maxRetries,userID.max_retries는 쓰지 않습니다.- 약어는 대소문자를 통일합니다:
userID,httpClient,parseURL.userId는 쓰지 않습니다. - 스코프가 짧으면 이름도 짧게: 반복문의
i, 리더의r, 오류의err. 패키지 수준 변수에는 길고 설명적인 이름을 씁니다. - 패키지 수준의 대문자 이름(
var Timeout)은 다른 패키지로 공개됩니다. 패키지와 import 페이지를 참고하세요.
패키지 수준 변수
함수 밖에서 선언된 변수는 프로그램 전체 기간 동안 살아 있고, main이 실행되기 전에 의존성 순서대로 초기화됩니다. 설정이나 캐시에 편리하며, 모든 고루틴이 공유합니다. 둘 이상의 고루틴이 같은 변수에 쓴다면 뮤텍스로 보호하거나 sync/atomic을 쓰세요. 그렇지 않으면 데이터 레이스가 생깁니다.
바뀌지 않는 값에는 상수를 쓰세요. const는 실수로 수정될 수 없고 런타임 비용도 없습니다.
자주 묻는 질문
Go에서 var와 :=의 차이는 무엇인가요?
:=는 타입을 추론해 변수를 선언하고 초기화하며, 함수 안에서만 쓸 수 있습니다: count := 10. var는 패키지 수준을 포함해 어디서나 쓸 수 있고, 타입을 명시할 수 있으며, 초기값을 생략하면 제로 값이 들어갑니다: var count int. 함수 안에서 관용적인 Go는 초기값이 있으면 :=, 제로 값을 원하면 var를 씁니다.
Go에서 제로 값이란 무엇인가요?
값 없이 선언된 모든 변수는 그 타입의 제로 값을 가집니다. 숫자는 0, bool은 false, string은 "", 포인터, 슬라이스, 맵, 채널, 함수, 인터페이스는 nil입니다. 구조체는 각 필드의 제로 값을 가집니다. Go에는 초기화되지 않은 변수가 없습니다.
Go에서 "declared and not used" 오류는 어떻게 고치나요?
Go는 대입만 하고 한 번도 읽지 않는 지역 변수를 거부합니다. 사용하거나, 지우거나, 디버깅 중이라면 _ = x로 빈 식별자에 대입하세요. 패키지 수준 변수와 함수 매개변수는 예외입니다.
Go에서 전역 변수는 어떻게 선언하나요?
함수 밖, 패키지 수준에서 var를 씁니다: var counter int. 거기서는 짧은 형태 :=를 쓸 수 없습니다. 대문자 이름(var Counter int)은 다른 패키지에서 보이고, 소문자 이름은 자기 패키지 안에서만 보입니다. 고루틴이 쓰는 패키지 수준 변수는 뮤텍스로 보호하세요.