이 글은 Effective Go를 일부 번역한 글입니다.


자료

출력

Go의 형식 출력은 C의 printf 계열과 비슷한 방식이지만 더 다채롭고 더 범용적이다. 함수는 fmt 패키지에 있고 대문자 이름이다. fmt.Printf, fmt.Fprintf, fmt.Sprintf 등등이 있다. 문자열 함수(Sprintf 등등)는 주어진 버퍼를 채우는 대신 문자열을 반환한다.

형식 문자열을 넣지 않아도 된다. Printf, Fprintf, Sprintf 등 함수 각각엔 다른 함수 짝이 있다. Print와 Println이다. 이 함수는 형식 문자열을 받지 않는 대신 각 인수마다 기본 형식을 만든다. Println 버전은 추가로 인수 사이에 공백을 넣고 뒤에 줄바꿈을 붙여 출력한다. 반면 Print 버전은 인수 좌우 양쪽에 문자열이 없을 때만 공백을 넣는다. 아래 예에서 각 줄은 똑같이 출력한다.

fmt.Printf("Hello %d\n", 23)
fmt.Fprint(os.Stdout, "Hello ", 23, "\n")
fmt.Println("Hello", 23)
fmt.Println(fmt.Sprint("Hello ", 23))

형식 출력 함수 fmt.Fprint 계열은 첫 인수에 io.Writer 인터페이스를 구현한 객체를 받는다. os.Stdout과 os.Stderr 변수는 익숙한 개체다.

여기서부터는 C와 다른 점이다. 먼저, %d 같은 숫자 형식은 부호나 크기에 대한 신호를 받지 않는다. 대신 출력 루틴이 인수 유형으로 이 성질을 정한다.

var x uint64 = 1<<64 - 1
fmt.Printf("%d %x; %d %x\n", x, x, int64(x), int64(x))

위 루틴은 아래처럼 출력한다.

18446744073709551615 ffffffffffffffff; -1 -1

정수에 십진수 출력처럼 기본 변환만 하려면 범용 형식 %v(“value”를 줄임)를 사용하면 된다. Print와 Println이 만드는 것과 같은 결과가 나온다. 게다가 이 형식은 아무 값이나 출력할 수 있다. 심지어 배열, 조각, 구조체, 맵 등도 출력할 수 있다. 아래 예문은 이전 절에 정의한 표준시간대 맵을 출력한다.

fmt.Printf("%v\n", timeZone)  // or just fmt.Println(timeZone)

출력은 아래와 같다.

map[CST:-21600 PST:-28800 EST:-18000 UTC:0 MST:-25200]

맵은 물론 열쇠를 아무 순서로 출력한다. 구조체를 출력할 땐 변형된 형식 %+v로 구조체의 각 영역을 이름과 출력한다. 그리고 또 다른 형식 %#v는 값을 완전한 Go 형식으로 출력한다.

type T struct {
    a int
    b float64
    c string
}
t := &T{ 7, -2.35, "abc\tdef" }
fmt.Printf("%v\n", t)
fmt.Printf("%+v\n", t)
fmt.Printf("%#v\n", t)
fmt.Printf("%#v\n", timeZone)

출력은 아래와 같다.

&{7 -2.35 abc   def}
&{a:7 b:-2.35 c:abc     def}
&main.T{a:7, b:-2.35, c:"abc\tdef"}
map[string] int{"CST":-21600, "PST":-28800, "EST":-18000, "UTC":0, "MST":-25200}

(앰퍼샌드에 주의하라.) 따옴표로 감싼 문자열 형식은 %q를 string이나 []byte 형에 사용하면 된다. 이를 변형한 형식 %#q는 가능하다면 대신 역따옴표(Backquote)를 쓴다. (%q 형식은 정수나 룬에도 사용할 수 있다. 작은 따옴표로 감싼 룬 상수를 만든다.) 또한 %x는 정수뿐만 아니라 문자열, 바이트 배열, 바이트 조각에도 동작하며 긴 16진수 문자열을 만든다. 그리고 형식에 공백을 추가하면 (% x) 바이트 사이에 공백을 넣는다.

또다른 편리한 형식은 %T다. 값의 유형을 출력한다.

fmt.Printf("%T\n", timeZone)

출력은 아래와 같다.

map[string] int

직접 만든 유형의 기본 형식을 조정하려면 그 유형에 String() string 메서드를 정의하면 된다. 우리의 간단한 T 형으로 보면 아래처럼 하면 된다.

func (t *T) String() string {
    return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c)
}
fmt.Printf("%v\n", t)

출력은 아래와 같다.

7/-2.35/"abc\tdef"

(T 포인터 말고 값도 출력하려면 String의 수신자를 값 유형으로 하라. 이 예제는 포인터를 사용하고 있는데, 구조체에 더 효율적이고 더 자연스럽기 때문이다. 아래 있는 포인터 vs 값 수신자 부분을 보라.)

이 String 메서드도 Sprintf를 부를 수 있다. 출력 루틴이 완전히 재진입 가능(Reentrant)하며 출력 루틴을 이 방법으로 감쌀 수 있기 때문이다. 하지만 이 방법을 사용할 때 이거 하난 분명히 알아야 한다. String 메서드를 만들 때 곧바로 Sprintf를 호출하지 말라. String 메서드를 무한히 호출하게 된다. Sprintf를 호출하면 수신자를 곧바로 문자열로 출력하려 하고, 그러면 다시 메서드를 호출하기 때문이다. 아래 예처럼 일반적이고 만들기 쉬운 실수다.

type MyString string

func (m MyString) String() string {
    return fmt.Sprintf("MyString=%s", m) // Error: will recur forever.
}

고치기도 쉽다. 인수를 기본 문자열로 변환하면 된다. 기본 문자열은 메서드가 없다.

type MyString string
func (m MyString) String() string {
    return fmt.Sprintf("MyString=%s", string(m)) // OK: note conversion.
}

초기화 부분에서 이 반복을 피하는 다른 기법을 볼 것이다.

또 다른 출력 기법으로 출력 루틴 인수를 곧바로 다른 출력 루틴에 넣는 방법이 있다. Printf 용법은 마지막 인수로 ...interface{}를 사용해 형식 뒤에 가변 개수, 가변 유형 매개 변수를 넣도록 지정한다.

func Printf(format string, v ...interface{}) (n int, err error) {

Printf 함수에서 v는 []interface{}처럼 작동하지만 다른 가변 함수로 넘기면 정규 인수 목록처럼 작동한다. 위에 쓴 log.Println은 아래처럼 구현돼있다. 인수를 Sprintln에 곧바로 넘겨 실제 형식 처리를 맡긴다.

// Println prints to the standard logger in the manner of fmt.Println.
func Println(v ...interface{}) {
    std.Output(2, fmt.Sprintln(v...))  // Output takes parameters (int, string)
}

Sprintln 호출에서 v 뒤에 ...를 써 컴파일러에게 v를 인수 목록으로 취급하라 알렸다. ...가 없다면 v를 단순히 조각 인수 하나로 넘긴다.

여기서 다룬 게 다가 아니다. 자세히 알려면 fmt 패키지의 godoc 문서를 보라.

덤으로 … 매개 변수에 유형을 지정할 수 있다. 아래 있는 함수에서 ...int가 그 예다. 함수는 정수 목록에서 가장 작은 수를 고르는 최소 함수다.

func Min(a ...int) int {
    min := int(^uint(0) >> 1)  // largest int
    for _, i := range a {
        if i < min {
            min = i
        }
    }
    return min
}