상수 (const)
상수란?
상수(Constant)는 프로그램 실행 중에 변경될 수 없는 값입니다. Go에서 상수는 컴파일 타임(compile time) 에 결정되며, 런타임에 계산되는 값은 상수로 선언할 수 없습니다.
변수(var)와의 차이:
| 구분 | 변수 (var) | 상수 (const) |
|---|---|---|
| 값 변경 | 가능 | 불가 |
| 결정 시점 | 런타임 | 컴파일 타임 |
| 초기값 | 선택 (零값) | 필수 |
| 주소 참조 | 가능 (&x) | 불가 |
| 타입 | 모든 타입 | 기본 타입만 |
상수는 보통 수학적 상수, 설정값, 열거형 값 등을 나타낼 때 사용합니다.
상수 선언
단일 상수
package main
import "fmt"
const Pi = 3.14159265358979
const AppName = "MyApp"
const MaxSize = 1024
const IsDebug = false
func main() {
fmt.Println(Pi, AppName, MaxSize, IsDebug)
// 원의 넓이 계산
radius := 5.0
area := Pi * radius * radius
fmt.Printf("반지름 %.0f인 원의 넓이: %.4f\n", radius, area)
}
상수 블록 선언
여러 상수를 한꺼번에 선언할 때는 블록 형태를 사용합니다.
package main
import "fmt"
const (
StatusOK = 200
StatusNotFound = 404
StatusError = 500
DefaultTimeout = 30
MaxRetries = 3
)
func main() {
fmt.Println("HTTP 상태:", StatusOK, StatusNotFound, StatusError)
fmt.Printf("타임아웃: %d초, 최대 재시도: %d회\n", DefaultTimeout, MaxRetries)
}
타입 있는 상수 vs 타입 없는 상수
Go의 상수는 타입 있는 상수(typed constant) 와 타입 없는 상수(untyped constant) 두 종류로 나뉩니다.
타입 없는 상수 (Untyped Constant) — 더 유연
타입을 명시하지 않은 상수는 높은 정밀도를 가지며, 사용되는 맥락에 따라 타입이 결정됩니다.
package main
import "fmt"
const (
// 타입 없는 상수 — 문맥에 따라 타입이 결정됨
UntypedInt = 42
UntypedFloat = 3.14
UntypedString = "hello"
UntypedBool = true
)
func main() {
// UntypedInt는 int, int32, int64, float64 등 다양한 타입으로 사용 가능
var i int = UntypedInt
var i32 int32 = UntypedInt
var i64 int64 = UntypedInt
var f float64 = UntypedInt // int처럼 생겼지만 float64로도 사용 가능!
fmt.Println(i, i32, i64, f)
// 타입 없는 정수 상수는 매우 큰 정밀도 지원
const huge = 1_000_000_000_000_000_000 // 10^18, int64 범위 내
fmt.Println(huge)
}
타입 있는 상수 (Typed Constant) — 더 엄격
package main
import "fmt"
const (
// 타입 있는 상수 — 명시된 타입에만 사용 가능
TypedInt int = 42
TypedFloat float64 = 3.14
TypedString string = "hello"
)
func main() {
var i int = TypedInt
// var i32 int32 = TypedInt // 컴파일 에러! int32에 int 상수 대입 불가
var i32 int32 = int32(TypedInt) // 명시적 변환 필요
fmt.Println(i, i32)
// 타입 있는 상수는 해당 타입의 연산만 허용
result := TypedFloat * 2 // OK: float64 * 2
fmt.Println(result)
}
iota — 자동 증가 열거자
iota는 const 블록 안에서 사용되는 특별한 식별자입니다. 블록의 첫 번째 상수에서 0으로 시작하여, 상수가 하나씩 추가될 때마다 1씩 자동으로 증가합니다.
기본 iota
package main
import "fmt"
const (
Sunday = iota // 0
Monday // 1
Tuesday // 2
Wednesday // 3
Thursday // 4
Friday // 5
Saturday // 6
)
func main() {
fmt.Println("일요일:", Sunday) // 0
fmt.Println("월요일:", Monday) // 1
fmt.Println("토요일:", Saturday) // 6
today := Wednesday
if today == Wednesday {
fmt.Println("오늘은 수요일입니다.")
}
}
iota 식(Expression): 비트 플래그
iota를 수식에서 사용하면 더 복잡한 패턴을 만들 수 있습니다.
package main
import "fmt"
// 파일 권한 비트 플래그 (Unix 스타일)
const (
Read = 1 << iota // 1 << 0 = 1 (0b001)
Write // 1 << 1 = 2 (0b010)
Execute // 1 << 2 = 4 (0b100)
)
// 네트워크 기능 플래그
const (
FeatureHTTP = 1 << iota // 1
FeatureHTTPS // 2
FeatureWebSocket // 4
FeatureGRPC // 8
)
func main() {
// 단일 권한
fmt.Printf("Read: %d (0b%03b)\n", Read, Read)
fmt.Printf("Write: %d (0b%03b)\n", Write, Write)
fmt.Printf("Execute: %d (0b%03b)\n", Execute, Execute)
// 권한 조합 (비트 OR)
readWrite := Read | Write // 3 (0b011)
allPerms := Read | Write | Execute // 7 (0b111)
fmt.Printf("Read+Write: %d (0b%03b)\n", readWrite, readWrite)
fmt.Printf("모든 권한: %d (0b%03b)\n", allPerms, allPerms)
// 권한 확인 (비트 AND)
perm := Read | Execute // 5 (0b101)
if perm&Write != 0 {
fmt.Println("쓰기 권한 있음")
} else {
fmt.Println("쓰기 권한 없음")
}
// 네트워크 기능 확인
enabledFeatures := FeatureHTTP | FeatureWebSocket
fmt.Printf("HTTP: %v\n", enabledFeatures&FeatureHTTP != 0) // true
fmt.Printf("HTTPS: %v\n", enabledFeatures&FeatureHTTPS != 0) // false
fmt.Printf("WebSocket: %v\n", enabledFeatures&FeatureWebSocket != 0) // true
}
첫 값 건너뛰기 (_ 사용)
package main
import "fmt"
type LogLevel int
const (
_ = iota // 0 건너뜀 (언더스코어로 무시)
LevelDebug // 1
LevelInfo // 2
LevelWarn // 3
LevelError // 4
LevelFatal // 5
)
func (l LogLevel) String() string {
switch l {
case LevelDebug:
return "DEBUG"
case LevelInfo:
return "INFO"
case LevelWarn:
return "WARN"
case LevelError:
return "ERROR"
case LevelFatal:
return "FATAL"
default:
return "UNKNOWN"
}
}
func main() {
level := LevelInfo
fmt.Printf("레벨 값: %d, 이름: %s\n", level, level) // 2, INFO
// 현재 레벨 이상만 출력
currentLevel := LevelWarn
logs := []struct {
level LogLevel
message string
}{
{LevelDebug, "디버그 메시지"},
{LevelInfo, "정보 메시지"},
{LevelWarn, "경고 메시지"},
{LevelError, "에러 메시지"},
}
for _, log := range logs {
if log.level >= currentLevel {
fmt.Printf("[%s] %s\n", log.level, log.message)
}
}
}
iota 식: 다양한 패턴
package main
import "fmt"
// 곱 패턴
const (
_ = iota // 0
KB = 1 << (10 * iota) // 1 << 10 = 1024
MB // 1 << 20 = 1048576
GB // 1 << 30 = 1073741824
TB // 1 << 40 = 1099511627776
)
// 10배 패턴
const (
Level1 = (iota + 1) * 10 // 10
Level2 // 20
Level3 // 30
Level4 // 40
Level5 // 50
)
func main() {
fmt.Printf("1 KB = %d bytes\n", KB)
fmt.Printf("1 MB = %d bytes\n", MB)
fmt.Printf("1 GB = %d bytes\n", GB)
fileSize := int64(2.5 * float64(GB))
fmt.Printf("파일 크기: %.1f GB\n", float64(fileSize)/float64(GB))
fmt.Println("레벨 경험치:", Level1, Level2, Level3, Level4, Level5)
}
여러 iota 블록의 독립성
각 const 블록은 독립적인 iota를 가집니다. 블록이 새로 시작되면 iota는 다시 0으로 초기화됩니다.
package main
import "fmt"
const (
A = iota // 0
B // 1
C // 2
)
const (
X = iota // 0 — 새 블록에서 다시 0부터 시작!
Y // 1
Z // 2
)
func main() {
fmt.Println(A, B, C) // 0 1 2
fmt.Println(X, Y, Z) // 0 1 2
}
실전 예제: 요일 열거형
package main
import "fmt"
type Weekday int
const (
Sunday Weekday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)
var weekdayNames = [...]string{
"일요일", "월요일", "화요일", "수요일",
"목요일", "금요일", "토요일",
}
func (d Weekday) String() string {
if d < Sunday || d > Saturday {
return "알 수 없는 요일"
}
return weekdayNames[d]
}
func (d Weekday) IsWeekend() bool {
return d == Sunday || d == Saturday
}
func main() {
today := Wednesday
fmt.Printf("오늘: %s\n", today)
fmt.Printf("주말 여부: %v\n", today.IsWeekend())
// 한 주 출력
for day := Sunday; day <= Saturday; day++ {
weekend := ""
if day.IsWeekend() {
weekend = " (주말)"
}
fmt.Printf("%d: %s%s\n", day, day, weekend)
}
}
상수 표현식
상수는 컴파일 타임에 계산되는 수식을 포함할 수 있습니다.
package main
import "fmt"
const (
SecondsPerMinute = 60
MinutesPerHour = 60
HoursPerDay = 24
DaysPerWeek = 7
// 컴파일 타임에 계산되는 파생 상수
SecondsPerHour = SecondsPerMinute * MinutesPerHour // 3600
SecondsPerDay = SecondsPerHour * HoursPerDay // 86400
SecondsPerWeek = SecondsPerDay * DaysPerWeek // 604800
)
func main() {
fmt.Println("1분 =", SecondsPerMinute, "초")
fmt.Println("1시간 =", SecondsPerHour, "초")
fmt.Println("1일 =", SecondsPerDay, "초")
fmt.Println("1주 =", SecondsPerWeek, "초")
// 타임스탬프를 사람이 읽을 수 있는 형태로 변환
totalSeconds := 90_000
days := totalSeconds / SecondsPerDay
hours := (totalSeconds % SecondsPerDay) / SecondsPerHour
minutes := (totalSeconds % SecondsPerHour) / SecondsPerMinute
seconds := totalSeconds % SecondsPerMinute
fmt.Printf("%d초 = %d일 %d시간 %d분 %d초\n",
totalSeconds, days, hours, minutes, seconds)
}
상수와 switch 패턴
iota로 만든 열거형은 switch 문과 자연스럽게 어울립니다.
package main
import "fmt"
type Direction int
const (
North Direction = iota
East
South
West
)
func (d Direction) String() string {
return [...]string{"북", "동", "남", "서"}[d]
}
func (d Direction) Opposite() Direction {
return (d + 2) % 4
}
func move(d Direction, steps int) string {
switch d {
case North:
return fmt.Sprintf("북쪽으로 %d칸 이동", steps)
case East:
return fmt.Sprintf("동쪽으로 %d칸 이동", steps)
case South:
return fmt.Sprintf("남쪽으로 %d칸 이동", steps)
case West:
return fmt.Sprintf("서쪽으로 %d칸 이동", steps)
default:
return "알 수 없는 방향"
}
}
func main() {
d := North
fmt.Printf("현재 방향: %s\n", d)
fmt.Printf("반대 방향: %s\n", d.Opposite())
fmt.Println(move(East, 5))
fmt.Println(move(South, 3))
}
Go에 enum이 없는 이유와 iota
Go는 C, Java, Rust처럼 별도의 enum 키워드를 제공하지 않습니다. 그 대신 const + iota + 타입 정의 조합으로 열거형을 구현합니다.
이 접근 방식의 장점:
- 메서드 추가 가능: 타입을 정의하면 해당 타입에 메서드를 붙일 수 있어 String(), IsValid() 같은 기능을 자연스럽게 추가할 수 있습니다.
- 타입 안전성: 정의된 타입 덕분에 잘못된 값이 전달될 위험을 줄입니다.
- 단순성: 별도 키워드 없이 기존 언어 구성요소만으로 강력한 열거형 구현이 가능합니다.
package main
import "fmt"
type Status int
const (
StatusPending Status = iota
StatusActive
StatusInactive
StatusDeleted
)
// isValid 메서드로 유효성 검사
func (s Status) IsValid() bool {
return s >= StatusPending && s <= StatusDeleted
}
func (s Status) String() string {
names := []string{"pending", "active", "inactive", "deleted"}
if !s.IsValid() {
return fmt.Sprintf("unknown(%d)", int(s))
}
return names[s]
}
func processUser(status Status) {
if !status.IsValid() {
fmt.Println("유효하지 않은 상태:", status)
return
}
fmt.Printf("사용자 상태 처리: %s\n", status)
}
func main() {
processUser(StatusActive)
processUser(StatusDeleted)
processUser(Status(99)) // 잘못된 값
}
고수 팁
팁 1: iota를 사용할 때 0 값은 의미 있게 설계하라
열거형에서 0은 "설정되지 않음" 또는 "알 수 없음"을 나타내는 게 관례입니다.
type OrderStatus int
const (
OrderStatusUnknown OrderStatus = iota // 0 — 초기화되지 않은 상태
OrderStatusPending // 1
OrderStatusShipped // 2
OrderStatusDelivered // 3
)
// var o OrderStatus — 자동으로 OrderStatusUnknown(0)이 됨
팁 2: 타입 없는 상수를 적극 활용하라
타입 없는 상수는 다양한 타입과 호환되어 API 유연성이 높아집니다.
const Timeout = 30 // untyped int 상수
var t1 int = Timeout // OK
var t2 int64 = Timeout // OK
var t3 float64 = Timeout // OK
팁 3: 상수 이름에 패키지 이름을 포함하지 마라
패키지 이름이 접두사 역할을 하므로, 상수 이름에 패키지 이름을 반복하지 않아도 됩니다.
// Bad: http.HTTPStatusOK (HTTP 중복)
// Good: http.StatusOK
팁 4: iota 값에 의존하는 직렬화 코드를 조심하라
iota 값은 코드 순서에 따라 결정됩니다. 데이터베이스나 네트워크에 숫자 값을 저장한다면, 새 상수를 중간에 삽입 하면 기존 값이 바뀌는 위험이 있습니다. 이럴 때는 명시적 값을 사용하세요.
// 위험: 중간에 삽입하면 기존 값 변경
const (
A = iota // 0
// 여기에 새 상수를 추가하면 B, C 값이 변경됨!
B // 1
C // 2
)
// 안전: 명시적 값 사용
const (
A = 1
B = 2
C = 3
)