본문 바로가기
golang

Go 상속 vs 구성

by marble25 2023. 10. 1.

golang의 객체지향은 일반적인 다른 언어들과 다르다. golang은 명시적으로 상속의 개념이 없다. 대신 composition으로 상속을 대체한다. golang에서의 상속 개념을 정리해두고자 한다.

Struct

golang은 c++과 유사한 부분이 많다. 공식적으로 클래스라는 개념이 존재하지 않고, struct로 class를 구현하는 점에서 그러하다.

type Person struct {
  name string
  age int
}

func(p Person) greeting() { // greeting 함수 Person구조체에 연결
   fmt.Println("Hello")
}
 
type Student struct {
    p Person // 학생 구조체 안에 사람 구조체를 필드로 가지고 있음( Has - a )
    school string
    grade int
}
 
func main() {
	var s Student
	s.p.name = "me"
	s.p.greeting() // Hello me
}

Student 구조체는 p라는 Person 변수를 가질 수 있다. struct 정의 내에는 변수들만 표현할 수 있고, 함수는 receiver로 연결된다.

Receiver는 value와 pointer receiver가 있는데, 객체 내부의 값을 변경하는 경우 pointer receiver, 값을 변경하지 않는 경우 value receiver를 사용한다.

type Person struct {
	name string
	age  int
}

func (p Person) setNameByValue(n string) {
	p.name = n
}

func (p *Person) setNameByPointer(n string) {
	p.name = n
}

func main() {
	var p Person
	p.name = "default"
	p.setNameByValue("t1")
	fmt.Println(p.name) // default
	p.setNameByPointer("t2")
	fmt.Println(p.name) // t2
}

위의 예제에서 보듯, struct 내부 value를 변경하는 경우가 있으면 pointer receiver를 사용하고, 값을 변경하지 않는 경우에는 value receiver를 사용하면 된다.

type Person struct { // 사람 구조체 정의
	name string
	age  int
}

func (p Person) greeting() { // greeting 함수 Person구조체에 연결
	fmt.Println("Hello", p.name)
}

type Student struct {
	Person // 임베딩( Is-a )
	school string
	grade  int
}

func main() {
	var s Student
	s.name = "me"
	s.greeting() // Hello me
}

struct는 내부에 구조체를 단순히 넣어주는 것으로 상속을 구현할 수 있다. (struct embedding) 자식 구조체에서는 부모 구조체의 모든 변수와 메소드에 접근할 수 있다.

이 방법으로 여러 구조체를 하나의 구조체에 embedding할 수 있다. 다만, 같은 이름의 변수나 같은 타입의 메소드가 존재하지 않아야 한다.

Interface

Java의 interface와 유사하게, golang에도 interface 개념이 존재한다. 하지만 명시적인 상속이 없다는 점에서 차이가 있다.

golang에서 struct가 interface를 implement하는 방식은 duck typing이다. 즉 동일한 타입의 메소드가 정의되어 있으면 해당 interface를 구현하는 것으로 판단한다는 것이다.

type Person interface {
	Greeting(string) string
}

type Student struct {
}

func (s Student) Greeting(name string) string {
	return fmt.Sprintf("Hello, %s", name)
}

func printGreeting(p Person, name string) {
	fmt.Println(p.Greeting(name))
}

func main() {
	var s Student
	printGreeting(s, "Alice") // Hello, Alice
}

위 코드에서 명시적으로 Person interface와 Student struct는 관계가 없다. 단지 Person에 정의된 method를 Student가 구현했을 뿐이다.

만약 Greeting의 구현을 빠뜨리면 다음과 같은 에러가 나게 된다.

cannot use s (variable of type Student) as Person value in argument to printGreeting: Student does not implement Person (missing method Greeting)

Student가 Greeting을 구현하지 않았기 때문에 printGreeting의 첫번째 인자 Person type으로 전달할 수 없다는 얘기다. 이때, 동일한 이름, 동일한 매개변수와 동일한 리턴 타입으로 되어 있어야 동일한 메소드로 판단한다.

interface는 Java에서와는 다르게 변수를 가질 수 없다. [관련 discussion] 그 이유는 interface는 구체적인 implementation을 포함한다는 초기의 설계를 파괴하기 때문이다. 변수와 유사하게 구현하려면 아래와 같이 getter와 setter를 이용해서 구현할 수 있다.

type myInterface interface {
	Value() string
	SetValue(string)
}

type myStruct struct {
	value string
}

func (s myStruct) Value() string {
	return s.value
}

func (s *myStruct) SetValue(v string) {
	s.value = v
}

func main() {
	var s myStruct
	s.SetValue("abc")
	fmt.Println(s.Value()) // abc
}

Empty Interface

아무 메소드도 포함하지 않는 interface를 empty interface라고 한다. Empty interface는 함수에 어떤 type의 변수도 전달할 수 있도록 하기 위해 사용된다.

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

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

	i = 42
	describe(i)

	i = "hello"
	describe(i)
}

위 코드를 보면 int type도, string type도 describe 함수로 전달할 수 있다. 참고로, interface{} 는 any로 대체해서 사용할 수 있다.

Type 변환

기본적으로는 type assertion을 통해 타입을 다시 변환해서 사용할 수 있다. Interface를 또 다른 interface나 struct type으로 변환할 수 있다. 이때, 두 번째 리턴값은 assertion이 성공하는지를 의미한다.

func typeAssert(i interface{}) {
	v, ok := i.(string)
	if !ok {
		fmt.Println("assertion failed")
	} else {
		fmt.Println("assertion completed,", v)
	}
}

func main() {
	typeAssert("real_string") // assertion completed, real_string
	typeAssert(123) // assertion failed
}

Empty interface로 넘어온 type을 type switch를 통해 다시 타입 정보를 알아낼 수 있다.

func typeSwitch(i interface{}) {
	switch v := i.(type) {
	case nil:
		fmt.Println("x is nil")
	case int:
		fmt.Println("x is", v)
	case bool, string:
		fmt.Println("x is bool or string")
	default:
		fmt.Printf("type unknown %T\\n", v)
	}
}

func main() {
	typeSwitch("value") // x is bool or string
	typeSwitch(123) // x is 123
}

또는 reflection을 이용해서 타입 정보를 다시 복구할 수 있다.

func typeRecover(i any) {
	fmt.Println(reflect.TypeOf(i))
}

func main() {
	var a string
	a = "abc"
	typeRecover(a) // string

	var i int
	i = 123
	typeRecover(i) // int
}

'golang' 카테고리의 다른 글

Go convention 정리  (0) 2023.10.03
golang에서 for loop scoping  (1) 2023.10.02
Go context 파헤치기  (0) 2023.09.29
goroutine 파헤치기  (0) 2023.09.23
Formatting in golang  (0) 2023.09.03