Go 언어는 객체지향 프로그래밍(OOP)을 고유의 방식으로 지원한다. 타 언어의 OOP의 클래스가 필드와 메서드를 함게 갖는 것과 Go 언어에서는 struct가 필드만을 가지며, 메서드는 별도로 분리되어 정의된다.
Go 메서드는 특별한 형태의 func 함수이다. 메서드는 함수 정의에서 func 키워드와 함수명 사이에 "그 함수가 어느 struct를 위한 메서드인지"를 표시하게 된다. 이를 *receiver로 불려진다.
이 부분은 메서드가 속한 struct 타입과 struct 변수명을 지정하는데, struct 변수명은 함수 내에서 입력파라미터 처럼 사용된다. 구조체의 객체는 rect.area() 처럼 area 메서드를 struct 객체로부터 직접 호출할 수 있다.
package methodtest
import "fmt"
type Rect struct {
width, height int
}
// Rect struct를 위한 메서드란것을 선언 (메서드는 클래스에 종속되있는것, 여기서는 struct)
// 해당 struct에 선언한 필드를 꺼내올 수 있다
func (r Rect) area() int {
return r.width * r.height
}
func Methodtest() {
rect := Rect{10, 20}
area := rect.area()
fmt.Println(area)
}
저번에 얘기 했던 pass by value 와 pass by pointer 처럼 value로 전달하면 마찬가지로 copy가 되어 전달되기 때문에 원본의 값은 변경이 안될 것이고, pointer로 전달하면 원본의 값이 변경 될 것이다.
위의 Rect.area() 메서드는 value receiver이고 pointer receiver는 아래와 같다
package methodtest
import "fmt"
type Rect struct {
width, height int
}
func (r Rect) area() int {
return r.width * r.height
}
func (r *Rect) areaPointer() int {
r.width++
return r.width * r.height
}
func Methodtest() {
rect := Rect{10, 20}
area := rect.area()
fmt.Println(area)
areaPointer := rect.areaPointer()
fmt.Println(area, rect, areaPointer)
}
////
200 {11 20} 220
go언어의 인터페이스는 객체의 행위를 지정해주는 하나의 방법이다. 인터페이스를 사용하는 가장 큰 이유는 어떤 값이 어떤 특징 타입을 갖는지 관심이 없는 경우가 있기 때문이다.
그 값이 무엇인지 보다는, 어떤 행동을 하는지, 어떤 일을 할 수 있는 지에 대해 더 관심을 두는 경우가 있다.
즉, 어떤 값에서 특정 메서드를 호출할 수 있는 지가 주요 관심사가 된다. 쉽게 예를 들면 자판기가 있다면, 버튼을 눌러서 음료수를 꺼내는게 관심사지, 어느 제조사에서 이 자판기를 만들어 졌는지는 중요하지 않다.
구조체가 필드들의 집합체라면, interface는 메서드들의 집합체가 된다. interface는 타입(type)이 구현해야 하는 메서드 원형(prototype)들을 정의한다. 하나의 사용자 정의 타입이 interface를 구현하기 위해서는 단순히 그 인터페이스가 갖는 모든 메서드들을 구현하면 된다.
package interfacetest
import "fmt"
type TapePlayer struct {
Batteries string
}
func (t TapePlayer) Play(song string) {
fmt.Println("playing", song)
}
type TapeRecoder struct {
Microphones int
}
func (t TapeRecoder) Play(song string) {
fmt.Println("recording", song)
}
func playlist(device TapePlayer, songs []string) {
fmt.Println(device, "디바이스")
for _, song := range songs {
device.Play(song)
}
}
func Interfacetest() {
player := TapePlayer{}
mixtape := []string{"first", "second", "third"}
playlist(player, mixtape)
}
위 코드에서 playlist 함수를 실행시키면 playing ${mixtape의 인자들} 이런식으로 출력이 될 것이다.
허나 이 상황에서 문제가 하나 있다. 위의 TapeRecoder에 종속되어 있는 Play메서드도 분명 거의 같은 기능을 하는데, 저 메서드를 쓰기 위해서는 새롭게 playList 함수를 다시 만들고, 이름도 다르게하고, 매개변수 또한 TapeRecoder로 바꾸어 줘야 할 것이다.
이는 매우 번거롭고 불필요한 반복 작업이다. 단지 타입이 다르다는 이유로 같은 동작을 하는 다른 함수를 만드는 것은 매우 비효율 적이다.
즉 위의 인터페이를 사용하는 가장 큰 이유는 어떤 값이 어떤 특징 타입을 갖는지 관심이 없는 경우 가 될 것이다.
이 때 인터페이스를 이용하게 된다. playlist() 함수의 device 매개변수는 어떤 특징 타입을 갖는 지 관심이 없다. 같은 동작을 하는지가 중요하다.
Go에서 인터페이스는 특정 값이 가지고 있기를 기대하는 메서드의 집합으로 정의된다. 즉 위에서 말했듯이 특정한 동작 또는 로직을 수행할 수 있는 타입이 지녀야 하는 동작들의 집합이라 생각할 수 있다.
특정 값이 가지고 있기를 기대하는 메서드 집합
interface 키워드 뒤, 메서드가 가지고 있기를 기대하는 매개변수, 반환값의 타입을 기입하여 작성한다.
type myInterface interface {
methodWithoutParameters()
methodWithParameter(float64)
methodWithReturnValue() string
}
interface 안에 정의되는 메서드들은 반드시 매개변수와 리턴 타입을 명시해야 한다.
인터페이스 정의에 나열된 모든 메서드르 가진 타입은 해당 인터페이스를 만족한다고 한다.
인터페이스를 만족하는 타입은 해당 인터페이스가 필요한 모든 곳에서 사용할 수 있다.
인터페이스를 만족하려면 인터페이스에 정의된 메서드명, 매개변수 타입, 그리고 반환 값 타입이 모두 일치해야 한다.
타입은 여러 인터페이스를 만족할 수 있으며, 인터페이스 또한 인터페이스를 만족하는 여러 타입을 가질 수 있다.
인터페이스에 값을 담을 때, 인터페이스가 요구하는 메서드를 구현한 타입의 인스턴스여야 한다. 따라서 인터페이스에 값을 담을 때 단순한 기본 타입인 int를 직접 사용할 수는 없다. 대신 사용자 정의 타입을 만들어야 합니다.
// 안되는 예
type myInterface interface {
methodWithoutParameters()
methodWithParameter(float64)
methodWithReturnValue() string
}
func methodWithoutParameters() {
fmt.Println("mwithoutparmas")
}
var value myInterface = myType(4)
// 위처럼 있다해도 interface가 요구하는 메서드를 구현한 타입이 지정이 안되어 있으므로 interface에 값을 못담음
// 올바른 예
type myType int
type myInterface interface {
methodWithoutParameters()
methodWithParameter(float64)
methodWithReturnValue() string
}
func (m myType) methodWithoutParameters() {
fmt.Println("mwithoutparmas")
}
// interface가 요구하는 메서드의 구현 타입이 myType로 종속되어있기에 가능
var value myInterface = myType(4)
java처럼 interface를 implements하는 명시적 선언은 필요하지 않다. Go에서는 자동으로 처리되기 때문에 어떤 타입이 특정 인터페이스에 선언된 모든 메서드를 구현하고 있으면 추가로 선언하지 않아도 해당 인터페이스가 필요한 모든 곳에서 사용할 수 있다.
따라서, 인터페이스의 모든 메서드들을 구현한 타입은 이제 해당 인터페이스 타입을 가진 변수에 할당될 수 있다. 그래서 value 변수에 myType 타입의 값이 할당될 수 있는 것이다.
type myInterface interface {
methodWithoutParameters()
methodWithParameter(float64)
methodWithReturnValue() string
}
type myType int
type testtype string
func (m myType) methodWithoutParameters() {
fmt.Println("mwithoutparmas")
}
func (m myType) methodWithParameter(arg float64) {
fmt.Println("mwparmas", arg)
}
func (t testtype) methodWithReturnValue() string {
fmt.Println("mwrvalue")
return "end"
}
func Interfacetest2() {
// myinterface에 값을 ekadk wnftneh dlTdma
var value myInterface = myType(5)
하지만 위와 같이 인터페이스의 구성 메서드임에도 불구하고 methodWithReturnValue만 다른 type을 이용하고 있으면 오류가 발생한다.
또한 인터페이스의 메서드를 직접 호출할 순 없다.
package interfacetest
import "fmt"
type myInterface2 interface {
methodWithoutParameters()
methodWithParameter(float64)
methodWithReturnValue() string
}
func methodWithoutParameters2() {
fmt.Println("mwithoutparmas")
}
func methodWithParameter2(arg float64) {
fmt.Println("mwparmas", arg)
}
func Interfacetest3() {
myInterface2.methodWithParameter2(2.3)
}
// 위와 같이 직접호출은 불가능
인터페이스의 메서드는 인터페이스를 구현하는 타입의 인스턴스를 통해 호출해야 한다.
package interfacetest
import "fmt"
// 인터페이스 정의
type myInterface2 interface {
methodWithoutParameters()
methodWithParameter(float64)
methodWithReturnValue() string
}
// 인터페이스를 구현하는 구조체 정의
type MyType struct{}
func (m MyType) methodWithoutParameters() {
fmt.Println("methodWithoutParameters called")
}
func (m MyType) methodWithParameter(arg float64) {
fmt.Println("methodWithParameter called with", arg)
}
func (m MyType) methodWithReturnValue() string {
fmt.Println("methodWithReturnValue called")
return "Done"
}
// 인터페이스 테스트 함수
func Interfacetest3() {
var myVar myInterface2 = MyType{} // MyType의 인스턴스를 인터페이스에 할당
myVar.methodWithoutParameters() // methodWithoutParameters called
myVar.methodWithParameter(2.3) // methodWithParameter called with 2.3
ret := myVar.methodWithReturnValue() // methodWithReturnValue called
fmt.Println(ret) // 출력: Done
}
또한 java에서는 interface의 구현제가 되면 모든 메서드들을 구현했어야 했는데, go에서는 특정 메서드만 구현할 수 있다.
다만 이런 경우에는 인터페이스 타입과 연동이 안된다. 즉 위의 예제에서 value에 myType 타입을 가진 변수가 들어가지 못하게 된다.
인터페이스 타입을 가진 변수는 인터페이스를 만족하는 모든 타입의 값을 가질 수 있다. 때문에 아래 예제에서도 playlist함수에서 첫 번째 인자값을 interface로 받도록 선언했어도 TapePlayer라는 구조체 타입을 인자로 줬지만, 인터페이스의 조건을 만족하기 때문에 인자로 전달할 수 있게 된다.
하지만 해당 값이 가지고 있는 메서드를 호출하는 것은 불가능하다. 무조건 인터페이스에 정의된 메서드만 호출이 가능하다.
package interfacetest
import (
"fmt"
)
type Whistle string
func (w Whistle) MakeSound() {
fmt.Println("Tweet")
}
type Horn string
func (h Horn) MakeSound() {
fmt.Println("Horn")
}
type NoiseMaker interface {
MakeSound()
}
func play(n NoiseMaker) {
n.MakeSound()
}
type Robot string
func (r Robot) MakeSound() {
fmt.Println("beepbo")
}
func (r Robot) Walk() {
fmt.Println("powering legs")
}
func Interfacetest4() {
play(Whistle(""))
play(Horn(""))
var robotTest NoiseMaker = Robot("")
robotTest.MakeSound()
// 아래는 interface에 정의가 되어 있지 않아 호출이 불가능해진다
robotTest.Walk()
}
이를 바탕으로 처음의 예제를 수정해보자면 아래와 같다.
package interfacefinal
import "fmt"
type TapeInerface interface {
Play(string)
Stop()
}
type TapePlayer struct {
Batteries string
}
func (t TapePlayer) Play(song string) {
fmt.Println("playing : ", song)
}
func (t TapePlayer) Stop() {
fmt.Println("Stop!!")
}
type TapeRecorder struct {
Microphones int
}
func (t TapeRecorder) Play(song string) {
fmt.Println("Recording : ", song)
}
func (t TapeRecorder) Stop() {
fmt.Println("Stop!!")
}
func playlist(device TapeInerface, songs []string) {
for _, arg := range songs {
fmt.Println(arg)
}
device.Stop()
}
func Interfacefinal() {
player := TapePlayer{}
record := TapeRecorder{}
mixtape := []string{"first", "second", "third"}
playlist(player, mixtape)
playlist(record, mixtape)
}
tapeRecorder struct를 쓰는 playlist라는 함수를 새로 만들 필요 없이 함수에다가 interface를 받게해서 지정된 인터페이스의 메서드를 실행할 수 있게 변경되었다.
위 예제에서의 코드에서 TapeRecorder에서만 존재하는 메서드가 있다고 할 때, 해당 고유 메서드를 호출하고 싶다.
그러면 interface로 선언된 device를 다시 TapeRecorder 타입으로 변환한 다음, 호출 하면 될까?
func playlist(device TapeInerface, songs []string) {
for _, arg := range songs {
fmt.Println(arg)
}
// 여기가 에러
recorder := TapeRecorder(device)
recorder.Record()
device.Stop()
}
하지만 이러한 코드는 에러를 발생시킨다. 타입 변환은 인터페이스 타입에는 사용할 수 없기 때문이다
이런 상황에서 문제를 해결하는 것이 타입 단언이다.
구체 타입의 값이 인터페이스 타입의 변수에 할당되었을 대 타입 단언을 사용하면 구체 타입의 값을 가져올 수 있다.
var noiseMarker NoiseMaker = Robot("")
var robot Robot = noiseMaker.(Robot)
다음과 같이 인터페이스.(구현체타입) 으로 사용한다.
인터페이스에서 특정 메서드를 사용하는 방법과 비슷하다. 이 방법으로 다시 실행하면
func playlist(device TapeInerface, songs []string) {
for _, arg := range songs {
fmt.Println(arg)
}
recorder := device.(TapeRecorder)
recorder.Recording()
device.Stop()
}
panic: interface conversion: interfacefinal.TapeInerface is interfacefinal.TapePlayer, not interfacefinal.TapeRecorder
에러가 발생한다.
이와 같은 에러는 TapeInterface에는 TapeRecorder 뿐 아니라 TapePlayer 도 들어가기 때문이다. TapePlayer 구현체가 들어왔는데, 이를 TapeRecorder로 변환이 안되기 대문에 panic이 발생하게 되는 것이다.
panic은 컴파일 도중이 아닌 런타임 중에 발생한다.
이러한 타입 단언 실패를 찾아내기 위해서는 타입단언( interface.(struct) ) 두 번째 반환값인 성공 여부를 확인하면 된다. bool 타입으로 반환된다.
따라서 아래와 같이 에러 핸들링으로 오류를 방지할 수 있다
func playlist(device TapeInerface, songs []string) {
for _, arg := range songs {
fmt.Println(arg)
}
recorder, ok := device.(TapeRecorder)
if ok {
recorder.Recording()
}
device.Stop()
}