쉽게 읽는 Effective Go 번역 (4)

[첫번째], [두번째], [세번째] 에 이어 네번째 Effective Go 번역 이어갑니다.



초기화 (Initialization)

표면적으로는 C 나 C++ 에서의 초기화와 크게 달라보이지 않지만, Go 에서의 초기화가 훨씬 강력한 기능을 제공합니다.
복잡한 구조체가 초기화동안 생성될 수 있을 뿐 더러, 초기화되는 객체 간 또는 서로 다른 패키지 간의 순서 문제도 정확하게 처리되거든요.
(이 말은 계속 읽다 보면 이해하게 되실거예요.)

상수 (Constants)

Go 에서의 상수는 그냥 우리가 알고 있는 그 상수입니다.
(설사 함수 내에서 로컬변수로 정의된 경우라도)컴파일 할 때 생성이 되고, 숫자(numbers)와 문자(character, rune), 그리고 문자열(string)과 boolean 만이 상수가 될 수 있어요.
컴파일러가 컴파일할 때 인지할 수 있도록 상수는 반드시 상수 표현식으로 정의되어야 합니다.
예를 들면, 1<<3 은 상수 표현식인데요.
반면에 math.Sin(math.Pi/4)은 아닙니다.
math.Sin 이라는 함수의 호출은 런타임(run time)에 발생해야 하기 때문이예요.

Go 에서 상수는 iota 라는 것을 이용해서 열거형으로 생성할 수도 있어요.
iota 가 표현식의 일부가 될 수 있고, 표현식은 암시적으로 반복이 가능하므로, 복잡한 일련의 값들을 쉽게 생성할 수 있습니다.
(역자: 아래에서 일련의 상수값을 생성할 때 iota 는 표현식의 일부가 되고, 이러한 표현식은 암시적으로 반복되지요. 즉 1 << (10 * iota) 라는 표현식이 반복됨과 동시에 iota 는 계속 1 씩 증가하는 것입니다. 그러니까 이렇게 변하는 표현식의 결과가 각 상수들의 값이 되는거지요.)

type ByteSize float64

const (
    _           = iota // blank 지시자를 할당해서 첫번째 값 0 을 무시
    KB ByteSize = 1 << (10 * iota)  // iota 값은 1 이 됨
    MB     // 1 << (10 * 2) 의 결과가 됨
    GB     // 1 << (10 * 3) 의 결과가 됨
    TB
    PB
    EB
    ZB
    YB
)

사용자 정의 타입에 String 같은 메소드를 붙일 수 있는데요.
이러한 기능때문에 상수값들은 자동으로 자신의 출력 포맷을 만들어낼 수가 있어요. (아래 코드 참조)
대부분 이러한 테크닉은 구조체에 대해 사용하지만 위의 코드에 있는 ByteSize(float 타입)같은 스칼라(scala) 타입에 대해서도 유용하게 사용할 수 있습니다.

func (b ByteSize) String() string {
    switch {
    case b >= YB:
        return fmt.Sprintf("%.2fYB", b/YB)
    case b >= ZB:
        return fmt.Sprintf("%.2fZB", b/ZB)
    case b >= EB:
        return fmt.Sprintf("%.2fEB", b/EB)
    case b >= PB:
        return fmt.Sprintf("%.2fPB", b/PB)
    case b >= TB:
        return fmt.Sprintf("%.2fTB", b/TB)
    case b >= GB:
        return fmt.Sprintf("%.2fGB", b/GB)
    case b >= MB:
        return fmt.Sprintf("%.2fMB", b/MB)
    case b >= KB:
        return fmt.Sprintf("%.2fKB", b/KB)
    }
    return fmt.Sprintf("%.2fB", b)
}

YB 는 1.00YB 로 출력하는 반면 ByteSize(1e13)은 9.09TB 를 출력합니다.

여기에서 ByteString 의 String 을 구현할 때 Sprintf 를 사용하는 것은 안전한 방식인데요.
(출력 섹션에서 보았던 무한 재귀 호출문제가 발생하지 않아요.)
왜냐하면, Sprintf 에서 %f 를 사용했기 때문입니다. 문자열(string) 포맷이 아니잖아요.
Sprintf 는 string 을 원할 때만 String 메소드를 호출하니까요.
%f 는 float 값을 요구하는 것이니 문제가 생기지 않겠죠.

변수 (Variables)

변수는 마치 상수처럼 초기화할 수 있지만 일반적인 변수처럼 런타임에 계산되는 표현식입니다.

var (
    home   = os.Getenv("HOME")
    user   = os.Getenv("USER")
    gopath = os.Getenv("GOPATH")
)

init 함수 (The init function)

마지막으로 각 소스 파일은 어떠한 상태(state)를 셋업하기 위해 init 함수를 정의할 수 있는데요.(사실 각 파일은 다수의 init 함수를 가질 수 있어요)
init 함수는 패키지에서 모든 변수들의 초기화가 이루어진 다음에 호출됩니다.
이러한 변수의 초기화들은 import 되는 패키지들이 초기화된 다음에 이루어지구요.

선언의 형태로 표현할 수 없는 것들을 초기화하는 것 외에도, init 은 실제 프로그램의 실행이 시작되기 전에 그 상태를 검증하거나 올바른 상태로 복구하는데도 많이 사용됩니다.

func init() {
    if user == "" {
        log.Fatal("$USER not set")
    }
    if home == "" {
        home = "/home/" + user
    }
    if gopath == "" {
        gopath = home + "/go"
    }
    // gopath may be overridden by --gopath flag on command line.
    flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH")
}

메소드 (Methods)

포인터 대 값 (Pointers vs. Values)

우리가 ByteSize 에서 보았듯이, 메소드는 이름을 가진 어떤 타입에 대해서도 정의될 수 있어요. (포인터와 인터페이스만 빼구요.)
리시버(receiver)가 반드시 구조체일 필요는 없습니다.

이전에 슬라이스에 대해 살펴볼 때 Append 함수를 작성해 보았는데요.
이것을 슬라이스에 대한 메소드로 정의해 볼 수도 있습니다.
이렇게 하려면 먼저 메소드를 바인딩할 타입을 선언한 다음, 메소드의 리시버가 그 타입의 값을 받도록 만듭니다.

type ByteSlice []byte

func (slice ByteSlice) Append(data []byte) []byte {
    // 이전에 작성했던 Append 함수의 body 를 똑같이 작성해요.
}

이렇게 작성해도 여전히 메소드는 갱신된 슬라이스를 반환해야 하잖아요?
이런 어색한 처리를 없애기 위해 메소드에서 리시버가 ByteSlice 의 포인터를 받도록 재정의할 수 있습니다.
그렇게 하면 메소드는 호출자의 슬라이스를 변경할 수가 있어요.

func (p *ByteSlice) Append(data []byte) {
    slice := *p
    // 위쪽의 body 와 똑같이 작성해요. 반환할 필요는 없구요.
    *p = slice
}

사실 이것보다도 더 좋은 방법이 있어요.
바로 standard 라이브러리의 Write 메소드와 똑같은 모양으로 우리의 함수를 고치는거죠.

func (p *ByteSlice) Write(data []byte) (n int, err error) {
    slice := *p
    // body 는 위랑 똑같아요.
    *p = slice
    return len(data), nil
}

위처럼 작성하면 *ByteSlice 타입은 standard 라이브러리의 인터페이스인 io.Writer 를 만족시키기 때문에 아주 유용합니다.
예를 들면, 우린 이제 ByteSlice 값을 아래처럼 출력할 수 있어요.

    var b ByteSlice
    fmt.Fprintf(&b, "This hour has %d days\n", 7)

위에서 ByteSlice 의 주소를 넘겨주었는데요.
이는 *ByteSlice 만이 io.Writer 를 만족시키기 때문입니다.
리시버로 포인터와 값을 사용할 때 규칙이 하나 있어요.
값을 리시버로 사용하는 메소드는 포인터와 값 모두에 대해 사용할 수 있는 반면, 포인터를 리시버로 사용하는 메소드는 포인터에 대해서만 사용이 가능합니다.

이런 규칙은 포인터 메소드가 리시버를 변경할 수 있기 때문에 생겨난 것입니다.
값에 대해 포인터 메소드를 사용하게 되면 메소드는 그 값의 복사본을 받게 되거든요.
그러면 메소드 내에서 변경이 발생해도 모두 무시되죠.
그래서, 언어 레벨에서 이러한 실수를 허용하지 않습니다.
그런데, 유용하게 쓰일 수 있는 예외가 있긴 해요.
만약 값에 대한 주소를 얻을 수 있는 경우에는, 언어가 주소 연산자를 자동으로 추가해서 값에 대해 포인터 메소드를 이용하는 것을 가능하게 해주거든요.
위의 예에서 변수 b 에 대해 주소를 얻을 수 있죠.
그래서 우리는 이 변수의 Write 메소드를 단지 b.Write 로 호출할 수 있어요.
컴파일러가 우리를 위해서 b.Write 를 (&b).Write 로 변경시켜 주거든요.

어쨌건, 바이트의 슬라이스에 Write 를 사용할 수 있다는 것은 bytes.Buffer 라는 응용이 만들어진 핵심 아이디어입니다.

인터페이스와 기타 타입들 (Interfaces and other types)

인터페이스 (Interfaces)

Go 언어에서 인터페이스는 객체가 할 수 있는 행위를 지정하는 방법을 제공하는데요 : 어떤 것이 이것(this)을 할 수 있다는 것은 곧 여기(here)에서 사용될 수 있다는 것을 의미합니다.
(역자: 특정 행위를 할 수 있는 타입이라면 그 행위가 필요한 모든 자리에 호환되어 사용될 수 있다는 의미)
우린 지금까지 이러한 예를 벌써 몇가지 보았어요.
객체의 String 메소드를 구현하면 사용자가 정의한대로 출력 output 을 만들 수 있고, Fprintf 는 그러한 output 을 Write 메소드를 가진 어떤 장치에도 출력할 수 있습니다.
Go 에서 한개 또는 두개의 메소드를 가진 인터페이스는 아주 흔합니다.
그리고, 그 인터페이스의 이름은 자신이 가진 메소드로부터 따오는 경우가 많습니다.
예를 들면, Write 메소드를 구현한 어떤 것을 io.Writer 라고 부르는 것 처럼요.

하나의 타입은 다수의 인터페이스를 구현할 수 있습니다.
예를 들어, 어떤 컬렉션(collection)이 sort.Interface 를 구현하고 있다면, 그 컬렉션은 sort 패키지로 정렬을 할 수가 있는데요.
sort.Interface 는 Len(), Less(i,j int) bool, 그리고 Swap(i,j int) 메소드를 포함하고 있습니다.
거기에다 사용자 formatter 도 가질 수 있구요.
아래 Sequence 타입을 보면 그 두가지를 모두 만족하고 있는걸 알 수 있어요.

type Sequence []int

// sort.Interface 가 요구하는 메소드들
func (s Sequence) Len() int {
    return len(s)
}
func (s Sequence) Less(i, j int) bool {
    return s[i] < s[j]
}
func (s Sequence) Swap(i, j int) {
    s[i], s[j] = s[j], s[i]
}

// Copy 메소드는 Sequence 의 복제본을 리턴하고 있어요.
func (s Sequence) Copy() Sequence {
    copy := make(Sequence, 0, len(s))
    return append(copy, s...)
}

// 출력을 위한 메소드예요.
// 반환하기 전에 요소들을 정렬하고 있네요.
func (s Sequence) String() string {
    // 인자를 덮어쓰지 말고 복사본을 만들어요.
    s = s.Copy()
    sort.Sort(s)
    str := "["
    // 반복문이 O(N²)으로 동작하는데요.
    // 이는 다음 예제에서 수정할거예요.
    for i, elem := range s {
        if i > 0 {
            str += " "
        }
        str += fmt.Sprint(elem)
    }
    return str + "]"
}

변환 (Conversions)

위에서 Sequence 의 String 메소드는 Sprint 가 슬라이스에 대해 이미 할 수 있는 기능을 다시 만든 셈이예요.
(Sprint 또한 O(N2) 시간복잡도로 동작하는 것이라서 안좋긴 해요.)
Sprint 를 호출하기 전에 Sequence 를 그냥 평범한 []int 로 변환하면 훨씬 수고도 줄어 들고 속도도 빠르게 할 수 있죠.

func (s Sequence) String() string {
    s = s.Copy()
    sort.Sort(s)
    return fmt.Sprint([]int(s))
}

위의 메소드는 String 메소드에서 Sprintf 를 안전하게 호출하는 변환 기술의 한 예입니다.
왜냐 하면 Sequence 와 []int 의 경우 타입의 이름을 무시한다면 사실 똑같은거잖아요. 그래서 둘 사이에 변환을 수행하는건 전혀 문제가 없고 합법적인거죠.
변환은 새로운 값을 생성하는게 아닙니다.
단지 그 값이 잠깐 새로운 타입을 가지는 것처럼 동작하게 하는거죠.
(integer 를 float 타입으로 변환하는 것도 합법적이긴 한데요. 이 경우는 새로운 값을 생성하는 경우죠)

Go 에서는 타입을 변환해서 다른 세트의 메소드들을 사용할 수 있도록 하는 방법도 일반적으로 많이 사용합니다.
Sequence 타입을 sort.IntSlice 라는 타입으로 변환을 해주면 전체 코드가 아래 예처럼 줄어들게 됩니다.
(역자: sort.IntSlice 타입의 경우 이름에 Int 가 들어가 있고, Len(), Less(i,j int) bool, Swap(i,j int) 메소드가 이미 구현되어 있으므로 이를 별도로 구현해줄 필요가 없습니다. int 형 슬라이스를 정렬하는 것이 명확히 정해져 있는 것이죠. 반면에 sort.Sort(s)는 어떤 타입을 정렬할 지 모르기 때문에 인자로 주는 타입에 대해 3 개 메소드를 사용자가 구현해주어야 합니다.)

type Sequence []int

func (s Sequence) String() string {
    s = s.Copy()
    sort.IntSlice(s).Sort()
    return fmt.Sprint([]int(s))
}

자, 이제 우리는 Sequence 가 정렬이나 출력 인터페이스를 모두 구현하는 대신, 하나의 데이터 아이템이 Sequence, sort.IntSlice 그리고 []int 같은 여러 타입으로 변환될 수 있는 능력을 이용합니다.
이런 각 타입이 작업의 일정 부분을 담당하여 수행하게 되는거죠.
일반적으로 사용되는 방법은 아니지만 효율적입니다.

인터페이스 변환 및 타입 Assertion (Interface conversions and type assertions)

타입 스위치(Type switch)는 변환의 한 형태입니다.
인터페이스를 받아서 switch 문의 각 case 의 타입에 맞게 변환을 해줍니다.
아래 코드는 fmt.Printf 가 어떻게 타입 스위치를 이용하여 값을 문자열로 변환하는지를 간단히 보여줍니다.
만약 타입이 이미 문자열이면 인터페이스의 원래 값을 사용하면 되고, String 메소드를 가진 타입이면 해당 메소드 호출의 결과를 사용하면 됩니다.

type Stringer interface {
    String() string
}

var value interface{} // 호출자가 제공하는 값
switch str := value.(type) {
case string:
    return str
case Stringer:
    return str.String()
}

첫번째 case 는 구체적인 값을 찾는 경우이고, 두번째 case 는 인터페이스를 다른 인터페이스로 변환합니다.
타입을 이런식으로 섞어서 사용해도 전혀 문제가 없습니다.

우리가 신경써야할 타입이 하나밖에 없다면 어떻게 할까요?
어떤 값이 string 타입이라는걸 이미 알고 있고 단지 그 값을 추출하고 싶을 경우말이죠.
case 가 하나 밖에 없는 switch 를 사용할 수도 있겠지만, 이럴 때는 type assertion 을 사용하면 됩니다.
type assertion 은 특정 타입을 명시하여 인터페이스로부터 그 값을 추출합니다.
syntax 는 switch 에서 사용하는 구문으로부터 빌려왔지만, type 이라는 키워드 대신에 특정 타입을 명시하여 사용합니다.

value.(typeName)

위 구문의 결과는 typeName 이라는 정적 타입을 가진 새로운 값입니다.
이 때의 타입은 인터페이스로 전달된 구체적인 타입이거나, value 의 변환 대상이 될 수 있는 인터페이스 타입이 되어야 합니다.
value 가 가지고 있는 string 을 추출하기 위해 아래처럼 작성할 수 있어요.

str := value.(string)

하지만, 위의 구문에서 value 가 string 을 가지지 않은 상태라면 프로그램은 런타임 에러로 인해 crash 가 발생할 거예요.
이를 방지하기 위해서 "comma, ok" 구문으로 value 가 string 인지를 안전하게 검사하세요.

str, ok := value.(string)
if ok {
    fmt.Printf("string value is: %q\n", str)
} else {
    fmt.Printf("value is not a string\n")
}

type assertion 이 실패하면, str 은 여전히 string 타입으로 유지되긴 하지만, 제로 값 즉 empty string 을 가지게 됩니다.
이런 기능의 실례로서, 이 섹션의 첫 부분에서 나왔던 type switch 구문을 아래와 같이 if-else 구문으로 작성할 수 있습니다.

if str, ok := value.(string); ok {
    return str
} else if str, ok := value.(Stringer); ok {
    return str.String()
}

일반화 (Generality)

만약 타입이 단지 인터페이스를 구현하기 위해서만 존재하고, 인터페이스를 구현하는 메소드 이외의 메소드는 노출(export)하지 않는다면, 굳이 타입 자신을 노출할 필요는 없어요.
인터페이스만을 노출하게 되면, 값이 가진 행위들 중에서 인터페이스와 관련된 것만으로 관심이 좁혀져서 아주 깔끔하고 명료해집니다.
그리고 공통 메소드의 인스턴스마다 작성해야할 문서를 그렇게 반복해서 작성할 필요가 없어지구요.

그런 경우에는, 생성자가 구현 타입보다는 인터페이스를 반환해야 합니다.
예를 들면, 해쉬 라이브러리에서 crc32.NewIEEE 와 adler32.New 는 둘 다 인터페이스인 hash.Hash32 를 반환합니다.
Go 프로그램에서 Adler-32 를 위한 CRC-32 알고리즘을 교체하려면 생성자 호출만 변경하면 됩니다.
나머지 코드는 알고리즘이 변경되더라도 영향을 받지 않죠.

(역자: 이에 대해 원문에서는 crypto/cipher 패키지를 예로 들었는데, 왜 굳이 이렇게 이해하기 어려운 예를 가지고 설명했는지 이해가 안감. 얼마든지 쉬운 예가 있는데... 번역을 하면 오히려 이상해서 원문으로 둡니다.)

A similar approach allows the streaming cipher algorithms in the various crypto packages to be separated from the block ciphers they chain together. The Block interface in the crypto/cipher package specifies the behavior of a block cipher, which provides encryption of a single block of data. Then, by analogy with the bufio package, cipher packages that implement this interface can be used to construct streaming ciphers, represented by the Stream interface, without knowing the details of the block encryption.

The crypto/cipher interfaces look like this:

type Block interface {
    BlockSize() int
    Encrypt(dst, src []byte)
    Decrypt(dst, src []byte)
}

type Stream interface {
    XORKeyStream(dst, src []byte)
}

아래 코드는 블럭 암호를 스트림 암호로 변경해주는 CTR 스트림의 생성자를 보여줍니다.
블럭 암호의 상세한 내용은 추상화되어 있다는 것에 유의해야 합니다.

// NewCTR 은 주어진 Block input 을 이용해서 암호화/복호화하는 Stream 을 리턴한다.
// iv 의 길이는 Block 의 block 크기와 같아야 한다.
func NewCTR(block Block, iv []byte) Stream

NewCTR applies not just to one specific encryption algorithm and data source but to any implementation of the Block interface and any Stream. Because they return interface values, replacing CTR encryption with other encryption modes is a localized change. The constructor calls must be edited, but because the surrounding code must treat the result only as a Stream, it won't notice the difference.

인터페이스와 메소드 (Interfaces and methods)

어떤 것이든지 메소드를 붙일 수 있는데, 이는 곧 어떤 것이든 인터페이스를 만족시킬 수 있다는 것을 의미합니다.
이것을 보여주는 예가 Handler 인터페이스를 정의하고 있는 http 패키지인데요.
Handler 를 구현하는 어떤 객체든지 HTTP 요청을 처리할 수 있습니다.

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

ResponseWriter 는 그 자체로 인터페이스인데요.
이는 client 에게 응답을 반환할 필요가 있는 메소드들에게 접근 방법을 제공합니다.
그러한 메소드에는 표준 Write 메소드도 포함합니다.
그래서, io.Writer 가 사용될 수 있는 곳에는 http.ResponseWriter 또한 사용될 수 있습니다.
ServeHTTP 의 인자 중 Request 는 client 로부터 받은 요청을 파싱하여 가지고 있는 구조체입니다.

좀 단순화하기 위해 HTTP 요청이 항상 POST 는 없고 GET 만 있다고 가정해보죠.
이렇게 단순화해도 handler 를 셋업하는 방법에는 영향을 주지 않거든요.
아래의 예는 페이지의 방문 횟수를 세는 handler 를 간단하게 구현한 것입니다.

// 단순한 Counter 서버
type Counter struct {
    n int
}

func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    ctr.n++
    fmt.Fprintf(w, "counter = %d\n", ctr.n)
}

(위에서 말했던 이론을 바탕으로, Fprintf 가 http.ResponseWriter 에 어떻게 출력을 하는지 한번 유심히 보세요.)
실제 서버에서는 ctr.n 에 대한 접근이 동시에 이루어질 경우 보호를 해줄 필요가 있겠죠.
이에 대한 좋은 처리 방법으로 sync 와 atomic 패키지를 한번 살펴보세요.

참고로, URL 트리의 한 노드에 server 를 어떻게 붙이는지 한번 보세요.

import "net/http"
...
ctr := new(Counter)
http.Handle("/counter", ctr)

그런데, Counter 로 구조체를 사용할 필요가 있을까요?
여기에서는 integer 만으로 충분합니다.
(값을 증가시키면 호출하는 측에서 이를 볼 수 있어야 하므로 receiver 는 포인터일 필요가 있습니다.)

// 단순한 Counter 서버
type Counter int

func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    *ctr++
    fmt.Fprintf(w, "counter = %d\n", *ctr)
}

만약 여러분의 프로그램에 페이지 방문에 대한 알림을 받아야하는 내부 상태값 같은 것을 유지해야하면 어떻게 하죠?
그럴 때는 채널을 웹 페이지에 연결시켜 버리세요.

// 각각의 방문에 대해 알림을 전송하는 채널
// (아마도 buffered 채널이 필요할 듯)
type Chan chan *http.Request

func (ch Chan) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    ch <- req
    fmt.Fprint(w, "notification sent")
}

마지막으로, 우리가 server 바이너리를 실행할 때 사용한 인자를 /args 라는 api 로 제공하고 싶다고 해봅시다.
인자를 출력하는 함수를 작성하는 것은 아래처럼 무척 쉽습니다.

func ArgServer() {
    fmt.Println(os.Args)
}

그런데, 이것을 HTTP 서버에서 제공하고 싶을 때는 어떻게 할까요?
어떤 타입에 대해 그 값은 무시하고 ArgServer 라는 메소드를 만들 수도 있겠지요.
하지만, 좀 더 깔끔한 방법이 있습니다.
포인터와 인터페이스를 제외하고는 어떤 타입에 대해서도 메소드를 정의할 수 있기 때문에, 우린 함수에 대해서도 메소드를 작성할 수 있습니다.
http 패키지에 있는 아래 코드를 보세요.

// HandleFunc 타입은 일반적인 함수를 HTTP 핸들러로 사용할 수 있도록 해주는 아답터예요.
// 만약 f 가 적절한 시그너처를 가진 함수라면, HandleFunc(f)는 f 를 호출하는 핸들러 객체입니다.
type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP 는 f(w, req)를 호출합니다.
func (f HandlerFunc) ServeHTTP(w ResponseWriter, req *Request) {
    f(w, req)
}

HandleFunc 는 ServeHTTP 라는 메소드를 가진 타입이예요.
그래서, 그 타입의 값은 HTTP 요청을 처리할 수 있죠.
메소드의 구현을 보세요.
리시버(receiver)는 함수 f 이고, 메소드는 f 를 호출하고 있습니다.
아마 이상하게 보일거예요.
하지만, 이것은 채널(channel)이 리시버인 상황에서 그 채널에 메시지를 전송하는 메소드와 별로 다를 것이 없습니다.

ArgServer 를 HTTP 서버로 만들기 위해서는 먼저 적절한 시그너처를 가지도록 수정해야 합니다.

// Argument server.
func ArgServer(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintln(w, os.Args)
}

이제 ArgServer 는 HandlerFunc 와 같은 시그너처를 가지게 됐네요.
그래서, ArgServer 는 자신의 메소드를 사용할 수 있는 타입으로 변환될 수 있게 되었습니다.
마치 우리가 Sequence 를 IntSlice.Sort 를 사용할 수 있는 IntSlice 로 변환할 수 있는 것처럼 말이죠.
이를 셋업하는 코드는 아래처럼 간결합니다.

http.Handle("/args", http.HandlerFunc(ArgServer))

누군가가 /args 페이지를 방문할 때, 이 페이지에 설치된 핸들러는 ArgServer 라는 값과 HandleFunc 라는 타입을 가집니다.
HTTP 서버는 그 타입의 ServeHTTP 메소드를 실행합니다(이 때 리시버는 ArgServer).
그리고, 다음으로 HandlerFunc.ServeHTTP 내에 있는 f(w, req) 를 실행함으로써 ArgServer 를 호출하게 됩니다.
이제 인자들(arguments)이 출력되겠죠.

이번 섹션에서는 구조체와 integer, 채널, 그리고 함수로부터 HTTP 서버를 만들어 봤습니다.
이 모든 것들이 가능한 이유는 인터페이스가 (거의) 어떤 타입에 대해서도 정의될 수 있는 단순한 메서드의 집합이기 때문입니다.


You may also like...

0 0 votes
Article Rating
Subscribe
Notify of
guest
1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments

[…] "쉽게 읽는 Effective Go 번역 (4)"에서 만나요~ […]

1
0
Would love your thoughts, please comment.x
()
x