go init 함수를 이용한 패키지 개발 중 발생한 실수

go init 함수를 이용하여 static한 패키지를 개발하던 중 발생한 사소한 버그

Go init 함수

go에서는 main 함수 이외에 init 함수라는 특별한 함수가 있다.

go 에서는 디렉토리 단위로 패키지로 사용하며 go compiler에 의하여 같은 디렉토리를 사용하는 go 소스코드들은 하나의 패키지로 처리된다.

패키지를 사용하기 위해서는 $GOPATH를 기준으로 만들어진 main 패키지가 존재하는 디렉토리의 경로를 기준으로 한다.

이전 포스팅에서 사용한 프로젝트를 예로 들면

$ tree dlv_practice
dlv_practice
├── main.go
└── mylib
    ├── calc.go
    └── calc_test.go

현재 dlv_practice라는 디렉토리는 $GOPATH/src/dlv_practice가 전체 경로이다.

// $GOPATH/src/dlv_practice/main.go
package main

import (
	"dlv_practice/mylib"
	"fmt"
)

func main() {
	c := &mylib.Calc{}
	fmt.Println(c.Add(1, 2))
}
// $GOPATH/src/dlv_practice/mylib/calc.go
package mylib

type Calc struct {
}

func (c *Calc) Add(a int, b int) int {
	result := a + b
	return result
}

calc라는 구조체를 이용하기 위해서는 package를 import하여 패키지명.함수 와 같은 방식으로 사용할 수 있다.

일반적인 OOP 방식으로 개발을 하려면 struct를 설계하고 pointer receiver를 이용하여 함수를 연결하여 클래스처럼 사용할 수 있다.

main 함수를 보면 calc를 생성하여 Add 함수를 사용하는 것을 볼 수 있다.

golang에서는 init이라는 특별한 함수가 있다.

package를 import할 때 실행되는 함수이다.

golang의 개발 패러다임은 inherit이 아닌 composite이다.

언어 계층에서 kernel 함수를 사용하여 기본적인 패키지들을 만들고 이를 이용하여 개발자들이 추가해서 개발하는 방식이고 이것을 권장하고 있다.

이것을 가능하게 해주는 것이 바로 init 함수이다.

패키지 변수를 생성하고 init 함수에서 이를 초기화하면 import하는 패키지에서는 초기화와 관련된 코드를 줄일 수 있다.

이런 방식을 이용하여 logging 패키지를 개발하던 중 사소한 버그가 발생했다.

logging을 하기 전 config파일을 이용하여 기본적인 값을 불러오는 코드를 init에 작성하였으나 import하는 main 패키지에서 해당 config가 초기화 되지 않아 nil pointe exception이 발생하여 segfault가 발생한 것이다.

문제가 된 코드를 한 번 보자

package B

type InstanceB struct {
	Data string
}

func NewInstanceB() (*InstanceB, error){
  return &InstanceB{Data:"hello world!"}, nil
}
package A
import (
	"fmt"
	"init_error/B"
)

var instance *B.InstanceB

func init(){
    instance, err := B.NewInstanceB()
    if err != nil {
        panic(err)
    }
    fmt.Println(instance)
}

func NewFormat() string{
    data := instance.Data
    return fmt.Sprintf("%s",data)
}
package main

import (
	"fmt"
	"init_error/A"
)

func main() {
	data := A.NewFormat()
	fmt.Println(data)
}

간소화한 코드를 순서대로 한번 보면

main에서 A라는 패키지를 import하면서 A패키지의 init 함수가 호출된다.

init함수 내에서 B패키지의 NewInstanceB를 호출하였다.

그냥 단순히 봤을때 NewInstanceB를 호출하여 패키지에서 선언된 instance가 초기화되고 err를 자동으로 만드는 것으로 생각할 수 있다.

하지만 실제 동작은 같은 이름이 있더라도 := 를 사용하면 package 변수인 instance가 있음에도 불구하고 동일한 변수이름이 init함수의 local 변수로 생성된다.

따라서 package 변수 instance는 여전히 nil값을 가진 *B.InstanceB가 되어 sigfault에러가 발생한다.

이를 해결하기 위해서는 err를 로컬 변수로 추가하면 해결된다.

package A
import (
	"fmt"
	"init_error/B"
)

var instance *B.InstanceB

func init(){
    var err error // local 변수 추가
    instance, err = B.NewInstanceB() // := 에서 = 으로 변경
    if err != nil {
        panic(err)
    }
    fmt.Println(instance)
}

func NewFormat() string{
    data := instance.Data
    return fmt.Sprintf("%s",data)
}

결론

예제 코드

결론은 golang의 변수 정의에 관련된 :=와 =의 차이로 인해 생긴 사소한 버그였다.

:=와 같은 방식은 위처럼 err가 포함된 패키지를 호출할 때 많이 사용했었는데 패키지 변수 활용을 위한 코드를 작성하지 않았다면 한동안 몰랐을 것 같다.

처음 디버깅을 할 때는 변수 선언보다 init 함수의 호출 순서를 잘못 이해하고 있는 건가 하고 의심을 했고 패키지 import와 관련하여 디버깅 삽질 및 검색을 한참 했다.

참고로 init 함수의 호출 순서는 다음과 같다.

init

dlv를 이용하여 패키지 호출 단계를 하나씩 분석하면서 init.0라는 함수와 autogenerated function라는 개념도 알게 되었으니 마냥 낭비한 것은 아닌 것 같은데 이게 과연 필요할 까 싶으면서도 아직 잘 모르겠다. 내부 원리를 알아서 나쁠 건 없으니 나중에 이 부분도 정리를 해봐야겠다.


mcauto 2018 ©