본문으로 건너뛰기

변수 선언

변수란?​

변수(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, int640
uint, uint8, uint16, uint32, uint640
float32, float640.0
complex64, complex128(0+0i)
boolfalse
string"" (빈 문자열)
pointernil
slicenil
mapnil
channelnil
functionnil
interfacenil
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
)