[GO] #2-5. 고랭 기본문법 (리시버, 인터페이스)

Study·2021년 5월 18일
0

고랭

목록 보기
6/18
post-thumbnail

리시버

GO 는 클래스를 가지지 않는다.

하지만 이와 같은 타입의 메소드를 정의할 수 있다.
그 함수는 특별한 receiver 인자가 있는 함수이다.

리시버func 키워드와 메소드 이름 사이의 자체 인수 목록에 나타난다.

아래 예제에서 Abs 메소드엔 v 라는 이름의 Vertex 유형의 리시버가 있다.

type Vertex struct {
	X, Y float64
}

func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
	v := Vertex{3, 4}
	fmt.Println(v.Abs())
}

리시버 인수가 없이 일반 함수로 작성된 Abs 는 다음과 같을 것이다.

type Vertex struct {
	X, Y float64
}

func Abs(v Vertex) float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
	v := Vertex{3, 4}
	fmt.Println(Abs(v))
}

구조체가 아닌 형식에 대해서도 메소드를 선언할 수 있다.

다음 예를 살펴보자.

type MyFloat float64

func (f MyFloat) Abs() float64 {
	if f < 0 {
		return float64(-f)
	}
	return float64(f)
}

func main() {
	f := MyFloat(-math.Sqrt2)
	fmt.Println(f.Abs())
}

위 예에서는 Abs 메소드가 있는 숫자 유형 MyFloat 을 확인할 수 있다.

메소드와 동일한 패키지에 유형이 정의된 수신자가 있는 메소드만 선언할 수 있다.

1. 포인터 리시버

리시버는 포인터로도 메소드를 선언할 수 있다.

이는 리시버 유형이 일부 유형 T 에 대한 리터럴 구문 *T 를 가짐을 의미한다.

다음 예를 살펴보자.

type Vertex struct {
	X, Y float64
}

func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func (v *Vertex) Scale(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

func main() {
	v := Vertex{3, 4}
	v.Scale(10)
	fmt.Println(v.Abs())
}

위의 Scale 방법은 *Vertex 에 정의되어 있다.

포인터 리시버가 있는 메소드는 Scale 처럼 리시버가 가리키는 값을 수정할 수 있다.

메소드는 종종 리시버를 수정해야하기에 포인터 리비서가 값 수신기보다 더 일반적이다.

위 예제에서 Scale 함수 선언에서 * 를 선언하고 실행했을 때와 제거하고 실행했을 때의 결과를 살펴보자.

두 가지 경우의 실행결과를 보면 알 수 있듯, main 함수에 선언된 Vertex 값을 변경하기 위한 포인터 리시버가 있어야 한다.

2. 메소드와 포인터 우회

다음 예제를 살펴보자.

func Scale(v Vertex, f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

func main() {
	v := Vertex{3, 4}
	Scale(&v, 10)
}

위 결과를 실행하면 컴파일 에러가 발생한다.

cannot use &v (type *Vertex) as type Vertex in argument to Scale

위 에러에서 볼 수 있듯, 포인터 인수의 함수는 동일하게 포인터를 사용해야함을 알 수 있다.

그렇다면 다음 예제를 살펴보자.

type Vertex struct {
	X, Y float64
}

func (v *Vertex) Scale(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

func ScaleFunc(v *Vertex, f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

func main() {
	v := Vertex{3, 4}
	v.Scale(2)
	ScaleFunc(&v, 10)

	p := &Vertex{4, 3}
	p.Scale(3)
	ScaleFunc(p, 8)

	fmt.Println(v, p)
}

포인터 인수의 함수가 존재하기 때문에 포인터를 사용해야 한다.

var v Vertex
ScaleFunc(v, 5)		// 컴파일 에러
ScaleFunc(&v, 5)	// OK

포인터 리시버가 있는 메소드는 다음과 같이 호출될 때 값이나 포인터를 리시버로 받아들인다.

var v Vertex
v.Scale(5)  // OK
p := &v
p.Scale(10) // OK

두 번째 줄 v.Scale(5) 와 같은 경우는 포인터가 아니라 값인데도 포인터 리시버가 있는 메소드는 자동으로 호출된다.

즉, Scale 메소드가 포인터 리시버를 가졌기 때문에 편의상 GO 는 v.Scale(5) 라는 것을 (&v).Scale(5) 로 해석한다.

이러한 상황은 역방향에서도 일어날 수 있다.

다음 예제를 살펴보자.

type Vertex struct {
	X, Y float64
}

func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func AbsFunc(v Vertex) float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
	v := Vertex{3, 4}
	fmt.Println(v.Abs())
	fmt.Println(AbsFunc(v))

	p := &Vertex{4, 3}
	fmt.Println(p.Abs())
	fmt.Println(AbsFunc(*p))
}

값 인수를 사용하는 함수는 다음과 같이 특정 유형의 값을 사용해야 한다.

var v Vertex
fmt.Println(AbsFunc(v))		// OK
fmt.Println(AbsFunc(&v))	// 컴파일 에러

리시버 값이 있는 메소드는 다음과 같이 호출될 때, 값이나 포인터를 리시버로 사용한다.

var v Vertex
fmt.Println(v.Abs()) // OK
p := &v
fmt.Println(p.Abs()) // OK

이 경우에, p.Abs() 라는 메소드는 (*p).Abs() 로 해석된다.

3. 리시버 정리

포인터 리시버를 사용하는 데에는 크게 두 가지 이유가 있다.

  1. 메소드가 리시버가 가리키는 값을 수정할 수 있음
  2. 각각의 메소드 call 에서의 value 복사 문제를 피하기 위함

그렇기 때문에 리시버가 큰 구조체라면 더 효율적일 수 있다.

type Vertex struct {
	X, Y float64
}

func (v *Vertex) Scale(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

func (v *Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
	v := &Vertex{3, 4}
	fmt.Printf("Before scaling: %+v, Abs: %v\n", v, v.Abs())
	v.Scale(5)
	fmt.Printf("After scaling: %+v, Abs: %v\n", v, v.Abs())
}

위 예제에선 Abs 메소드는 굳이 리시버 방식으로 할 필요는 없지만 ScaleAbs 는 모두 *Vertex 라는 리시버 타입으로 되어 있다.

일반적으로 특정 유형의 모든 방법에는 값이나 포인터 리시버가 있어야 하지만 둘 다 혼합되어서는 안된다.

인터페이스

1. 인터페이스 기초

interface 타입은 메소드의 시그니처 집합으로 정의된다.

인터페이스 유형의 값은 해당 메소드를 구현하는 모든 값을 보유할 수 있다.
그래서 인터페이스는 객체의 행위를 지정해 주는 하나의 방법이다.

먼저 인터페이스를 정의한다.

type Abser interface {
	Abs() float64
}

그 후에 구조체와 타입을 정의하고 그에 맞는 Abs 함수를 정의한다.

type MyFloat float64

func (f MyFloat) Abs() float64 {
	if f < 0 {
		return float64(-f)
	}
	return float64(f)
}

type Vertex struct {
	X, Y float64
}

func (v *Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

이제 구현한 Abs 함수를 인터페이스로 사용해보자.

func main() {
	var a Abser
	f := MyFloat(-math.Sqrt2)
	v := Vertex{3, 4}

	a = f  // 구현됨
	fmt.Println(a.Abs())
	a = &v // 구현됨
	fmt.Println(a.Abs())
    
	// 아래 라인의 v 는 *Vertex 가 아님
	// 그리고 Vertex 는 구현되지 않음
	a = v
	fmt.Println(a.Abs())
}

위와 같이 사용할 수 있으며, Vertex 에 대해서는 구현을 하지 않았기 때문에 오류가 발생한다.

1-1. 암시적 구현

GO 에서는 인터페이스를 구현할 때, 명시적 intent 선언도, implementation 키워드도 존재하지 않는다.

type T struct {
	S string
}

// 이 메소드는 I 인터페이스에 대한 T 를 구현하는 것임
// 그러나 명시적으로 선언할 필요가 없음
func (t T) M() {
	fmt.Println(t.S)
}

이렇게 암시적 인터페이스는 인터페이스의 정의를 구현으로부터 분리하며, 이는 사전 정렬 없이 어느 패키지에든 등장할 수 있다.

1-2. 인터페이스 값

인터페이스의 값은 값과 콘크리트 타입의 튜플이라고 생각할 수 있다.

(value, type)

인터페이스 값은 특정 기초 콘크리트 유형의 값을 가진다.

인터페이스 값으로 메소드를 호출하면 기본 형식에 동일한 이름의 메소드가 실행된다.

먼저 인터페이스, 구조체, 타입과 메소드를 정의한다.

type I interface {
	M()
}

type T struct {
	S string
}

func (t *T) M() {
	fmt.Println(t.S)
}

type F float64

func (f F) M() {
	fmt.Println(f)
}

그리고 다음으로 main 함수와 describe 함수를 정의한다.

이때 describe 함수의 인수를 주목해보자.

func main() {
	var i I

	i = &T{"Hello"}
	describe(i)
	i.M()

	i = F(math.Pi)
	describe(i)
	i.M()
}

func describe(i I) {
	fmt.Printf("(%v, %T)\n", i, i)
}
(&{Hello}, *main.T)
Hello
(3.141592653589793, main.F)
3.141592653589793

describe 함수의 인자를 살펴보면 인터페이스를 받는 것을 확인할 수 있다.

이는 앞서 설명했듯, 메소드를 호출하면 기본 형식에 동일한 이름의 메소드가 실행하게 된다는 것이다.

1-3. Nil 인터페이스 값

인터페이스 자체 내부의 콘크리트 값이 0 일 경우엔 그 메소드는 nil 리시버로 호출된다.

일부 언어는 null 포인트 예외를 발생시키겠지만, GO 에서는 nil 리시버로 호출되는 매우 좋은 방법을 사용하는 것이 일반적이다.

nil 콘크리트 값을 갖는 인터페이스 값 자체가 nil 이 아니라는 것을 유의하자.

type I interface {
	M()
}

type T struct {
	S string
}

func (t *T) M() {
	if t == nil {
		fmt.Println("<nil>")
		return
	}
	fmt.Println(t.S)
}

func main() {
	var i I

	var t *T
	i = t
	describe(i)
	i.M()
}

func describe(i I) {
	fmt.Printf("(%v, %T)\n", i, i)
}

실행 결과

(<nil>, *main.T)
<nil>

하지만 다음의 nil 인터페이스에서 메소드를 호출하는 것은 런타임 에러가 발생한다.

왜냐면, 어떠한 구체적인 메소드를 호출할지 나타내는 인터페이스 튜플 내부의 타입이 없기 때문이다.

type I interface {
	M()
}

func main() {
	var i I
	describe(i)
	i.M()
}

func describe(i I) {
	fmt.Printf("(%v, %T)\n", i, i)
}
(<nil>, <nil>)
panic: runtime error: invalid memory address or nil pointer dereference ...

1-4. 빈 인터페이스 값

0 메소드를 지칭하는 인터페이스를 빈 인터페이스 라고 한다.

interface{}

빈 인터페이스는 모든 유형의 값을 가질 수 있다. (모든 유형은 최소 0개의 메소드를 구현)

빈 인터페이스는 알 수 없는 값을 처리하는데 이용된다.
예를 들면 fmt.Printinterface{} 타입의 어떤 인수라도 취할 수 있다.

func main() {
	var i interface{}
	describe(i)

	i = 42
	describe(i)

	i = "hello"
	describe(i)
}

func describe(i interface{}) {
	fmt.Printf("(%v, %T)\n", i, i)
}

위 결과는 다음과 같다.

(<nil>, <nil>)
(42, int)
(hello, string)

보는 바와 같이 각각의 인수에 따라 타입이 취하는 것을 확인할 수 있다.

2. 타입

2-1. 타입 선언

타입 선언은 인터페이스 값의 기초적인 콘크리트 값에 대한 접근을 제공한다.

t := i.(T)

이는 인터페이스 값 i 가 콘크리트 타입 T 를 갖고 있으며, 그 기본 값인 T 값을 변수 t 에 할당하고 있다는 것을 선언한다.

만약 iT 를 갖지 못하면 그 선언은 panic 상태가 된다.

인터페이스 값이 특정 유형을 보유하고 있는지 여부를 테스트하기 위해,
타입 선언엔 두 가지 값, 즉 기본 값과 선언 성공 여부를 부울 값으로 반환할 수 있다.

t, ok := i.(T)

iT 를 갖고 있다면, t내제된 값이 되며, ok 가 true 를 반환한다.

만약 그렇지 않으면 ok 는 false 가 되고 tT 란 유형의 zero 값이 되며 어떠한 패닉도 발생하지 않는다.

func main() {
	var i interface{} = "hello"

	s := i.(string)
	fmt.Println(s)

	s, ok := i.(string)
	fmt.Println(s, ok)

	f, ok := i.(float64)
	fmt.Println(f, ok)

	f = i.(float64) // panic
	fmt.Println(f)
}

결과

hello
hello true
0 false
panic: interface conversion: interface {} is string, not float64

2-2. 타입 스위치

타입 스위치는 여러 타입의 선언을 직렬로 허용하는 구조다.

일반 스위치문 같지만 타입 스위치문의 경우 값이 아닌 타입을 명시하며,
그 값드은 지정된 인터페이스 값에 의해 유지되는 값의 타입과 비교된다.

switch v := i.(type) {
	case T:
		// T 타입을 가졌을 때
	case S:
		// S 타입을 가졌을 때
	default:
		// 매칭되는 타입이 없을 때
}

타입 스위치 선언은 i.(T) 와 같은 구문을 가진다.

func do(i interface{}) {
	switch v := i.(type) {
	case int:
		fmt.Printf("Twice %v is %v\n", v, v*2)
	case string:
		fmt.Printf("%q is %v bytes long\n", v, len(v))
	default:
		fmt.Printf("I don't know about type %T!\n", v)
	}
}

func main() {
	do(21)
	do("hello")
	do(true)
}

결과

Twice 21 is 42
"hello" is 5 bytes long
I don't know about type bool!

2-3. Stringers

가장 널리 사용되는 인터페이스 중 하나는 fmt 패키지의 Stringer 이다.

type Stringer interfaec {
	String() string
}

Stringer 는 자신을 문자열로 설명할 수 있는 타입이다.
fmt 패키지 및 기타 여러 패키지는 값을 출력하기 위해 이 인터페이스를 사용한다.

type Person struct {
	Name string
	Age  int
}

func (p Person) String() string {
	return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}

func main() {
	a := Person{"Arthur Dent", 42}
	z := Person{"Zaphod Beeblebrox", 9001}
	fmt.Println(a, z)
}

결과

Arthur Dent (42 years) Zaphod Beeblebrox (9001 years)

에러

GO 프로그램은 error 값으로 오류 상태를 표현한다.

error 타입은 fmt.Stringer 와 유사한 내장 인터페이스다.

type error interface {
	Error() string
}
// fmt.Stringer 와 마찬가지로
// fmt 패키지는 값 출력 시 error 인터페이스를 찾음

함수는 종종 error 값을 반환하는데, 호출 코드는 오류가 nil 과 같은지 테스트하여 오류를 처리해야 한다.

i, err := strconv.Atoi("42")
if err != nil {
	fmt.Printf("숫자로 변환 안됨 : %v\n", err)
	return
}
fmt.Println("integer 변환 : ", i)

nil error 는 성공을 나타내며, nil 이 아닌 error 는 실패를 나타낸다.

integer 변환 : 42

Atoi("hello") 로 수정 후 실행해보자.

hello 문자열을 숫자로 변환하지 못하기 때문에 error 를 반환하여 nil 이 아니기 때문에 다음과 같은 결과를 초래할 것이다.

couldn't convert number: strconv.Atoi: parsing "hello": invalid syntax

이외의 인터페이스

1. 리더

io 패키지는 데이터 스트림의 읽기를 나타내는 io.Reader 인터페이스를 지정한다.

GO 표준 라이브러리에는 파일, 네트워크 연결, 압축기, 암호 등을 포함하여 인터페이스의 많은 구현이 포함되어 있다.

io.Reader 인터페이스에는 다음과 같은 Reader 메소드가 있다.

func (T) Read(b []byte) (n int, err error)

Read 는 주어진 바이트 조각을 데이터로 채우고 채워진 바이트 수와 오류 값을 반환한다.

스트림이 종료되면 io.EOF 오류를 반환한다.

다음 예제는 strings.Reader 에서 생성할 수 있고 한번에 8 바이트 씩 사용한다.

func main() {
	r := strings.NewReader("Hello, Reader!")

	b := make([]byte, 8)
	for {
		n, err := r.Read(b)
		fmt.Printf("n = %v err = %v b = %v\n", n, err, b)
		fmt.Printf("b[:n] = %q\n", b[:n])
		if err == io.EOF {
			break
		}
	}
}

결과는 다음과 같다.

n = 8 err = <nil> b = [72 101 108 108 111 44 32 82]
b[:n] = "Hello, R"
n = 6 err = <nil> b = [101 97 100 101 114 33 32 82]
b[:n] = "eader!"
n = 0 err = EOF b = [101 97 100 101 114 33 32 82]
b[:n] = ""

2. 이미지

다음 image 인터페이스는 다음과 같이 정의되어 있다.

package image

type Image interface {
    ColorModel() color.Model
    Bounds() Rectangle
    At(x, y int) color.Color
}

color.Colorcolor.Model 타입도 인터페이스지만,
사전 정의된 구현 color.RGBAcolor.RGBAModel 을 사용하여 이를 무시한다.

이러한 인터페이스 및 타입은 다음 사이트에서 자세히 확인할 수 있다.

profile
Study

0개의 댓글