패키지 초기화

패키지 초기화 (Package initialization)

프로그램이 실행되기 전에, 패키지의 전역 변수들은 어떤 순서로 값이 채워질까요? 순서가 뒤죽박죽이면 변수를 쓰는 순간 아직 초기화되지 않은 값을 만날 수 있어요. Go는 이 순서를 '선언 순서 + 의존성'이라는 명확한 규칙으로 정해 두는데, 그 규칙을 지금 하나씩 살펴볼게요.

출처: Go Specification

본문

패키지 안에서 패키지 수준 변수 초기화는 단계적으로(stepwise) 진행돼요. 각 단계에서 선언 순서(declaration order)상 가장 앞에 있는 변수 중, 아직 초기화되지 않은 변수에 의존하지 않는 변수를 골라 초기화하죠.

좀 더 정확히 말하면, 패키지 수준 변수가 초기화될 준비가 되었다는 것은 다음과 같아요. 즉, 아직 초기화되지 않았고, 그 변수가 초기화 표현식이 없거나, 있다면 그 표현식이 아직 초기화되지 않은 변수에 의존하지 않는 경우예요. 초기화는 "선언 순서가 가장 빠르면서 초기화 준비가 된" 다음 패키지 수준 변수를 계속해서 초기화하는 방식으로 진행되고, 준비된 변수가 더 이상 없을 때 끝나요. 이 과정이 끝났는데도 아직 초기화되지 않은 변수가 남아 있다면, 그 변수들은 하나 이상의 초기화 순환(initialization cycle) 에 속한 것이고, 프로그램은 유효하지 않아요.

변수 선언에서 오른쪽의 단일 (다중 값) 표현식으로 초기화되는 왼쪽의 여러 변수는 함께 초기화돼요. 왼쪽 변수 중 하나라도 초기화되면, 그 모든 변수가 같은 단계에서 초기화되죠.

var x = a
var a, b = f() // a와 b가 함께 초기화되고, 그다음에 x가 초기화돼요

패키지 초기화의 관점에서 볼 때, 블랭크 변수는 선언 안의 다른 변수들과 똑같이 취급돼요.

여러 파일에 걸쳐 선언된 변수들의 선언 순서는 파일이 컴파일러에 제시되는 순서에 따라 결정돼요. 첫 번째 파일에 선언된 변수는 두 번째 파일에 선언된 어떤 변수보다 먼저 선언된 것으로 보죠. 이렇게 재현 가능한 초기화 동작을 보장하기 위해, 빌드 시스템은 같은 패키지에 속한 여러 파일을 어휘적 파일 이름 순서대로 컴파일러에 제시하는 것이 권장돼요.

의존성 분석은 변수의 실제 값에 의존하지 않고, 소스에서 그 변수에 대한 어휘적 참조만을 전이적으로 분석해요. 예를 들어 변수 x의 초기화 표현식이, 그 본문이 변수 y를 참조하는 함수를 가리킨다면, xy에 의존하게 돼요. 구체적으로는:

  • 변수나 함수에 대한 참조는 그 변수나 함수를 뜻하는 식별자예요.
  • 메서드 m에 대한 참조는 t.m 형태의 메서드 값 또는 메서드 표현식인데, 여기서 t의 (정적) 타입이 인터페이스 타입이 아니고, 메서드 mt의 메서드 집합에 있어야 해요. 결과로 나오는 함수 값 t.m이 실제로 호출되는지는 중요하지 않아요.

변수·함수·메서드 x는, x의 초기화 표현식이나 (함수·메서드의 경우) 본문이 변수 y에 대한 참조, 또는 y에 의존하는 함수·메서드에 대한 참조를 포함한다면, y에 의존해요.

예를 들어 다음과 같은 선언이 있다고 해볼게요.

var (
	a = c + b // == 9
	b = f() // == 4
	c = f() // == 5
	d = 3 // == 5, 초기화가 끝난 후
)

func f() int {
	d++
	return d
}

이때 초기화 순서는 d, b, c, a예요. 참고로 초기화 표현식 안에서 부분식(subexpression)의 순서는 무관해요. 이 예시에서 a = c + ba = b + c는 같은 초기화 순서를 만들어 내죠.

의존성 분석은 패키지 단위로 수행돼요. 현재 패키지에 선언된 변수·함수·(비인터페이스) 메서드를 가리키는 참조만 고려해요. 만약 변수들 사이에 다른, 숨겨진 데이터 의존성이 존재한다면, 그 변수들 사이의 초기화 순서는 명시되지 않아요(unspecified).

예를 들어 다음 선언이 있다고 해볼게요.

var x = I(T{}).ab() // x는 a와 b에 대한, 감지되지 않은 숨은 의존성을 가지고 있어요
var _ = sideEffect() // x, a, b와 무관해요

var a = b
var b = 42

type I interface      { ab() []int }
type T struct{}
func (T) ab() []int   { return []int{a, b} }

여기서 변수 ab보다 뒤에 초기화되지만, xb보다 먼저 초기화되는지, ba 사이에 초기화되는지, 혹은 a 이후에 초기화되는지는 명시되지 않아요. 따라서 sideEffect()가 호출되는 시점(x 초기화 전인지 후인지) 역시 명시되지 않아요.

변수는 또한 패키지 블록에 선언된 init이라는 이름의 함수로도 초기화될 수 있어요. init 함수는 인자도 결과 매개변수도 없어요.

func init() { … }

이런 함수는 패키지마다 여러 개 선언될 수 있고, 심지어 한 소스 파일 안에서도 여러 개가 가능해요. 패키지 블록에서 init 식별자는 오직 init 함수를 선언하는 데만 쓸 수 있지만, 그 식별자 자체는 선언되지 않으므로(not declared), 프로그램 어디에서도 init 함수를 참조할 수 없어요.

패키지 전체는 모든 패키지 수준 변수에 초기값을 할당한 뒤, 컴파일러에 제시된 대로 (여러 파일에 걸쳐 있을 수 있는) 소스에 나타난 순서대로 모든 init 함수를 호출함으로써 초기화돼요.

더 알아보기