Menu

Golang Enum: const와 iota로 열거형 만들기

Go에는 enum 키워드가 없습니다. 관용적인 대체법인 이름 있는 타입과 iota를 쓴 const 블록, 그리고 String(), 검증, 파싱, 비트 플래그, JSON 지원을 추가하는 방법을 보여 줍니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

Go에는 enum 키워드가 없습니다. 열거형은 두 조각으로 만듭니다. 이름 있는 타입, 그리고 iota로 번호를 매긴 그 타입 값들의 const 블록입니다.

Sunday는 0이고, 뒤따르는 줄은 하나씩 커집니다. 이것을 숫자 목록이 아니라 열거형으로 만드는 것은 이름 있는 타입 Weekday입니다. isWeekend는 시그니처에서 무엇을 기대하는지 말해 주고, 타입에 메서드를 추가할 수 있습니다. 아직 Weekday를 텍스트로 보여 주는 방법을 Go에 알려 주지 않았기 때문에 출력은 1 5 6입니다. 그 방법은 아래에 나옵니다.

iota의 동작 방식

iota는 컴파일러가 const 블록 안에서 제공하는 카운터입니다. 이를 기반으로 한 모든 요령은 두 가지 규칙으로 설명됩니다:

  1. iota는 블록 안에서 현재 줄의 인덱스와 같고, 0부터 시작하며, 새 const 블록마다 0으로 초기화됩니다.
  2. = expression이 없는 줄은 이전 줄의 표현식과 타입을 반복하며, 새 iota로 평가됩니다.

그래서 위의 Mondayiota가 1이 된 Monday Weekday = iota의 줄임말입니다. 표현식이 반복되므로 iota만이 아니라 어떤 상수 표현식이든 될 수 있습니다:

iota는 이름이 아니라 줄을 셉니다. XY가 보여 주듯, 같은 줄에 있는 두 상수는 iota 값 하나를 공유합니다.

1부터 시작하기, 그리고 그러지 않을 이유

아무도 설정하지 않은 열거형 타입 변수는 제로 값인 0을 담습니다. 0이 Sunday 같은 실제 값이라면 "사용자가 일요일을 골랐다"와 "필드가 채워지지 않았다"를 구분할 수 없습니다. 흔한 해결책 세 가지:

운영 코드에서는 1번 방법이 가장 흔하고, protobuf가 생성하는 Go 열거형도 이를 따릅니다(..._UNSPECIFIED = 0). 그러면 제로 값이 정직한 의미를 갖습니다.

값 건너뛰기

빈 식별자 _는 이름을 만들지 않고 iota 값 하나를 소비합니다. 프로토콜이 정한 번호에 맞추거나 나머지 번호를 바꾸지 않고 값을 폐기하는 등 간격을 남길 때 쓰세요:

type Opcode byte

const (
	OpContinue Opcode = iota // 0
	OpText                   // 1
	OpBinary                 // 2
	_                        // 3, reserved
	_                        // 4, reserved
	_                        // 5, reserved
	_                        // 6, reserved
	_                        // 7, reserved
	OpClose                  // 8
	OpPing                   // 9
	OpPong                   // 10
)

이 WebSocket opcode처럼 번호가 외부 명세로 고정되어 있다면, 빈 줄을 세는 것보다 명시적으로 쓰는 편(OpClose Opcode = 8)이 대개 더 명확합니다. iota는 정확한 번호가 중요하지 않은 값을 위한 것입니다.

번호가 데이터베이스나 파일에 저장되거나 네트워크로 전송되는 iota 목록은 절대 순서를 바꾸거나 중간에 끼워 넣지 마세요. 중간에 줄을 추가하면 그 뒤의 모든 값이 밀립니다. 새 값은 끝에 추가하거나 번호를 명시적으로 정하세요.

String 메서드 추가하기

타입에 String() string 메서드를 주면 fmt%v, %s, Println에서 이를 씁니다:

이 메서드의 두 가지 세부 사항이 중요합니다:

  • 범위 확인. 이것이 없으면 Weekday(9).String()은 범위를 벗어난 인덱스로 패닉을 일으키며, 호출자가 Weekday(9)를 만드는 것을 막을 방법이 없으므로 언젠가는 반드시 일어납니다.
  • Sprintf 안의 int(d). d 자체를 %d로 형식화하는 것은 괜찮지만, %v로 형식화하면 String()을 다시 호출해서 스택이 넘칠 때까지 재귀합니다.

%d는 여전히 숫자를 출력하므로 두 형태를 모두 얻을 수 있습니다: Wednesday is day 3.

stringer로 String 생성하기

목록이 길다면 stringer 도구가 메서드를 대신 작성해 줍니다:

//go:generate go run golang.org/x/tools/cmd/stringer@latest -type=Weekday
go generate ./...

압축된 String() 구현과 함께, 다시 생성하지 않고 상수를 바꾸면 빌드를 깨뜨리는 컴파일 타임 검사를 담은 weekday_string.go를 만듭니다. -linecomment 플래그는 줄 끝 주석을 이름으로 쓰며, 공백이 들어간 이름에 편리합니다.

값 검증하기

Go 열거형은 닫혀 있지 않습니다. 기반 타입의 어떤 값이든 변환할 수 있고, 타입 없는 상수는 암묵적으로 변환됩니다:

var d Weekday = 42     // compiles
d = Weekday(userInput) // compiles

그러니 코드 밖에서 오는 값(JSON, 데이터베이스, 플래그, 다른 패키지)은 확인하세요:

블록 끝의 비공개 센티널 colorCount는 항상 마지막 실제 값의 바로 다음에 있으므로, 새 색을 추가해도 IsValid가 올바르게 유지됩니다.

열거형에 대한 switch

열거형은 보통 switch에서 쓰입니다. Go는 switch가 모든 값을 다루는지 확인하지 않으므로, 예상 밖의 값을 보고하는 default를 추가하세요:

func (c Color) Hex() string {
	switch c {
	case Red:
		return "#ff0000"
	case Green:
		return "#00ff00"
	case Blue:
		return "#0000ff"
	default:
		return "#000000"
	}
}

서드파티 exhaustive 린터(golangci-lint에 포함)는 case가 빠진 열거형 타입 switch를 보고하며, 다른 언어의 완전성 검사가 주는 이점 대부분을 얻을 수 있습니다.

비트 플래그 열거형

권한처럼 값이 조합된다면 1 << iota로 값마다 비트 하나를 쓰세요:

|는 플래그를 합치고, &는 확인하고, &^(Go의 AND NOT 연산자)는 지웁니다. 여기서는 부호 없는 기반 타입이 맞는 선택입니다. uint8은 플래그 8개를, uint64는 64개를 담습니다.

문자열 열거형

값을 어차피 텍스트로 저장하거나 보낸다면, 문자열 기반 타입을 쓰면 변환 계층이 필요 없습니다:

type Env string

const (
	EnvDev     Env = "dev"
	EnvStaging Env = "staging"
	EnvProd    Env = "prod"
)

String() 메서드 없이도 값이 읽기 좋게 출력되고 직렬화되며, 데이터베이스 컬럼에는 선언 순서에 따라 달라지는 숫자 대신 "prod"가 들어갑니다. 대가도 있습니다. 비교가 문자열 비교가 되고, 비트 플래그는 불가능하며, Env("banana")도 컴파일되므로 검증은 여전히 여러분 몫입니다.

열거형과 JSON

정수 열거형은 기본적으로 숫자로 직렬화됩니다. 대신 이름으로 읽고 쓰려면 encoding.TextMarshalerencoding.TextUnmarshaler를 구현하세요. encoding/json은 값과 맵 키 모두에 이를 씁니다:

MarshalTextLevel*Level 모두에 동작하도록 값 리시버를 쓰고, UnmarshalText는 값을 바꾸므로 포인터 리시버가 필요합니다. 같은 두 메서드로 이 타입이 flag 패키지의 TextVar와 대부분의 설정 라이브러리에서도 동작합니다.

주의할 점

  • 리터럴의 암묵적 변환. Weekday를 받는 함수는 타입 없는 상수 42도 받습니다. 거부되는 것은 다른 타입의 타입 있는 값뿐입니다.
  • 첫 줄에서 타입을 잊음. const ( Red = iota; Green; Blue )에서 셋은 모두 Color 값이 아니라 타입 없는 정수 상수이므로 Color의 메서드가 적용되지 않습니다. 반복되는 표현식이 타입을 가지도록 Red Color = iota라고 쓰세요.
  • 저장된 열거형의 순서를 바꿈. iota 블록 중간에 값을 끼워 넣으면 이미 다른 곳에 저장된 번호가 조용히 바뀝니다.
  • String 안의 재귀. String() 안에서는 리시버를 %v%s로 절대 형식화하지 마세요. 먼저 기반 타입으로 변환하세요.

자주 묻는 질문

Go에 열거형(enum)이 있나요?

언어 기능으로는 없습니다. enum 키워드가 없습니다. 관용적인 대체법은 이름 있는 타입과, 보통 iota로 번호를 매긴 타입 있는 상수 블록입니다:

type Color int

const (
	Red Color = iota
	Green
	Blue
)

타입이 있으면 시그니처가 읽기 좋아지고 String() 같은 메서드를 붙일 곳이 생깁니다. 누군가 Color(42)라고 쓰는 것은 막지 못하므로 외부에서 오는 값은 검증하세요.

Go에서 iota란 무엇인가요?

iotaconst 블록 안에서 현재 줄(상수 명세)의 인덱스와 같은 미리 선언된 식별자이며, 0부터 시작합니다. 새 const 블록마다 0으로 초기화됩니다. 어떤 줄이 표현식을 생략하면 Go는 이전 표현식을 다음 iota로 반복하며, 그래서 Red = iota; Green; Blue가 0, 1, 2가 됩니다.

iota를 1부터 시작하게 하려면 어떻게 하나요?

첫 줄에 First Kind = iota + 1이라고 쓰거나, 빈 식별자로 0을 건너뜁니다: _ = iota 다음에 First. 많은 Go 개발자는 대신 0을 남겨 두고 Unknown이나 Invalid라는 이름을 붙여서, 초기화되지 않은 변수(제로 값이 0)가 실제 선택지가 아니라는 것을 분명히 합니다.

Go에서 열거형을 문자열로 출력하려면 어떻게 하나요?

타입에 String() string 메서드를 줍니다. fmt%v, %s, Println에서 이를 호출하므로 fmt.Println(Green)1 대신 Green을 출력합니다. 메서드는 switch나 배열로 직접 작성하거나 go run golang.org/x/tools/cmd/stringer@latest -type=Color로 생성할 수 있습니다.

Go에서 문자열을 열거형으로 바꾸려면 어떻게 하나요?

보통 map[string]Colorswitch에서 문자열을 찾아보고 알 수 없는 입력에는 오류를 반환하는 파싱 함수를 작성합니다: func ParseColor(s string) (Color, error). 같은 로직으로 UnmarshalText를 구현하면 JSON, 플래그, 설정 로더가 자동으로 이를 씁니다.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기