변수 선언
변수란?
변수(Variable)는 프로그램 실행 중에 값을 저장하는 메모리 공간에 붙인 이름입니다. Go에서 변수는 선언과 동시에 타입이 결정 되며, 한 번 타입이 정해지면 바꿀 수 없습니다. 이를 정적 타입(static typing)이라고 합니다.
Go의 변수 시스템은 두 가지 큰 특징이 있습니다.
첫째, 零값(zero value): 변수를 선언하면 초기값을 지정하지 않아도 타입에 맞는 기본값이 자동으로 할당됩니다. C언어처럼 쓰레기 값이 들어오지 않습니다.
둘째, 선언 후 반드시 사용: Go 컴파일러는 선언만 하고 사용하지 않은 변수를 컴파일 에러로 처리합니다. 불필요한 변수를 방지해 코드 품질을 높이는 Go의 철학입니다.
var 키워드로 변수 선언
Go에서 가장 기본적인 변수 선언 방법은 var 키워드입니다.
package main
import "fmt"
func main() {
// 1. 타입과 초기값 모두 지정
var name string = "Gopher"
var age int = 30
var score float64 = 98.5
// 2. 타입만 지정 (초기값은 零값으로 자동 설정)
var count int // 0
var isActive bool // false
var message string // ""
// 3. 초기값만 지정 (타입 추론 — Go가 타입을 자동으로 결정)
var pi = 3.14159 // float64로 추론
var greeting = "Hi" // string으로 추론
var flag = true // bool로 추론
fmt.Println(name, age, score)
fmt.Println(count, isActive, message)
fmt.Println(pi, greeting, flag)
}
패키지 레벨(함수 밖)에서도 var로 선언할 수 있습니다.
package main
import "fmt"
// 패키지 레벨 변수 (전역 변수)
var AppName = "MyApp"
var Version = "1.0.0"
var MaxConnections int = 100
func main() {
fmt.Printf("%s v%s (max connections: %d)\n", AppName, Version, MaxConnections)
}
var 블록 선언
여러 변수를 한꺼번에 선언할 때는 블록 형태를 사용합니다.
package main
import "fmt"
var (
host = "localhost"
port = 8080
debug = false
maxRetry = 3
)
func main() {
fmt.Printf("Server: %s:%d (debug=%v, maxRetry=%d)\n", host, port, debug, maxRetry)
}
:= 단축 선언
:=는 함수 내부에서만 사용할 수 있는 단축 선언 연산자입니다. var 키워드와 타입 선언을 생략하고 초기값으로부터 타입을 추론합니다. Go 코드에서 가장 많이 사용하는 선언 방식입니다.
package main
import "fmt"
func main() {
// := 로 선언 (var + 타입 추론 + 초기화를 한 번에)
name := "Gopher"
age := 30
score := 98.5
active := true
fmt.Println(name, age, score, active)
// 기존 변수에 새 값 할당 (= 사용)
age = 31
score = 99.0
fmt.Println(name, age, score)
}
var vs := 비교
| 구분 | var | := |
|---|---|---|
| 사용 위치 | 패키지 레벨 + 함수 내부 | 함수 내부만 |
| 타입 명시 | 가능 (선택) | 불가능 (항상 추론) |
| 초기값 없음 | 가능 (零값 사용) | 불가능 (초기값 필수) |
| 코드 스타일 | 명시적 | 간결 |
package main
import "fmt"
func main() {
// var - 타입을 명시하고 싶을 때, 초기값 없을 때
var buffer []byte // nil slice
var result map[string]int // nil map
var err error // nil error
// := - 함수 내부에서 빠르게 선언할 때
message := "hello"
count := 0
items := []string{"a", "b", "c"}
fmt.Println(buffer, result, err)
fmt.Println(message, count, items)
}
零값 (Zero Value)
Go에서는 변수를 초기화하지 않으면 해당 타입의 零값이 자동으로 할당됩니다.
| 타입 | 零값 |
|---|---|
| int, int8, int16, int32, int64 | 0 |
| uint, uint8, uint16, uint32, uint64 | 0 |
| float32, float64 | 0.0 |
| complex64, complex128 | (0+0i) |
| bool | false |
| string | "" (빈 문자열) |
| pointer | nil |
| slice | nil |
| map | nil |
| channel | nil |
| function | nil |
| interface | nil |
package main
import "fmt"
func main() {
var i int
var f float64
var b bool
var s string
var p *int
var sl []int
var m map[string]int
fmt.Printf("int: %v\n", i)
fmt.Printf("float64: %v\n", f)
fmt.Printf("bool: %v\n", b)
fmt.Printf("string: %q\n", s) // %q로 빈 문자열 확인
fmt.Printf("pointer: %v\n", p)
fmt.Printf("slice: %v (nil: %v)\n", sl, sl == nil)
fmt.Printf("map: %v (nil: %v)\n", m, m == nil)
}
실행 결과:
int: 0
float64: 0
bool: false
string: ""
pointer: <nil>
slice: [] (nil: true)
map: map[] (nil: true)
다중 변수 선언과 다중 할당
Go는 한 줄에 여러 변수를 동시에 선언하거나 할당할 수 있습니다.
package main
import "fmt"
func main() {
// 다중 선언 (같은 타입)
var x, y, z int
fmt.Println(x, y, z) // 0 0 0
// 다중 선언 + 초기화
var a, b, c = 1, 2, 3
fmt.Println(a, b, c) // 1 2 3
// := 다중 선언
name, age, score := "Alice", 25, 95.5
fmt.Println(name, age, score)
// 다중 할당 (스왑 패턴 — Go의 우아한 방법)
p, q := 10, 20
fmt.Println("before:", p, q) // before: 10 20
p, q = q, p // 임시 변수 없이 스왑!
fmt.Println("after:", p, q) // after: 20 10
}
:= 재선언 (redeclaration)
:=를 사용할 때 이미 선언된 변수가 있어도, 적어도 하나의 새 변수 가 포함되면 컴파일됩니다.
package main
import "fmt"
func main() {
x := 1
fmt.Println(x)
// x는 이미 선언됐지만 y가 새로 선언되므로 OK
x, y := 2, 3
fmt.Println(x, y)
// 이 경우 x에는 새 값이 재할당됨
x, y = 10, 20
fmt.Println(x, y)
}
변수 스코프
Go의 변수 스코프는 블록({}) 단위입니다. 내부 블록은 외부 블록의 변수를 참조할 수 있지만, 반대는 불가능합니다.
package main
import "fmt"
// 패키지 레벨 변수
var globalVar = "I am global"
func main() {
// 함수 레벨 변수
funcVar := "I am in main"
if true {
// 블록 레벨 변수
blockVar := "I am in if block"
fmt.Println(globalVar) // 접근 가능
fmt.Println(funcVar) // 접근 가능
fmt.Println(blockVar) // 접근 가능
}
// blockVar는 여기서 접근 불가 (컴파일 에러)
// fmt.Println(blockVar) // 에러!
for i := 0; i < 3; i++ {
loopVar := i * 2
fmt.Println(loopVar) // 접근 가능
}
// i, loopVar는 여기서 접근 불가
}
섀도잉 (Shadowing) 주의
외부 변수와 같은 이름으로 내부 블록에 선언하면 섀도잉이 발생합니다. 의도치 않은 버그를 유발할 수 있으므로 주의가 필요합니다.
package main
import "fmt"
var value = 100 // 패키지 레벨
func main() {
fmt.Println(value) // 100
value := 200 // 함수 레벨에서 섀도잉!
fmt.Println(value) // 200 (패키지 레벨 value가 가려짐)
{
value := 300 // 블록 레벨에서 다시 섀도잉!
fmt.Println(value) // 300
}
fmt.Println(value) // 200 (함수 레벨 value로 돌아옴)
}
선언했지만 사용하지 않은 변수 — 컴파일 에러
Go는 사용하지 않는 변수를 선언하면 컴파일 에러 를 발생시킵니다. 이는 코드 품질을 강제로 높이는 Go의 철학적 선택입니다.
package main
func main() {
x := 10
// x를 사용하지 않으면:
// ./main.go:4:2: x declared and not used
_ = x // 이렇게 쓰면 컴파일 에러 방지 가능
}
단, 패키지 레벨 변수 는 사용하지 않아도 컴파일 에러가 발생하지 않습니다.
빈 식별자 _ (Blank Identifier)
_(언더스코어)는 특별한 빈 식별자로, 값을 무시할 때 사용합니다. 선언했지만 사용하지 않아도 컴파일 에러가 없습니다.
package main
import "fmt"
// 다중 반환값을 가진 함수
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
}
func getCoordinates() (x, y, z float64) {
return 1.0, 2.5, 3.7
}
func main() {
// 에러를 무시하고 결과만 받기 (실제로는 에러 처리를 권장)
result, _ := divide(10.0, 3.0)
fmt.Printf("결과: %.4f\n", result)
// 필요 없는 반환값 무시
x, _, z := getCoordinates()
fmt.Printf("x=%.1f, z=%.1f (y는 무시)\n", x, z)
}
실전 예제: 함수 다중 반환값 처리
Go는 다중 반환값을 지원합니다. 에러 처리 패턴에서 자주 사용됩니다.
package main
import (
"fmt"
"strconv"
)
// 문자열을 정수로 변환 (에러 반환)
func parseAge(s string) (int, error) {
age, err := strconv.Atoi(s)
if err != nil {
return 0, fmt.Errorf("잘못된 나이 값 %q: %w", s, err)
}
if age < 0 || age > 150 {
return 0, fmt.Errorf("나이 범위 초과: %d", age)
}
return age, nil
}
func main() {
inputs := []string{"25", "abc", "-5", "200", "42"}
for _, input := range inputs {
age, err := parseAge(input)
if err != nil {
fmt.Printf("에러: %v\n", err)
continue
}
fmt.Printf("나이: %d세\n", age)
}
}
실행 결과:
나이: 25세
에러: 잘못된 나이 값 "abc": strconv.Atoi: parsing "abc": invalid syntax
에러: 나이 범위 초과: -5
에러: 나이 범위 초과: 200
나이: 42세
루프에서 인덱스 또는 값 무시
package main
import "fmt"
func main() {
fruits := []string{"apple", "banana", "cherry", "date"}
// 인덱스 무시 — 값만 필요할 때
fmt.Println("=== 과일 목록 ===")
for _, fruit := range fruits {
fmt.Println("-", fruit)
}
// 값 무시 — 인덱스만 필요할 때
fmt.Println("\n=== 인덱스 목록 ===")
for i := range fruits {
fmt.Printf("인덱스 %d\n", i)
}
// 맵 순회에서 값 무시 (키 존재 여부 확인)
seen := map[string]bool{
"go": true, "python": true, "rust": true,
}
languages := []string{"go", "java", "rust", "c++"}
for _, lang := range languages {
if _, exists := seen[lang]; exists {
fmt.Printf("%s: 학습 목록에 있음\n", lang)
} else {
fmt.Printf("%s: 학습 목록에 없음\n", lang)
}
}
}
고수 팁
팁 1: 짧은 변수명은 짧은 스코프에서만
Go 커뮤니티 관례상 스코프가 짧으면 i, v, k 같은 짧은 이름을 씁니다. 스코프가 길면 의미 있는 이름을 사용하세요.
// 루프 변수 — 짧은 이름 OK
for i, v := range items { ... }
// 패키지 레벨 — 명확한 이름 사용
var maxConnectionPoolSize = 100
var defaultRequestTimeout = 30
팁 2: 초기값이 명확할 때는 := 사용
// Good
conn, err := net.Dial("tcp", "localhost:8080")
defer conn.Close()
// 불필요하게 verbose
var conn net.Conn
var err error
conn, err = net.Dial("tcp", "localhost:8080")
팁 3: 패키지 레벨 변수는 최소화
패키지 레벨 변수는 전역 상태를 만들어 테스트와 유지보수를 어렵게 합니다. 가능하면 함수 인수나 구조체 필드로 전달하세요.
팁 4: 複합 선언으로 관련 변수를 묶기
var (
// DB 설정
dbHost = "localhost"
dbPort = 5432
dbName = "myapp"
// 서버 설정
serverPort = 8080
serverTimeout = 30
)