ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 7장 인터페이스
    프로그래밍/Golang 2019. 11. 6. 23:53

    The Go Programming Language

    7장 인터페이스

    인터페이스 타입은 다른 타입의 동작을 일반화하거나 추상화해서 표현

    7.1 인터페이스 규약

    • 구상 타입

    지금까지 살펴본 모든 타입은 구상 타입(concrete type)

    구상 타입은 값의 정확한 표현을 지정하고 숫자의 산술 연산이나 슬라이스의 색인, append, range 등 해당 표현에 대한 내부의 연산을 드러냄

    구상 타입은 메소드를 통해 부가적인 동작을 제공할 수 있음

    구상 타입의 값을 보면 해당 값이 무엇인지 무엇을 할 수 있는지 정확히 알 수 있음

    • 인터페이스 타입

    인터페이스는 추상 타입

    인터페이스는 값의 표현이나 내부 구조 또는 지원하는 기본 연산을 드러내지 않음

    인터페이스는 메소드 중 일부만 보여줌

    인터페이스 타입의 값을 보면 해당 값에 대해 알 수 있는 것이 없고 이 값에 있는 메소드에서 어떤 동작을 제공하는지만 알 수 있음

    7.2 인터페이스 타입

    인터페이스 타입은 구상 타입이 해당 인터페이스의 인스턴스로 인식되기 위해 필요한 메소드들을 지정

    • io 패키지

    io.Writer 타입은 파일, 메모리 버퍼, 네트워크 연결, HTTP 클라이언트, 압축, 해시 등 바이트를 쓸 수 있는 모든 타입에 대한 추상화를 제공하므로 가장 널리 쓰이는 인터페이스 중 하나

    Reader는 바이트를 읽을 수 있는 모든 타입을 나타냄

    Closer는 파일이나 네트워크 접속 등 직접 당을 수 있는 모든 값을 표현

    package io
    type Reader interface {
        Read(p []byte) (n int, err error)
    }
    type Closer interface {
        Close() error
    }

    다음은 기존 인터페이스 조합으로 이뤄진 새 인터페이스 선언을 찾을 수 있음

    type ReadWriter interface {
        Reader
        Writer 
    }
    type ReadWriteCloser interface {
        Reader
        Writer
        Closer 
    }

    앞에서 살펴본 구조체 내장과 유사한 문법

    모든 메소드에 대한 약칭 역할을 하는 새 인터페이스를 정의할 수 있고 이를 인터페이스 내장(embedding)이라 함

    다음은 io.ReadWriter를 내장 없이 사용할 수 있음

    type ReadWriter interface {
        Read(p []byte) (n int, err error)
        Write(p []byte) (n int, err error)
    }

    다음은 두 스타일을 혼합할 수도 있음

     type ReadWriter interface {
        Read(p []byte) (n int, err error)
        Writer
    }

    위 3가지 선언의 효과는 같음

    메소드가 나타나는 순서는 중요하지 않음

    메소드의 집합만이 중요

    7.3 인터페이스 충족

    인터페이스에서 요구하는 모든 메소드가 타입 안에 있다면 이 타입이 인터페이스를 충족한다고 함

    • 예를 들어

    *os.File은 io.Reader, Writer, Closer, ReadWriter를 충족

    *bytes.Buffer는 Reader, Writer, ReadWriter가 충족하지만 Close 메소드가 없으므로 Closer 인터페이스는 충족하지 않음

    구상 타입이 단지 특정 인터페이스 타입의 "한 종류(is a)"라고 하는 경우가 많으며 이는 구상 타입이 해당 인터페이스를 충족한다는 의미

    • 예를 들어

    *bytes.Buffer는 io.Writer의 한 종류

    *os.File은 io.ReadWriter의 한 종류

    • 할당 규칙

    인터페이스 할당 규칙은 매우 단순

    표현식은 그 타입이 인터페이스를 충족할 때에만 인터페이스에 할당할 수 있음

    var w io.Writer
    w = os.Stdout // OK: *os.File has Write method
    w = new(bytes.Buffer) //OK: *bytes.Buffer has Write method
    w = time.Second //compile error: time.Duration lacks Write method
    
    var rwc io.ReadWriteCloser
    rwc = os.Stdout //OK: *os.File has Read, Write, Close methods
    rwc = new(bytes.Buffer) //compile error: *bytes.Buffer lacks Close metho

    위 규칙은 오른쪽 표현식이 인터페이스일 때에도 적용

    w = rwc // OK: io.ReadWriteCloser has Write method
    rwc = w // compile error: io.Writer lacks Close method
    • interface{}

    interface{} 타입처럼 메소드가 전혀 없다면 이를 만족하는 구상 타입에 대해 무엇을 알려주는가? 아무것도 없음

    쓸모없는 것처럼 보일 수 있지만 빈 인터페이스 타입으로 불리는 interface{} 타입은 반드시 필요

    빈 인터페이스 타입은 충족하는 타입에 대해 아무런 요구 조건도 없으므로 빈 인터페이스에는 어떤 값도 할당할 수 있음

    var any interface{} 
    any = true
    any = 12.34
    any = "hello"
    any = map[string]int{"one": 1} 
    any = new(bytes.Buffer)

    물론 불리언, 부동소수점, 문자열, 맵, 포인터, 기타 타입의 interface{} 값을 생성하면 그 안에 어떤 메소드도 없으므로 해당 값에 대해 직접 할 수 있는 연산은 없고 값을 다시 밖으로 빼내는 방법은 타입 단언(type assertion)을 사용하는 것

    다음 대상을 인터페이스로 추상화해 표현

    Album
    Book
    Movie
    Magazine
    Podcast
    TVEpisode
    Track

    제목, 작성일, 작성자 목록(저자 또는 화가) 등의 일부 속성은 모든 상품에 공통으로 나타남

    type Artifact interface {
        Title() string
        Creators() []string
        Created() time.Time 
    }

    기타 속성은 특정 상품에 한정되며 출력된 단어 속성은 책과 잡지에만 있고 화면 해상도 속성은 영화와 TV 프로그램에만 있음

    type Text interface {
        Pages() int
        Words() int
        PageSize() int
    }
    type Audio interface {
        Stream() (io.ReadCloser, error) 
        RunningTime() time.Duration
        Format() string // e.g., "MP3", "WAV"
    }
    type Video interface {
        Stream() (io.ReadCloser, error) 
        RunningTime() time.Duration
        Format() string // e.g., "MP4", "WMV" 
        Resolution() (x, y int)
    }

    이런 인터페이스는 관련된 구상 타입을 하나로 묶고 공유하는 공통부분을 표현하기에 유용한 방법 중 하나

    구상 타입의 선언을 변경하지 않고 필요시마다 관심 항목에 대한 새 추상화나 관심사의 그룹을 정의할 수 있고, 이 방식은 구상 타입이 다른 개발자가 작성한 패키지에 있을 때 유용하며 물론 구상 타입 간에 공통점이 있어야 함

    7.4 flag.Value로 플래그 분석

    flag.Value는 표준 인터페이스 중 하나

    다음은 flag.Value에서 커맨드라인 플래스에 새 표기법을 정의하는 방법(지정된 시간 동안 잠들어 있는 프로그램)

    gopl.io/ch7/sleep
    //flag.Duration()의 첫 번째 인자가 커맨드라인 플래그명
    var period = flag.Duration("period", 1*time.Second, "sleep period")
    func main() { 
        flag.Parse()
        fmt.Printf("Sleeping for %v...", *period) time.Sleep(*period)
        fmt.Println()
    }
    

     

    flag.Duration 함수는 time.Duration 타입의 플래그 변수를 생성

    잠드는 기간은 커맨드라인 플래그 -period로 변경할 수 있음

    대칭적인 설계가 좋은 사용자 인터페이스

    • 새 플래그 정의

    flag.Value 인터페이스를 충족하는 타입을 정의하면 됨

    package flag
    // Value is the interface to the value stored in a flag. 
    type Value interface {
    	String() string
    	Set(string) error
    }

    String 메소드는 커맨드라인 도움말 메시지에 사용할 플래그의 값을 포매팅함

    모든 flag.Value는 fmt.Stringer임

    Set 메소드는 문자열 인자를 분석하고 flag 값을 갱신함

    Set 메소드는 String 메소드의 반대이며 같은 표기법을 사용하는 것이 좋음

     

    7.5 인터페이스 값

    인터페이스 타입의 값이나 인터페이스 값에는 두 개의 구성 요소인 구상 타입과 값이 있음

    이를 인터페이스의 동적 타입과 동적 값이라 함

    Go와 같은 정적 타입 언어에서는 타입이 컴파일 시의 개념이므로 타입은 값이 아님

    이 개념 모델에서 타입 설명자(type descriptor)라는 일련의 값에는 각 타입에 대한 이름과 메소드 같은 정보가 있음

    인터페이스 값에서 타입 구성 요소는 그에 해당하는 타입 설명자로 표현됨

    다음 네 개의 문장에서 w 변수는 세 가지 다른 값을 취함(첫 번째와 마지막 값은 같음)

    var w io.Writer
    w = os.Stdout
    w = new(bytes.Buffer) 
    w = nil
    • var w io.Writer

    Go에서 변수는 항상 미리 정의된 값으로 초기화하며 인터페이스도 마찬가지임

    인터페이스의 제로 값은 타입과 값 구성 요소가 둘다 nil임

    • w = os.Stdout

    구상 타입에서 인터페이스 타입으로의 묵시적 변환이 포함돼 있으며 명시적인 io.Writer(os.Stdout) 변환과 같음

    이러한 종류의 변환은 명시적이든 묵시적이든 피연산자의 타입과 값을 캡처함

    인터페이스 값의 동적 타입은 포인터 타입 *os.File의 타입 설명자로 지정되며 그 안에는 프로세스의 표준 출력을 나타내는 os.File에 대한 포인터인 os.Stdout의 복사본이 들어 있음

    일반적으로 컴파일 시에는 인터페이스 값의 동적 타입을 예상할 수 없으므로 인터페이스를 통한 호출은 동적으로 이뤄져야 함

     

    • w = new(bytes.Buffer)

    *bytes.Buffer 타입의 값을 인터페이스 값에 할당함

    이제 동적 타입은 *bytes.Buffer이며, 동적 값은 새로 할당된 버퍼에 대한 포인터임

    • w = nil

    인터페이스 값에 nil을 할당함

    두 구성요소가 모두 nil로 초기화되고, w는 그림 7.1과 같이 최초에 선언된 상태로 돌아감

     

    인터페이스 값은 임의의 큰 동적 값을 가질 수 있음

    var x interface{} = time.Now()

    개념적으로 동적 값은 타입이 아무리 크더라도 항상 인터페이스 값으로 쓸 수 있음(이는 단지 개념적인 모델이며 실제 구현은 상당히 다름)

    • 비교연산

    인터페이스 값은 ==나 !=로 비교할 수 있음

    두 인터페이스 값은 둘다 nil이거나 동적 타입이 같고, 동적 값이 해당 타입의 일반적인 ==에 따라 같을 때 동일함

    인터페이스 값은 비교할 수 있으므로 맵의 키나 스위치의 피연산자로 쓸 수 있음

    두 인터페이스 값을 비교할 때 동적 타입이 같지만 비교할 수 없는 타입(예: 슬라이스)이면 비교 연산에서 패닉이 발생함

    var x interface{} = []int{1, 2, 3}
    fmt.Println(x == x) // panic: comparing uncomparable type []int

    다른 타입은 비교할 수 있거나(기본 타입이나 포인터처럼) 아예 비교할 수 없지만(슬라이스, 맵, 함수처럼) 인터페이스 값을 비교하거나 인터페이스 값을 포함한 타입을 결합할 때에는 잠재적으로 패닉이 일어날 가능성에 주의해야 함

    인터페이스를 맵의 키나 스위치의 피연산자로 사용할 때에도 유사한 위험성이 내포돼 있음

    인터페이스 값은 비교할 수 있는 타입의 동적 값이 확실할 때에만 비교할 것

     

    • 동적 타입 확인

    오류를 처리하거나 디버그할 때는 인터페이스 값의 동적 타입을 보고하는 것이 도움이 됨

    이를 위해 fmt 패키지의 %T 포매터를 사용함

    var w io.Writer 
    fmt.Printf("%T\n", w) // "<nil>"
    w = os.Stdout
    fmt.Printf("%T\n", w) // "*os.File"
    w = new(bytes.Buffer) 
    fmt.Printf("%T\n", w) // "*bytes.Buffer"

    fmt는 내부적으로 리플렉션(reflection)을 사용해 인터페이스의 동적 타입을 얻음

     

    7.5.1 주의: nil 포임터가 있는 인터페이스는 nil이 아니다

    아무 값도 담고 있지 않은 nil 인터페이스 값은 nil일 수도 있는 포인터를 갖는 인터페이스 값과 다름

     

    7.6 sort.Interface로 정렬

    sort 패키지에는 어떤 시퀀스라도 임의의 순서 함수에 따라 시퀀스 안에서 직접 정렬(in-place sorting)하는 기능이 있음

    Go의 sort.Sort 함수는 시퀀스나 원소의 표현 방식에 대해 아무런 가정을 하지 않음

    대신 sort.Interface 인터페이스를 사용해 일반적인 정렬 알고리즘과 정렬되는 각 시퀀스 타입 간의 규약을 지정함

    이 인터페이스의 구현이 보통은 슬라이스인 구체적인 시퀀스 표현 방식과 원하는 원소 정렬 순서를 결정함

    직접 정렬하는 알고리즘에는 3가지(시퀀스의 길이, 두 원소의 비교 방법, 두 원소의 교환 방법)가 필요하므로 sort.Interface에는 3개의 메소드가 있음

    package sort
    type Interface interface {
    	Len() int
    	Less(i, j int) bool // i, j are indices of sequence elements
    	Swap(i, j int)
    }

    시퀀스를 정렬하려면 이 세가지 메소드를 구현하는 타입을 정의하고 해당 타입에 sort.Sort를 적용해야 함

    문자열 슬라이스 정렬하는 예

    새 타입 StringSlice와 이 타입의 Len, Less, Swap 메소드는 다음과 같음

    type StringSlice []string
    func (p StringSlice) Len() int { return len(p) }
    func (p StringSlice) Less(i, j int) bool { return p[i] < p[j] } 
    func (p StringSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }

    문자열 슬라이스 names를 다음고 같이 StringSlice로 변환해 정렬할 수 있음

    sort.Sort(StringSlice(names))

    위 변환으로 names와 같은 길이, 용량, 내부 배열을 갖지만, 정렬에 필요한 3가지 메소드가 추가된 타입의 슬라이스 값을 생성함

    • sort.Reverse 함수

    sort.Reverse 함수는 중요한 개념인 조합(composition(6.3절))을 사용함

    sort 패키지는 익스포트되지 않은 타입 reverse를 정의하며 이는 sort.Interface를 내장한 구조체

    reverse의 Less 메소드는 내장된 sort.Interface 값의 Less 메소드를 호출하지만 인덱스가 반대이므로 정렬 결과의 순서가 반대임

    package sort
    type reverse struct{ Interface } // that is, sort.Interface
    func (r reverse) Less(i, j int) bool { return r.Interface.Less(j, i) } 
    func Reverse(data Interface) Interface { return reverse{data} }

    reverse의 다른 두 메소드인 Len과 Swap은 reverse가 내장된 필드이므로 원본 sort.Interface에서 묵시적으로 주어진 것

    익스포트된 함수 Reverse는 원본 sort.Interface 값을 갖는 reverse 타입의 인스턴스를 반환

     

    7.7 http.Handler 인터페이스

    http.Handler 인터페이스 예제

    net/http
    package http
    type Handler interface {
    	ServeHTTP(w ResponseWriter, r *Request)
    }
    func ListenAndServe(address string, h Handler) error

    ListenAndServe 함수에는 "localhost:8000"와 같은 서버 주소 및 모든 요청이 전달되는 Handler 인터페이스가 필요함

    이 함수는 서버가 중단(또는 시작 실패)돼 nil이 아닌 오류를 반환하며 종료되지 않는 한 게속 실행됨

     

    • 프레임워크

    Go에는 루비의 레일즈나 파이썬의 장고 같은 표준 웹 프레임워크가 없으며 이러한 프레임워크가 존재하지 않는 것은 아니지만 Go의 표준 라이브러리는 이러한 프레임워크가 필요 없을 만큼 충분히 유연함

    프레임워크는 프로젝트 초기 단계에는 편리하지만 그에 따라 추가된 복잡성으로 인해 장기적으로 유지 보수가 어려워질 수 있음

     

    7.8 error 인터페이스

    error는 단순히 오류 메시지를 반환하는 하나의 메소드가 있는 인터페이스 타입

    type error interface {
    	Error() string
    }

    error를 만드는 가장 쉬운 방법은 주어진 오류 메시지에 관한 새 error를 반환하는 errors.New를 호출하는 것

    전체 error 패키지는 4줄에 불과함

    package errors
    func New(text string) error { return &errorString{text} } 
    type errorString struct { text string }
    func (e *errorString) Error() string { return e.text }

    errorString의 내부 타입은 문자열이 아닌 구조체 표현에 대한 우연한(또는 고의적인) 갱신을 방지함

    New 메소드를 호출할 때마다 서로 다른 별개의 error 인스턴스가 할당되므로 error 인터페이스에 errorString만이 아닌 포인터 타입 *errorString도 사용함

     

    7.9 예제: 표현식 평가기

    • Expr

    산술 표현식(arithmetic expression)을 평가하는 평가기(evaluator)를 만듦

    / An Expr is an arithmetic expression. 
    type Expr interface{}

    표현식 언어는 부동소수점 리터럴; 이항 연산자 +,-,*,/; 단항 연산자 +x와 -x; 함수 호출 pow(x, y), sin(x), sqrt(x); x나 pi 같은 변수; 괄호와 표준 연산자 우선순위로 구성되고 모든 값은 float64 타입임

    gopl.io/ch7/eval
    // A Var identifies a variable, e.g., x. 
    type Var string
    // A literal is a numeric constant, e.g., 3.141. 
    type literal float64
    // A unary represents a unary operator expression, e.g., -x. 
    type unary struct {
    	op rune // one of '+', '-'
    	x Expr 
    }
    // A binary represents a binary operator expression, e.g., x+y. 
    type binary struct {
    	op rune // one of '+', '-', '*', '/'
    	x, y Expr 
    }
    // A call represents a function call expression, e.g., sin(x). 
    type call struct {
    	fn string // one of "pow", "sin", "sqrt"
    	args []Expr 
    }

    변수를 갖는 표현식을 평가하려면 변수명을 값과 매핑하는 환경(environment)이 필요함

    type Env map[Var]float64

    주어진 환경에서 표현식 값을 반환하는 Eval 메소드를 표현식의 종류별로 정의해야 함

    모든 표현식에는 이 메소드가 있어야 하므로 Expr 인터페이스에 추가

    이 패키지는 Expr, Env, Var 타입만 노출

    사용자는 평가기를 사용할 때 그 외의 표현식 타입에 접근할 필요 없음

    type Expr interface {
    // Eval returns the value of this Expr in the environment env. 
    Eval(env Env) float64
    }

    다음은 구상 Eval 메소드

    Var의 메소드는 환경을 조회하고 변수가 정의돼 있지 않으면 0을 반환

    literal의 메소는 리터럴 값을 반환

    func (v Var) Eval(env Env) float64 {
    	return env[v]
    }
    func (l literal) Eval(_ Env) float64 {
    	return float64(l)
    }

    unary와 binary의 Eval 메소드는 재귀적으로 피연산자를 평가하고 피연산자에 연산 op를 적용

    call의 메소드는 pow, sin, sqrt 함수로 인자를 평가하고 math 패키지에 있는 대응하는 함수를 호출

    func (u unary) Eval(env Env) float64 { 
    	switch u.op {
    	case '+':
    		return +u.x.Eval(env)
    	case '-':
    		return -u.x.Eval(env)
    	}
    	panic(fmt.Sprintf("unsupported unary operator: %q", u.op)) 
    }
    func (b binary) Eval(env Env) float64 { 
    	switch b.op {
    	case '+':
    		return b.x.Eval(env) + b.y.Eval(env)
    	case '-':
    		return b.x.Eval(env) - b.y.Eval(env)
    	case '*':
    		return b.x.Eval(env) * b.y.Eval(env)
    	case '/':
    		return b.x.Eval(env) / b.y.Eval(env)
    	}
    	panic(fmt.Sprintf("unsupported binary operator: %q", b.op)) 
    }
    func (c call) Eval(env Env) float64 { 
    	switch c.fn {
    	case "pow":
    		return math.Pow(c.args[0].Eval(env), c.args[1].Eval(env))
    	case "sin":
    		return math.Sin(c.args[0].Eval(env))
    	case "sqrt":
    		return math.Sqrt(c.args[0].Eval(env))
    	}
    	panic(fmt.Sprintf("unsupported function call: %s", c.fn)) 
    }
    • TestEval

    TestEval 함수는 평가기의 테스트

    t.Errorf를 호출하면 오류를 보고함

    TestEval 함수는 3가지 표현식과 표현식별로 다른 환경을 정의하는 입력 테이블을 순회함

    첫 번째 표현식은 주어진 영역 A에서 원의 반지름을 계산

    두 번째 표현식은 변수 x와 y로 정의된 정육면체의 합을 계산

    세 번째 표현식은 화씨온도 F를 섭씨로 변환

    func TestEval(t *testing.T) { 
    	tests := []struct {
        expr string
    	env  Env
    	want string
    	}{
    		{"sqrt(A / pi)", Env{"A": 87616, "pi": math.Pi}, "167"}, 
        	{"pow(x, 3) + pow(y, 3)", Env{"x": 12, "y": 1}, "1729"}, 
        	{"pow(x, 3) + pow(y, 3)", Env{"x": 9, "y": 10}, "1729"}, 
        	{"5 / 9 * (F - 32)", Env{"F": -40}, "-40"},
    		{"5 / 9 * (F - 32)", Env{"F": 32}, "0"},
    		{"5 / 9 * (F - 32)", Env{"F": 212}, "100"},
    	}
        var prevExpr string
        for _, test := range tests {
            // Print expr only when it changes. 
            if test.expr != prevExpr {
                fmt.Printf("\n%s\n", test.expr)
                prevExpr = test.expr 
            }
            expr, err := Parse(test.expr) 
            if err != nil {
                t.Error(err) // parse error
                continue 
            }
            got := fmt.Sprintf("%.6g", expr.Eval(test.env)) 
            fmt.Printf("\t%v => %s\n", test.env, got)
            if got != test.want {
            t.Errorf("%s.Eval() in %s = %q, want %q\n", test.expr, test.env, got, test.want)  
            } 
        }
    }

    위 테스트는 테이블의 각 항목에 대해 표현식을 분석하고 환경 내에서 평가한 후 결과를 출력

    Parse 함수는 go get으로 패키지를 다운로드하면 볼 수 있음

    다음 go test 명령은 패키지의 테스트를 수행

    $ go test -v gopl.io/ch7/eval

    -v 플래그로 보통 이와 같이 성공하는 테스트에서는 생략되는 테스트의 출력 결과를 볼 수 있음

    다음은 테스트에서 fmt.Printf 구문의 출력

    sqrt(A / pi)
    	map[A:87616 pi:3.141592653589793] => 167
    pow(x, 3) + pow(y, 3) 
    	map[x:12 y:1] => 1729 
    	map[x:9 y:10] => 1729
    5 / 9 * (F - 32) 
    	map[F:-40] => -40 
    	map[F:32] => 0 
    	map[F:212] => 100
    • Check 메소드

    Check 메소드는 표현식 문법의 트리에서 정적 오류를 확인함

    type Expr interface {
    	Eval(env Env) float64
    	// Check reports errors in this Expr and adds its Vars to the set. 
        Check(vars map[Var]bool) error
    }

    다음은 구상 Check 메소드

    literal과 Var의 표현식은 실패할 수 없으므로 Check 메소드는 이 타입일 때 nil을 반환

    unary와 binary의 메소드는 먼저 연산자가 유효한지 확인한 후 재귀적으로 피연산자를 확인

    call의 메소드도 마찬가지로 먼저 함수가 알고 있는 함수이고 인자의 개수가 맞는지 확인한 후 재귀적으로 각 인자를 확인

    func (v Var) Check(vars map[Var]bool) error { 
    	vars[v] = true
    	return nil 
    }
    func (literal) Check(vars map[Var]bool) error {
    	return nil
    }
    func (u unary) Check(vars map[Var]bool) error { 
    	if !strings.ContainsRune("+-", u.op) {
    		return fmt.Errorf("unexpected unary op %q", u.op) 
        }
    	return u.x.Check(vars) 
    }
    func (b binary) Check(vars map[Var]bool) error { 
    	if !strings.ContainsRune("+-*/", b.op) {
    		return fmt.Errorf("unexpected binary op %q", b.op)
    	}
    	if err := b.x.Check(vars); err != nil {
    		return err 
        }
    	return b.y.Check(vars) 
    }
    func (c call) Check(vars map[Var]bool) error { 
    	arity, ok := numParams[c.fn]
    	if !ok {
    		return fmt.Errorf("unknown function %q", c.fn) 
        }
    	if len(c.args) != arity {
    		return fmt.Errorf("call to %s has %d args, want %d",
    			c.fn, len(c.args), arity)
    	}
    	for _, arg := range c.args {
    		if err := arg.Check(vars); err != nil { 
            	return err
    		} 
        }
    	return nil 
    }
    var numParams = map[string]int{"pow": 2, "sin": 1, "sqrt": 1}

    다음은 잘못된 입력과 그 때 발생하는 오류를 두 그룹으로 나열

    Parse 함수는 문법 오류를 보고함

    Check 함수는 의미상의 오류를 보고함

    x%2 			unexpected '%'
    math.Pi 		unexpected '.'
    !true 			unexpected '!'
    "hello"			unexpected '"'
    log(10)			unknown function "log"
    sqrt(1, 2)		call to sqrt has 2 args, want 1

    7.10 타입 단언

    타입 단언(type assertion)은 인터페이스 값에 적용되는 연산

    구문적으로는 x.(T) 형태이며, x는 인터페이스 타입의 표현식이고 T는 "단언" 타입

    타입 단언은 피연산자의 동적 타입이 단언 타입과 일치하는지 확인

    • 2가지 경우의 수

    첫 번째는 타입 T가 구상 타입이면 타입 단언은 x의 동적 타입이 T와 같은지 확인함

    타입이 확인되면 타입 단언의 결과는 타입이 T인 x의 동적 값임

    구상 타입 단언은 피연산자의 구상 값을 추출함

    확인되지 않으면 이 연산은 패닉을 발생

    var w io.Writer
    w = os.Stdout
    f := w.(*os.File) // success: f == os.Stdout
    c := w.(*bytes.Buffer) // panic: interface holds *os.File, not *bytes.Buffer

    두 번째는 단언 타입 T가 인터페이스 타입이면 타입 단언은 x의 동적 타입이 T를 충족하는지 확인함

    타입이 확인되면 동적 값이 추출되지 않음

    결과는 여전히 같은 타입과 값으로 구성된 인터페이스 값이지만 이 결과는 인터페이스 타입 T가 됨

    인터페이스 타입에 대한 타입 단언은 표현식의 타입을 변경해 다른 메소드에 접근할 수 있게 하지만 인터페이스 값 내부의 동적 타입과 값은 유지함

     

    다음은 첫 번째 타입 단언 후 w와 rw에 os.Stdout이 할당돼 둘 다 동적 타입 *os.File이 되지만 io.Writer인 w는 파일의 Write 메소드만 노출하고 rw는 Read 메소드도 노출함

    var w io.Writer
    w = os.Stdout
    rw := w.(io.ReadWriter) // success: *os.File has both Read and Write
    w = new(ByteCounter)
    rw = w.(io.ReadWriter) // panic: *ByteCounter has no Read method

    피연산자가 nil 인터페이스 값이면 어떤 타입으로 단언하더라도 실패함

    덜 제한적인 인터페이스 타입으로의 단언은 nil일 때를 제외하면 할당과 동일하게 동작하므로 거의 사용되지 않음

    w = rw // io.ReadWriter is assignable to io.Writer 
    w = rw.(io.Writer) // fails only if rw == nil

    인터페이스 값의 동적 타입이 확실치 않아서 특정 타입인지 확인하고 싶은 경우 다음 선언과 같이 타입 단언에 두 개의 결과를 할당하면 실패시 패닉 대신 성공 여부를 나타내는 불리언 값을 두 번째 결과로 반환

    var w io.Writer = os.Stdout
    f, ok := w.(*os.File) // success: ok, f == os.Stdout 
    b, ok := w.(*bytes.Buffer) // failure: !ok, b == nil
    

     

    위의 두 번째 결과는 일반적으로 ok라는 변수에 할당하고 이 연산이 실패하면 ok가 거짓이며, 첫 번째 결과는 위 예제에서 nil *bytes.Buffer인 단언 타입의 제로 값이 됨

    결과 ok는 다음처럼 다음에 무엇을 할지 결정하는 데 사용

    if f, ok := w.(*os.File); ok { 
    	// ...use f...
    }

    타입 단언의 피연산자가 변수이면 새 지역 변수 이름을 짓는 다음과 같이 기존 이름을 재사용해 원래 변수를 가리는 것을 종종 볼 수 있음

    if w, ok := w.(*os.File); ok { 
    	// ...use w...
    }

     

    7.11 타입 단언으로 오류 식별

    사용자는 오류의 유형을 구별하기 위해 타입 단언으로 특정 타입의 오류를 검출할 수 있음

    특정 타입에는 단순한 문자열보다 더 상세한 내용이 있음

    _, err := os.Open("/no/such/file")
    fmt.Println(err) // "open /no/such/file: No such file or directory" 
    fmt.Printf("%#v\n", err)
    // Output:
    // &os.PathError{Op:"open", Path:"/no/such/file", Err:0x2}

    7.12 인터페이스 타입 단언으로 동작 조회

     

    7.13 타입 변환

    인터페이스는 2가지 방식으로 사용

    첫 번째는 인터페이스의 메소드로 인터페이스를 충족하는 구상 타입의 유사도를 표현하고 구상 타입의 세부 구현과 고유 작업을 숨기는 방식으로 io.Reader, io.Writer, fmt.Stringer, sort.Interface, http.Handler, error가 좋은 예이고 이때의 중점은 구상 타입이 아닌 메소드에 있음

    두번째는 다양한 구상 타입 값을 저장할 수 있는 인터페이스 값의 기능을 활용해 인터페이스를 이러한 타입들의 결합으로 간주하는 것

    이러한 타입을 동적으로 식별하고 각각의 경우를 다르게 취급하기 위해 타입 단언을 사용

    이 방식에서는 중점이 인터페이스의 메소드가 아닌 인터페이스를 충족하는 구상 타입에 있으며 정보를 은폐하지 않음

     

    7.14 예제: 토큰 기반 XML 디코딩

    encoding/xml 패키지에는 XML을 디코딩하는 저수준의 토큰 기반 API가 있음

    토큰 기반 방식에서는 파서가 입력을 받아 주로 4가지(StartElement, EndElement, CharData, Comment) encoding/xml 패키지의 구상 타입으로 이뤄진 토큰의 스트림을 생성

    (*xml.Decoder).Token을 호출할 때마다 토큰을 반환

     

    7.15 몇 마디 조언

    인터페이스는 두 개 이상의 구상 타입을 같은 방식으로 처리해야 할 때에만 필요

    인터페이스는 적은 수의 간단한 메소드로만 구성

    필요한 것만 정의하는 것이 경험적으로 좋은 인터페이스 설계

     

     

     

    '프로그래밍 > Golang' 카테고리의 다른 글

    8장 고루틴과 채널  (0) 2019.11.13
    Golang 관련 도서 다운로드 및 번역 문서  (0) 2019.11.13
    6장 메소드  (0) 2019.11.04
    5장 함수  (0) 2019.11.03
    4장 복합 타입  (0) 2019.10.30
Designed by Tistory.