Go에서 변수와 상수 이해하기 - 선언, 초기화 및 범위
Min-jun Kim
Dev Intern · Leapcell

Go의 간결함과 효율성은 부분적으로 변수 및 상수 처리에 대한 직관적인 접근 방식에 기인합니다. 다른 일부 언어와 달리 Go는 명확성과 명시적 선언을 강조하여 모호성을 최소화합니다. 이 문서는 Go 프로그래밍 언어 내에서 변수 및 상수 선언, 초기화 및 중요 측면인 범위의 핵심 개념을 살펴봅니다.
변수: 변경 가능한 데이터 컨테이너
변수는 데이터를 담는 명명된 저장 위치이며, 프로그램 실행 중에 값이 변경될 수 있습니다. Go에서는 모든 변수에 유형이 있어야 합니다. 이 유형은 변수가 저장할 수 있는 데이터의 종류와 변수에 대해 수행할 수 있는 작업을 결정합니다.
변수 선언
Go는 변수를 선언하는 여러 가지 방법을 제공하며, 각 방법마다 고유한 사용 사례가 있습니다.
1. var
키워드를 사용한 명시적 선언
변수를 선언하는 가장 장황한 방법은 var
키워드를 사용하고 변수 이름과 유형을 지정하는 것입니다.
package main import "fmt" func main() { var age int // 'age'라는 정수 변수를 선언합니다 var name string // 'name'이라는 문자열 변수를 선언합니다 var isGoProgram bool // 'isGoProgram'이라는 불리언 변수를 선언합니다 fmt.Println("기본 나이:", age) fmt.Println("기본 이름:", name) fmt.Println("기본 isGoProgram:", isGoProgram) }
관찰: var
를 사용하여 명시적 초기화 없이 변수를 선언하면 Go는 자동으로 "제로 값"을 할당합니다.
int
:0
string
:""
(빈 문자열)bool
:false
- 포인터:
nil
- 슬라이스, 맵, 채널:
nil
2. 초기화를 사용한 명시적 선언
=
연산자를 사용하여 선언 중에 변수를 직접 초기화할 수 있습니다.
package main import "fmt" func main() { var count int = 10 // 'count'를 10으로 선언하고 초기화합니다 var message string = "Hello, Go!" // 'message'를 선언하고 초기화합니다 fmt.Println("개수:", count) fmt.Println("메시지:", message) }
이 경우 제로 값이 사용되지 않습니다. 변수에 특정 값이 즉시 할당되기 때문입니다.
3. var
를 사용한 유형 추론
Go의 강력한 유형 시스템은 유형을 명시적으로 상태로 요구하지 않습니다. 특히 초기 값에서 유형을 추론할 수 있는 경우에 그렇습니다.
package main import "fmt" func main() { var price = 99.99 // Go는 'price'가 float64 유형으로 추론합니다 var city = "New York" // Go는 'city'가 string 유형으로 추론합니다 var pi = 3.14159 // Go는 'pi'가 float64 유형으로 추론합니다 fmt.Printf("가격: %f (유형: %T)\n", price, price) fmt.Printf("도시: %s (유형: %T)\n", city, city) fmt.Printf("파이: %f (유형: %T)\n", pi, pi) }
여기서 Go는 부동 소수점 리터럴이 기본적으로 float64
이기 때문에 price
와 pi
를 float64
로 추론합니다. 마찬가지로 문자열 리터럴은 string
입니다.
4. 짧은 변수 선언 (:=
)
이것은 Go 함수 내에서 변수를 선언하고 초기화하는 가장 일반적인 방법입니다. :=
연산자는 선언 및 초기화의 바로 가기이며 함수 내에서만 작동합니다. 패키지 수준에서는 사용할 수 없습니다.
package main import "fmt" func main() { // 짧은 변수 선언 score := 100 // Go는 'score'를 int로 추론합니다 isValid := true // Go는 'isValid'를 bool로 추론합니다 greeting := "Welcome!" // Go는 'greeting'을 string으로 추론합니다 // 한 줄에 여러 변수를 선언할 수 있습니다 x, y := 1, 2.5 // x는 int, y는 float64입니다 name, age := "Alice", 30 // name은 string, age는 int입니다 fmt.Println("점수:", score) fmt.Println("유효함:", isValid) fmt.Println("인사:", greeting) fmt.Println("X:", x, "Y:", y) fmt.Println("이름:", name, "나이:", age) // 동일한 범위 내에서 재선언은 허용되지 않습니다. 단, 새 변수가 있는 경우는 제외합니다. // greeting := "Hello" // 오류: :=의 왼쪽에 새 변수가 없습니다. // 하지만 'error'가 새 변수인 경우에는 허용됩니다. // 여기서 err는 처음으로 선언됩니다. file, err := openFile("example.txt") if err != nil { fmt.Println("파일 열기 오류:", err) } else { fmt.Println("파일 열림:", file) } // 이것도 허용됩니다. 기존 'err' 변수가 재할당되고 'data'는 새 변수입니다. data, err := readFile(file) if err != nil { fmt.Println("파일 읽기 오류:", err) } else { fmt.Println("파일 데이터:", data) } } // :=를 여러 반환 값과 함께 설명하기 위한 더미 함수 func openFile(filename string) (string, error) { if filename == "example.txt" { return "file handle", nil } return "", fmt.Errorf("파일을 찾을 수 없습니다") } func readFile(handle string) (string, error) { if handle == "file handle" { return "some content", nil } return "", fmt.Errorf("잘못된 핸들입니다") }
:=
연산자는 로컬 변수 선언에 매우 편리합니다. 코드를 단순화하고 Go의 뛰어난 유형 추론에 의존합니다.
변수 재할당
선언된 후에는 =
연산자를 사용하여 변수의 값을 변경(재할당)할 수 있습니다.
package main import "fmt" func main() { count := 5 fmt.Println("초기 개수:", count) count = 10 // 'count'의 값 재할당 fmt.Println("새 개수:", count) // 다른 유형 할당은 허용되지 않습니다. // count = "hello" // 오류: "hello" (type string)를 할당에서 int 유형으로 사용할 수 없습니다. }
사용되지 않는 변수
Go는 사용되지 않는 변수에 대해 엄격합니다. 변수를 선언하고 사용하지 않으면 컴파일 시간 오류가 발생합니다. 이는 깔끔한 코드를 유지하고 논리 오류를 방지하는 데 도움이 됩니다.
package main func main() { // var unusedVar int // 오류: unusedVar가 선언되었지만 사용되지 않았습니다. // _ = unusedVar // 이 줄은 변수를 "사용"하여 오류를 방지합니다. }
빈 식별자 _
는 값을 명시적으로 폐기하는 데 사용할 수 있으며, 함수가 여러 값을 반환하지만 일부만 필요한 경우 종종 유용합니다.
상수: 변경 불가능한 데이터 홀더
상수는 값을 보유한다는 점에서 변수와 유사하지만, 컴파일 시간에 값이 고정되어 프로그램 실행 중에 변경될 수 없습니다. 일반적으로 사전에는 알지만 변하지 않는 값(예: 수학 상수 또는 구성 값)에 사용됩니다.
상수 선언
Go에서 상수는 const
키워드를 사용하여 선언됩니다.
package main import "fmt" func main() { const Pi = 3.14159 // float64 상수를 선언합니다 const MaxUsers = 100 // int 상수를 선언합니다 const Greeting = "Hello, World!" // string 상수를 선언합니다 fmt.Println("Pi:", Pi) fmt.Println("최대 사용자:", MaxUsers) fmt.Println("인사:", Greeting) // Pi = 3.0 // 오류: Pi (상수)에 할당할 수 없습니다. }
변수와 유사하게 상수도 유형 추론을 활용할 수 있습니다. 유형이 지정되지 않으면 Go가 값에서 유형을 추론합니다.
package main import "fmt" func main() { const E = 2.71828 // float64로 추론됨 const Version = "1.0.0" // string으로 추론됨 fmt.Printf("E: %f (유형: %T)\n", E, E) fmt.Printf("버전: %s (유형: %T)\n", Version, Version) }
익명 상수
Go 상수의 고유한 기능은 "익명"일 수 있다는 것입니다. 이는 숫자 상수가 특정 유형을 요구하는 컨텍스트에서 사용될 때까지 처음에 고정된 유형(예: int
, float64
등)을 갖지 않음을 의미합니다. 이를 통해 상수를 보다 유연하게 사용할 수 있습니다.
package main import "fmt" func main() { const LargeNum = 1_000_000_000_000 // 익명 정수 상수 const PiValue = 3.1415926535 // 익명 부동 소수점 상수 var i int = LargeNum // LargeNum은 int로 암시적으로 변환됩니다 var f float64 = LargeNum // LargeNum은 float64로 암시적으로 변환됩니다 var complexVal complex128 = PiValue // PiValue는 complex128로 암시적으로 변환됩니다 fmt.Printf("i: %d (유형: %T)\n", i, i) fmt.Printf("f: %f (유형: %T)\n", f, f) fmt.Printf("complexVal: %v (유형: %T)\n", complexVal, complexVal) // 이 유연성은 강력합니다. LargeNum이 직접 int64 유형으로 지정되었다고 상상해 보세요: // var smallInt int = LargeNum // LargeNum이 int64이고 int보다 크면 컴파일 시간 오류가 됩니다. }
익명 상수는 값이 대상 유형에 맞는 한, 명시적인 유형 변환 없이 다양한 숫자 컨텍스트에서 사용할 수 있도록 하여 편의성을 제공합니다.
열거형 상수를 위한 iota
iota
는 const
선언에 각각 사용될 때마다 1씩 증가하는 간단한 카운터 역할을 하는 미리 선언된 식별자입니다. 관련 상수(
) 시퀀스를 만드는 데 특히 유용합니다.
package main import "fmt" func main() { const ( // iota는 0으로 시작합니다. Red = iota // Red = 0 Green // Green = 1 (암시적으로 = iota) Blue // Blue = 2 (암시적으로 = iota) ) const ( // 각 새 const 블록에 대해 iota는 0으로 재설정됩니다. Monday = iota + 1 // Monday = 1 Tuesday // Tuesday = 2 Wednesday // Wednesday = 3 Thursday Friday Saturday Sunday ) const ( _ = iota // _는 0 값을 폐기합니다. KB = 1 << (10 * iota) // KB = 1 << 10 (1024) MB // MB = 1 << 20 GB // GB = 1 << 30 TB // TB = 1 << 40 ) fmt.Println("빨강:", Red, "녹색:", Green, "파랑:", Blue) fmt.Println("월:", Monday, "화:", Tuesday, "수:", Wednesday) fmt.Println("KB:", KB, "MB:", MB, "GB:", GB, "TB:", TB) }
iota
는 집합 상수, 오류 코드 또는 요일과 같은 증분 상수를 선언하는 간결하고 읽기 쉬운 방법을 제공합니다.
범위: 변수와 상수가 보이는 곳
범위는 선언된 식별자(변수 또는 상수)에 액세스할 수 있는 프로그램 영역을 정의합니다. 이름을 충돌을 방지하고 데이터 수명을 관리하는 데 범위를 이해하는 것이 중요합니다. Go에는 패키지 범위와 블록 범위의 두 가지 기본 범위가 있습니다.
1. 패키지 범위 (전역 범위)
패키지 수준(함수, 메서드 또는 구조체 외부)에서 선언된 식별자는 패키지 범위를 갖습니다. 동일한 패키지의 모든 파일에서 볼 수 있습니다.
- 내보낸 식별자: 변수 또는 상수 이름이 대문자로 시작하면 "내보내진" 것입니다. 즉, 다른 패키지에서도 액세스할 수 있습니다.
- 내보내지 않은 식별자: 소문자로 시작하면 "내보내지 않은" 것입니다(패키지 비공개). 선언된 패키지 내에서만 액세스할 수 있습니다.
package main // 이것은 패키지 선언입니다 import "fmt" // 패키지 수준 변수/상수 var PackageVar int = 100 // 내보내기됨(대문자로 시작) const PackageConst string = "I'm a package constant" // 내보내기됨 var packagePrivateVar string = "I'm only visible in main package" // 내보내지 않음 func main() { fmt.Println("패키지 범위 변수에 액세스 중:") fmt.Println("PackageVar:", PackageVar) fmt.Println("PackageConst:", PackageConst) fmt.Println("packagePrivateVar:", packagePrivateVar) anotherFunction() } func anotherFunction() { fmt.Println("\n다른 함수에서 패키지 범위 변수에 액세스 중:") fmt.Println("PackageVar (anotherFunction에서):", PackageVar) fmt.Println("PackageConst (anotherFunction에서):", PackageConst) fmt.Println("packagePrivateVar (anotherFunction에서):", packagePrivateVar) }
2. 블록 범위 (로컬 범위)
함수, 메서드, if
문, for
루프, switch
문 또는 모든 중괄호 {}
내에서 선언된 식별자는 블록 범위를 갖습니다. 특정 블록과 해당 중첩 블록 내에서만 볼 수 있고 액세스할 수 있습니다.
package main import "fmt" var packageVar = "I'm defined at package level" func main() { // main 함수의 블록 범위에 선언된 변수 var functionScopedVar = "I'm visible only within main function" const functionScopedConst = "I'm also visible only within main function" fmt.Println(packageVar) fmt.Println(functionScopedVar) fmt.Println(functionScopedConst) if true { // if 블록 범위에 선언된 변수 blockScopedVar := "I'm visible only within this if block" fmt.Println(blockScopedVar) // 내부 범위에서 같은 이름의 변수를 다시 선언합니다. // 이것을 "섀도잉"이라고 합니다. functionScopedVar := "I'm a new variable, shadowing the outer one" fmt.Println("Inner functionScopedVar:", functionScopedVar) // 섀도잉된 것을 출력합니다. } // fmt.Println(blockScopedVar) // 오류: 정의되지 않음: blockScopedVar (범위 خارج) // 내부 블록 이후의 외부 functionScopedVar에 액세스합니다. fmt.Println("Outer functionScopedVar:", functionScopedVar) // 원본을 출력합니다. for i := 0; i < 2; i++ { // 'i'는 이 for 루프 내에서만 범위를 갖습니다. loopVar := "I'm visible only within this loop iteration" fmt.Println("Loop iteration:", i, loopVar) } // fmt.Println(i) // 오류: 정의되지 않음: i (범위 خارج) // fmt.Println(loopVar) // 오류: 정의되지 않음: loopVar (범위 خارج) }
블록 범위 및 섀도잉에 대한 주요 사항:
- 가시성: 식별자는 선언 시점부터 선언된 블록의 끝까지 보입니다.
- 섀도잉: 내부 범위가 외부 범위의 식별자와 동일한 이름으로 식별자를 선언하면 내부 선언이 외부 선언을 "섀도잉"합니다. 내부 범위 내에서는 내부 식별자에 액세스할 수 있습니다. 외부 식별자는 계속 존재하지만 일시적으로 액세스할 수 없습니다. 내부 범위가 끝나면 외부 식별자를 다시 액세스할 수 있습니다. 기술적으로 허용되지만 과도한 섀도잉은 코드를 읽고 디버그하기 어렵게 만들 수 있으므로 신중하게 사용해야 합니다.
수명 대 범위
식별자의 범위와 변수의 수명을 구별하는 것이 중요합니다.
- **범위(Scope)**는 컴파일 시간 개념으로, 식별자를 참조할 수 있는 위치를 결정합니다.
- **수명(Lifetime)**은 런타임 개념으로, 변수에 할당된 메모리가 얼마나 오랫동안 존재하는지 결정합니다.
Go의 가비지 수집기는 변수 수명을 관리합니다. 변수는 프로그램에서 도달할 수 있는 한, 범위에 관계없이 존재합니다. 함수에서 반환된 포인터와 같이 변수의 값이 범위를 벗어난 후에도 필요한 경우 Go의 이스케이프 분석은 힙에 할당되어야 함을 결정하여 즉각적인 블록을 넘어서 수명을 보장합니다. 반대로 변수가 더 이상 참조되지 않으면 기술적으로 범위 내에 있더라도 가비지 수집될 수 있습니다.
결론
변수와 상수를 선언, 초기화 및 관리하는 방법을 이해하는 것은 효과적인 Go 프로그램을 작성하는 데 기본이 됩니다. Go의 설계 선택, 즉 명시적 선언, :=
를 사용한 강력한 유형 추론, 상수를 위한 iota
의 유용성, 변수 사용 및 범위에 대한 엄격한 규칙은 Go의 가독성, 유지 관리성 및 동시성 안전성에 기여합니다. 이러한 개념을 마스터하면 깔끔하고 효율적이며 관용적인 Go 코드를 작성할 수 있습니다.