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 |