Backend with Golang

Effective Go - Part 3

아직개구리 2023. 8. 13. 16:46

Interface

  • 인터페이스는 어떤 객체가 정해진 행동을 할 수 있다면, 어떤 곳에서 쓰일 수 있다는 object의 행동을 명시한다.
  • 인터페이스의 이름은 보통 method의 이름에서 파생된다.

Conversion

  • type Sequence []int일때  프린팅에 필요한 메서드인 String 함수를 만든다면 사실 Sprint 함수가 slice를 가지고 하는 일을 반복하는 꼴이 된다. 이때 Sprint 를 실행하기 전에 Sequence 타입을 []int로 변환할 수 있다. 이름만 무시하면 동일하기 때문에 conversion이 가능한 것이고, 새로운 값을 만들어 내지 않고 현재 값에 새로운 타입이 있는 것처럼 일시적으로 행동하게 된다. (int -> floating point로 변환할 때는 새로운 값을 만드는 legal conversion 이다.)
  •  이렇게 다른 메소드들의 집합을 사용하기 위해서 타입을 변경하는 것은 Go에서 자연스러운 표현방식이다. 여러개의 타입으로도 변환이 가능하며, 각 타입이 주어진 작업의 일정 부분을 감당하게 되는 것이다. 

Type switch 

type switch performs several type assertions in series and runs the first case with a matching type. 

switch 구문은 인터페이스 변수의 동적 타입을 확인하는 데 사용된다.

type Stringer interface {
    String() string
}

var value interface{} //value provided by caller
switch str := value.(type) {
case string:
    return str
case Stringer:
    return str.String()
}

이때 이 코드는 value가 string이면 인터페이스가 잡고 있는 actual string value를 그리고 Stringer 타입이라면 String method를 실행한 결과를 내보낸다. 

Type Assertion

type assertion provides access to an interface value's underlying concrete value.

만약 하나의 타입만 관심있고, 그냥 그 값만 추출하는 것이 목적이라면, Type switch 보단 Type Assertion을 사용한다.  

type 키워드 대신 typeName을 집어넣어서 값을 추출한다. value.(string) 이런 식으로 string 값을 extract하고자 할때 이 value가 string을 가지고 있지 않으면 프로그램은 runtime error를 내고 죽는다. 이때 "comma, ok" idiom을 사용해서 있는지 없는지 안전하게 테스트할 수 있다. comma ok구문에서  Type assertion이 실패할 경우, str, ok := value.(string)에서 str은 string의 제로값인 빈 문자열을 가지게 된다. 

 

** 문서 안에서 순서가 switch문부터 나와서 type switch문이 먼저 나왔지만, 이보다도 하나의 값으로 conversion 하기를 원한다면 type assertion을 통해서 얻을 수 있다는 것이다. 

Generality 

오직 interface를 implement하기 위해서 만들어진 type일 경우에, 더욱이 interface 가 아닌 method를 export하지 않고 싶을 때, 그 타입 자체는 export할 필요가 없다. 단지 interface만 export하는 것으로 interface바깥에 정의된 다른 behavior들은 의미가 없다는 것을 나타낼 수 있다. 이런 경우에, constructor는 구현 타입 보다는 인터페이스 값을 반환해야한다. 

(이 아래 부분 이해 안감.) 

Interfaces & Methods

거의 모든것에 method를 가질수 있는 것은 결국 모든 것이 interface가 될 수 있는 조건을 만족시킨다는 것이기도 하다. Handler Interface를 예로 들을 수 있는데, 이 handler 를 구현하는 어떤 객체도 HTTP Request를 처리할 수 있게 된다. 

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

여기서 ResponseWriter 역시 클라이언트 응답 반환하는데 필요한 method들을 제공하는 interface이다.  standard Write 메서드를 포함하고 있어서 io.Writer가 사용되는 곳이면 어디든 사용할 수 있다. Request는 클라이언트로부터 오는 request의 내용을 담은 struct이다. (fmt.Fprint를 사용하면, responseWriter에 응답을 기록할 수 있다. ) 

 

  • 만약에 내부 상태가 페이지 방문을 알아야한다면? 웹페이지에 채널을 연결하면 된다. channel 을 연결한 형태는 receiver가 channel이고 ServeHTTP 함수 내에서 그 채널에 값을 sending하는 것이다.
  • 만약에 server binary를 부를 때 사용된 argument 들을 나타내고 싶다면? 
func ArgServer() {
	fmt.Println(os.Args)
}

위와 같은 함수를 호출하면 되는데, 이럴 때 사실 이걸 위한 type을 만드는 건 별로 좋은 방법이 아니다. 더 cleaner way가 있다. 

pointer와 interface이외에 모든 type에 대해서 method를 정의할 수 있기 때문에, function을 위한 method 도 정의할 수 있다. 

// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers.  If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler object that calls f.
type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, req).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, req *Request) {
    f(w, req)
}

HandlerFunc은 ServeHTTP method를 가지는 하나의 타입이다. receiver는 함수이고, 메서드 안에서 해당 함수를 호출한다. 이것은 채널을 연결했던 것과 별반 다르지 않다. 

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

이렇게  ArgServer와 HandlerFunc는 같은 signature를 가지기 때문에, type conversion을 통해서 ArgServer를 HanlderFunc타입으로 바꿔줄 수 있다. 바꿔주게 되면, HanlderFunc 의 ServeHTTP 메서드를 실행하게 된다. 

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

 

The blank identifier 

The blank identifier can be assigned or declared with any value of any type, with the value discarded harmlessly.  

  • 에러값 무시는 안됨. 나쁜 관행
  • Unused Imports & variables 가 있다면 compile이 안됨. 컴파일이 되게 만들고 싶다면, 아래와 같이 할 수 있다.  
    • var _ = fmt.Printf : import error를 안내게 하기 위해서는 import 구문 다음에 위치하게 만들고, 주석을 달아줘서 나중에 정리해할 때 찾기 쉽도록 한다. 
    • _ = fd
  • import for side effect: 만약에 explicit use는 없지만, side effect를 위해서 import를 해야할 수도 있는데, 이때 blank identifier가 사용될 수 있다. 예를 들어 net/http/pprof패키지에서는 이 패키지의 init function을 실행하면, 디버깅 정보를 제공하는 http handler를 등록하게 된다. 이것이외에도 exported된 API를 가지고 있지만, 대다수의 경우에는 핸들러 등록만 필요하고, 다른 정보는 웹페이지를 통해서 접근한다. 그럴 경우에 import _ "net/http/pprof" 이런식으로 import 될 수 있다. 이런 구문을 가지고 있다면 이 파일에서는 패키지가 이름을 가지지 않기 때문에 사용될 가능성이 없다는 것을 뜻한다. 

Interface checks

  • 타입이 interface를 구현했음을 명시적으로 declare하지 않아도 된다. 대신에 interface의 method를 구현하기만 하면 된다. 거의 모든 경우에, interface conversion 은 static하며, 그렇기 때문에 compile할 때 체크가 된다.  
    • 하지만 몇몇 경우에서는 interface check가 run-time에 이루어지기도 한다. encoding/json 패키지에서 예시를 들 수 있다. JSON encoder가 Marshaler interface 를 구현하는 value를 받는다면, 이 encoder는 그 value의 marshalling method를 사용해서 JSON 으로 바꾸는 작업을 하게 된다. encoder는 이 성질을 runtime으로 type assertion을 사용해서 체크한다. 
if _, ok := val.(json.Marshaler); ok {
    fmt.Printf("value %v of type %T implements json.Marshaler\n", val, val)
}

 

이런 상황이 일어나는 경우는 어떤 type을 구현하는 package내에서 그 타입이 interface를 실제로 만족함을 보장해야 하는 때이다. 예를 들어 json.RawMessage같이 custom JSON representation 을 필요로하는 type인 경우에, json.Marshaler를 무조건 만족해야한다. 하지만 static conversion 구문이 없다면, compile할 때 verify하지 못한다.

만약에 interface를 만족하지 못하게 되면, JSON encoder는 작동은 하겠지만, custom implementation을 실행하진 않는다. 이런 경우, implementation이 맞다는 것을 보장하기 위해서, 전역에서 blank identifier를 사용한다. 이 선언에서 (*RawMessage)가 Marshaler로 conversion 되기 위해서는 *RawMessage가 Marshaler를 implement해야하고, 이 특성은 compile할 때 체크가 된다. 그래서 json.Marshaler interface가 만약 변동이 있을 때, package는 더 이상 compile 되지 않을 것이고 그때 코드를 update해야함을 알 수 있게 된다.  이 구조에서 blank identifier를 사용했기 때문에 단지 타입 검사만을 위해서 존재하는 구문임을 알 수 있다. ( 이 선언은 하나의 interface를 만족하는 모든 타입에 사용하지말고, 코드에 존재하는 static conversion이 없는 경우에만 사용하라. ) 

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

궁금증

질문:

interface를 구현하는 것과 달리 어떤 타입에 대해서 method를 만드는 것은 사용하는 용도가 어떻게 다른가? 

어떤 다른 여러가지 타입이 하나의 interface를 구현했다고 할 수 있는 것이 차이인가? 

어떤 struct가 있고, 그 struct안에 member변수같은 것들이 선언되어 있고, 그리고 그 struct가 특정 interface의 구조를  implement했을 때 그 interface 타입을 통해서 member 함수를 실행하는 것처럼 하며, 여러 type이 같은 interface를 implement해서 같은 메서드를 호출할 수 있는 것? 

 

질문 

만약에 servehttp 메소드의 receiver가 포인터가 아니라면 value로 값을 가져오는 것인가? 

(real server에서는 concurrent한 값에 대한 access를 위해서 sync 나 atomic 패키지를 참고해서 고쳐야한다.)  

 

'Backend with Golang' 카테고리의 다른 글

Rate Limiting  (1) 2023.09.06
Effective Go - Part 4: Embedding & Concurrency  (0) 2023.08.16
Effective Go - Part 2  (0) 2023.08.10
Effective Go - Part 1  (1) 2023.08.09
[Network] IP Class, Subnet Mask, CIDR  (0) 2023.08.03