공백 식별자 (The blank identifier)

우리는 이미 for range 루프와 maps를 설명하면서, 공백 식별자에 대해 두 차례 언급했었다. 공백 식별자는 아무런 피해없이 그 어떤 타입의 그 어떤 값에 대해서도 할당 또는 선언될 수 있다. 이건 마치 Unix에서 /dev/null 파일(필요는 하지만 실제 값은 아무런 관련이 없는 변수를 저장하는 용도로서 사용되는 쓰기 전용 값)에 값을 넘기는 것과 유사하다.

다중 할당에서의 공백 식별자

for range루프에서 공백 식별자의 사용은 일반적인 상황에서의 특별한 경우(다중 할당)이다.

만약 좌변에 여려개의 값을 할당해야 하는데, 그 중 하나가 사용되지 않을 경우, 좌변에 공백 식별자를 두면 더미 변수를 생성 해야하는 필요가 없어지고 값 버리기를 깔끔하게 처리할 수 있다. 예를 들면, 하나의 값과 에러를 리턴하는 함수를 호출하는데 오직 에러만이 중요하다면, 무관한 값을 버리기 위해 공백 식별자를 사용한다.

if _, err := os.Stat(path); os.IsNotExist(err) {
    fmt.Printf("%s does not exist\n", path)
}

가끔 에러를 무시하기 위해 에러값을 버리는 코드를 볼 수도 있다. 이건 매우 나쁜 관행이다. 에러 반환을 항상 확인하라. 에러가 발생하는 데는 이유가 있다.

// Bad! This code will crash if path does not exist.
fi, _ := os.Stat(path)
if fi.IsDir() {
    fmt.Printf("%s is a directory\n", path)
}

미사용 임포트와 변수

패키지를 임포트하거나 변수를 선언해놓고 쓰지않게되면 에러가 발생한다. 미사용 임포트는 프로그램의 크기를 부풀리며 컴파일 속도도 저하시킨다. 또 사용되진 않지만 초기화된 변수는 적어도 연산을 낭비하며 어쩌면 큰 버그를 암시할 수도 있다. 그러나 프로그램이 개발중에 있을때 미사용 임포트와 변수들이 종종 생겨나게 될테고, 단지 컴파일이 진행되게 하기 위해서 나중에 다시 필요해질 이들을 지우는건 귀찮을 수 있다. 공백 식별자는 이를 피하는 방법을 제공한다.

아래의 반만 완성된 프로그램은 두 개의 미사용 임포트(fmt, io)와 미사용 변수(fd)를 가지고 있다. 따라서 이는 컴파일되지 않을 것이다. 하지만 지금까지 코드가 정확하게 만들어졌는지를 알 수 있다면 좋을 것이다.

package main

import (
    "fmt"
    "io"
    "log"
    "os"
)

func main() {
    fd, err := os.Open("test.go")
    if err != nil {
        log.Fatal(err)
    }
    // TODO: use fd.
}

미사용 임포트에 대한 불평을 잠재우려면, 임포트된 패키지의 상징을 참조하는 공백 식별자를 써라. 이와 유사하게, 미사용 변수 fd를 공백 식별자에 할당하면 미사용 변수에 대한 에러를 잠재울 수 있을 것이다. 이 버전의 프로그램은 컴파일이 된다.

package main

import (
    "fmt"
    "io"
    "log"
    "os"
)

var _ = fmt.Printf // For debugging; delete when done.
var _ io.Reader    // For debugging; delete when done.

func main() {
    fd, err := os.Open("test.go")
    if err != nil {
        log.Fatal(err)
    }
    // TODO: use fd.
    _ = fd
}

규약에 의하면, 임포트 에러를 잠재우기 위한 전역 선언은 임포트 구문 바로 다음에 위치되게 하며, 주석을 달아줘야 한다. 이들은 나중에 코드를 정리해야함을 쉽게 상기시키고 쉽게 찾아주게 만든다.

부수효과(side effect)를 위한 임포트

이전 예시에서 fmtio와 같은 미사용 임포트는 결국 사용되야 하거나 그렇지 않을 경우엔 없애야한다. (공백 할당은 아직 작업이 진행중인 코드로 인식해야 한다.) 그러나 때로는 직접 사용하지는 않으면서, 부수효과를 위해 패키지를 임포트하기도 하는데, 이는 유용한 사례이다. 예를 들면,net/http/pprof패키지는 패키지의 초기화 함수를 실행하는 동안 디버깅 정보를 제공하는 HTTP 핸들러를 등록한다. 이는 노출된 API를 가지고 있지만 대다수의 클라어언트는 오직 핸들러 등록만이 필요하고 정보에는 웹페이지를 통해 접근한다. 부수효과만을 위해 이 패키지를 임포트하기 위해선 이 패키지 이름을 공백 식별자로 바꾸면 된다:

import _ "net/http/pprof"

이러한 형태의 임포트는 패키지가 부수효과를 위해 임포트되고 있음을 명확하게 할 수 있다. 왜냐하면 이 파일에서는 패키지가 이름을 갖고 있지 않기 때문에 사용될 가능성이 없기 때문이다. (만약 이름을 갖고 있고 이를 사용하지 않는다면, 컴파일러는 프로그램을 거부할 것이다.)

인터페이스 검사

위의 인터페이스에 대한 논의에서 봤듯이, 타입은 인터페이스를 구현했음을 명시적으로 선언할 필요가 없다. 대신에, 타입은 인터페이스의 메서드를 구현함으로써 인터페이스를 구현한다. 실제로 ,대다수의 인터페이스 변환은 정적이며 따라서 컴파일 도중에 검사가 이루어진다. 예를 들면, 만약 *os.Fileio.Reader인터페이스를 구현하고 있지 않는데 이를 io.Reader를 기대하는 함수에 인자로 전달하게 되면 컴파일이 되지 않을 것이다.

하지만 몇 몇 인터페이스 검사는 런타임때 발생한다. 한 가지 예시는 Marshaler 인터페이스를 정의하는 encoding/json패키지에 있다. JSON 인코더가 저 인터페이스를 구현한 값을 받을 때, 인코더는 JSON으로 변환을 하기 위해 표준 변환을 진행하는 대신 값의 marshiling 메서드를 실행한다. 인코더는 런타임때 다음과 같이 타입 단언을 하면서 프로퍼티를 검사한다.

m, ok := val.(json.Marshaler)

만약 타입이 인터페이스를 구현했는지 안했는지를 실제 인터페이스 자체를 사용하지 않고, 에러 검사의 일부로서 확인할 필요가 있을 때, 타입 단언된 값을 무시하기 위해 공백 식별자를 사용하라:

if _, ok := val.(json.Marshaler); ok {
    fmt.Printf("value %v of type %T implements json.Marshaler\n", val, val)
}

이러한 상황이 나타나는 경우는 패키지가 실제로 인터페이스를 만족하는 타입을 구현하고 있는지를 보장할 필요가 있을 때이다. 만약 어떤 타입이 커스터마이징된 JSON 표기법이 필요하다면(예를 들면, json.RawMessage), 이는 json.Marshaler를 구현해야 한다. 그러나 컴파일러가 이를 자동으로 확인하도록 하는 정적 변환은 없다. 만약 부주의하게 타입이 그 인터페이스를 만족하는데에 실패를 하게되면 JSON 인코더는 여전히 실행되나 커스터마이징된 구현체를 사용할 수 없게된다. 인터페이스의 구현을 보증하기 위해서는, 패키지 안에서 공백 식별자를 이용하는 전역 선언문을 사용할 수 있다.

var _ json.Marshaler = (*RawMessage)(nil)

위의 선언에서 *RawMessageMarshaler로의 변환시키는 할당을 통해 *RawMessageMarshaler를 구현할 것을 요구하고 있으며, 이러한 특성은 컴파일시 검사될 것이다. 만약 json.Marshaler인터페이스에 변화가 생기면, 이 패키지는 더 이상 컴파일 되지 않을것이고, 패키지가 업데이트 되어야 함을 알게해준다.

이 구조에서 공백 식별자가 나타나는것은 위 선언이 변수를 만드는게 아니라 단지 타입 검사를 위해서만 존재함을 알려준다. 하지만 이를 하나의 인터페이스를 만족하는 모든 타입에 사용하지는 마라. 규약에 의하면, 위와 같은 선언은 드문 경우이지만, 이미 코드에 존재하는 정적 변환이 없는 경우에만 사용하라는 것이다.

results matching ""

    powered by

    No results matching ""