ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • golang: init functions
    Back-End/Golang 2024. 11. 21. 18:05
    반응형

    Go의 init 함수 다루기: 사용 방법 및 주의사항

    Go 언어에서 main 함수는 프로그램의 시작점을 나타내며, 해당 함수가 종료되면 프로그램의 실행도 종료됩니다. 그러나 init 함수는 main 함수와 함께 특별한 역할을 합니다. 이번 블로그에서는 Go의 init 함수에 대해 깊이 있게 알아보고, 그 사용 방법과 주의사항에 대해 살펴보겠습니다.

    init 함수란 무엇인가?

    init 함수는 패키지 블록 내에서 정의되며, 다음과 같은 목적으로 사용됩니다:

    • 복잡한 변수 초기화: 초기화 표현식으로 처리할 수 없는 복잡한 변수를 설정할 때.
    • 프로그램 상태 확인 및 수정: 시작 시 필요한 설정이나 상태를 확인하고 조정할 때.
    • 리소스 등록: 플러그인이나 핸들러를 등록할 때.
    • 일회성 작업 수행: 캐시 생성이나 데이터 로드 등 한 번만 실행되어야 하는 작업을 수행할 때.

    일반 함수 내부에서 유효한 모든 코드를 init 함수에도 작성할 수 있지만, 몇 가지 차이점이 있습니다.

    패키지 초기화 과정

    Go에서 패키지를 임포트하면, 해당 패키지는 다음과 같은 순서로 초기화됩니다:

    1. 임포트된 패키지의 초기화: 재귀적으로 모든 임포트된 패키지를 초기화합니다.
    2. 패키지 수준 변수의 초기화: 전역 변수들의 초기 값을 계산하고 할당합니다.
    3. init 함수 실행: 패키지 내에 정의된 모든 init 함수를 실행합니다.

    이 초기화 과정은 프로그램 실행 중 한 번만 수행되며, 패키지가 여러 번 임포트되더라도 다시 실행되지 않습니다.

    초기화 순서

    패키지가 여러 파일로 구성된 경우, 변수 초기화와 init 함수 호출의 순서는 어떻게 결정될까요? Go는 컴파일러에 전달되는 파일의 순서에 따라 초기화 순서를 결정합니다. 빌드 시스템이 z.go를 먼저 전달하면, a.go보다 z.go의 변수 초기화와 init 함수가 먼저 실행됩니다.

    Go 언어 명세에서는 빌드 시스템이 동일한 패키지의 여러 파일을 사전순으로 컴파일러에 전달할 것을 권장합니다:

    재현 가능한 초기화 동작을 보장하기 위해, 빌드 시스템은 동일한 패키지에 속한 여러 파일을 컴파일러에 사전순으로 제공하는 것이 좋습니다.

    하지만 특정한 파일 순서에 의존하는 것은 프로그램의 이식성을 떨어뜨릴 수 있으므로 주의해야 합니다.

    예제

    다음은 세 개의 파일로 구성된 간단한 프로그램입니다:

     

    test.go

    package main
    
    import "fmt"
    
    var _ int64 = t()
    
    func init() {
        fmt.Println("init in test.go")
    }
    
    func t() int64 {
        fmt.Println("calling t() in test.go")
        return 1
    }
    
    func main() {
        fmt.Println("main")
    }

     

    a.go

    package main
    
    import "fmt"
    
    var _ int64 = a()
    
    func init() {
        fmt.Println("init in a.go")
    }
    
    func a() int64 {
        fmt.Println("calling a() in a.go")
        return 2
    }

     

    b.go

    package main
    
    import "fmt"
    
    var _ int64 = b()
    
    func init() {
        fmt.Println("init in b.go")
    }
    
    func b() int64 {
        fmt.Println("calling b() in b.go")
        return 3
    }

     

    프로그램의 출력 결과는 다음과 같습니다:

    calling a() in a.go
    calling t() in test.go
    calling b() in b.go
    init in a.go
    init in text.go
    init in b.go
    main

    init 함수의 특성

    • 매개변수와 반환값이 없습니다: init 함수는 인자를 받지 않으며 값을 반환하지 않습니다.
    • 식별자로서 선언되지 않습니다: main 함수와 달리 init은 선언되지 않으므로 참조할 수 없습니다. 따라서 init()처럼 호출할 수 없습니다.
    package main
    
    import "fmt"
    
    func init() {
        fmt.Println("init")
    }
    
    func main() {
        init() // 오류: 정의되지 않은 식별자
    }

    여러 개의 init 함수를 가질 수 있습니다: 동일한 패키지나 파일 내에 여러 개의 init 함수를 정의할 수 있습니다.

    package main
    
    import "fmt"
    
    func init() {
        fmt.Println("init 1")
    }
    
    func init() {
        fmt.Println("init 2")
    }
    
    func main() {
        fmt.Println("main")
    }

    출력 결과:

    init 1
    init 2
    main

    init 함수의 활용 예

    복잡한 변수 초기화

    초기화 표현식으로 처리할 수 없는 복잡한 변수 초기화는 init 함수를 통해 수행할 수 있습니다.

    package main
    
    import "math/big"
    
    var bigInt *big.Int
    
    func init() {
        // 큰 수를 초기화해야 하는 경우
        bigInt = new(big.Int)
        bigInt.SetString("123456789012345678901234567890", 10)
    }
    
    func main() {
        println("큰 수의 값:", bigInt.String())
    }

    설명: 이 예제에서는 math/big 패키지를 사용하여 큰 정수를 초기화합니다. 일반 변수 초기화로는 처리하기 어려운 작업을 init 함수에서 수행합니다.

    설정 값 로드

     
    package config
    
    import (
        "encoding/json"
        "io/ioutil"
        "log"
    )
    
    var Settings map[string]interface{}
    
    func init() {
        data, err := ioutil.ReadFile("settings.json")
        if err != nil {
            log.Fatalf("설정 파일을 읽을 수 없습니다: %v", err)
        }
        err = json.Unmarshal(data, &Settings)
        if err != nil {
            log.Fatalf("설정 파일 파싱 오류: %v", err)
        }
    }

     

    설명: 프로그램 시작 시 설정 파일을 읽어와 Settings 변수에 저장합니다. 다른 패키지에서 config.Settings를 통해 설정 값을 사용할 수 있습니다.

    사이드 이펙트만을 위한 패키지 임포트

    특정 패키지를 임포트하여 그 안의 init 함수만 실행하고 싶을 때가 있습니다. 그러나 Go에서는 사용되지 않는 임포트를 허용하지 않습니다. 이 경우 블랭크 식별자 _를 사용하여 임포트합니다.

    import _ "expvar"

    설명: 위 코드는 expvar 패키지를 임포트하여 그 안의 init 함수를 실행하지만, 패키지 내의 다른 식별자는 사용하지 않습니다. 이를 통해 사이드 이펙트를 활용할 수 있습니다.

     

    주의사항

    • 초기화 순서에 의존하지 마세요: 파일의 컴파일 순서에 따라 초기화 순서가 달라질 수 있으므로, 초기화 순서에 의존하는 코드는 피해야 합니다.
    • init 함수 남용 금지: init 함수는 초기화 작업에만 사용하고, 복잡한 로직이나 프로그램 흐름을 변경하는 코드는 피하는 것이 좋습니다.
    • 패키지 간 순환 참조 주의: 패키지 초기화 시 순환 참조가 발생하면 컴파일 오류가 발생하므로, 패키지 구조를 설계할 때 이를 고려해야 합니다.

    마무리

    Go의 init 함수는 프로그램 초기화 단계에서 중요한 역할을 합니다. 적절하게 사용하면 프로그램의 초기 상태를 설정하고, 필요한 설정을 적용하는 데 유용합니다. 하지만 남용하거나 초기화 순서에 의존하면 예상치 못한 동작이 발생할 수 있으므로 주의해야 합니다.

    Go의 언어 사양과 패키지 초기화 메커니즘을 잘 이해하고 init 함수를 활용하면 더욱 안정적이고 효율적인 Go 프로그램을 작성할 수 있을 것입니다.

     

    참고 자료

    반응형
Designed by Tistory.