관리 메뉴

A seeker after truth

Golang기본(4) - 구조체, 메서드, 인터페이스 본문

Programming Language/Golang(Go)

Golang기본(4) - 구조체, 메서드, 인터페이스

dr.meteor 2019. 10. 12. 00:02

*본문은 <예제로 공부한 go 프로그래밍>(http://golang.site/go/basics) 사이트를 참고하여 공부하면서 정리한 내용입니다. Tip 위주로 간략하게 정리했으며, 보탬 내용은 <Go 디스커버리>(염재현 저, 한빛미디어, 2016)을 참고하여 작성되었습니다.

 

15. 구조체 struct

go의 구조체는 custom data type을 표현하며, 필드 데이터만 가진다!!! 메서드 없다! go는 oop를 고유의 방식으로 지원한다. 무슨 말이냐, go에는 전통적인 oop 언어가 갖는 클래스, 객체, 상속 개념이 없다. 전통적인 oop의 클래스는 go 언어에서 custom 타입을 정의하는 struct로 대응된다. 전통적인 oop 클래스가 필드와 메서드를 함께 갖는 것과 달리 고의 구조체는 필드만 가지며, 메서드는 별도로 분리하여 정의한다.

type person struct {

  name string

  age int

}

 

func main() {

  p := person()

  p.name = "Lee"

  p.age = 10

  fmt.Println(p)//{Lee 10}라고 출력된다.

}

 

/* 위 방법은 빈 person객체를 할당한 뒤 나중에 필드값을 채워넣는 방법이다.

하지만 객체 생성 시 초기값을 한번에 할당하는 방법도 있다. */

var p1 person

p1 = person{"Bob", 20}//필드값을 순서대로 괄호 안에 넣는 방법

p2 := person{name: "sean", age: 50}

순서에 상관없이 필드명 지정하고 값 넣는 경우. 특히 이 경우, 일부 필드가 생략되면 자동으로 zero value(정수면 0, 실수면 0.0, string이면 "", 포인터면 nil)를 갖는다.

 

또 다른 객체 생성 방법으로 go 내장함수 new()의 사용이 있다. 이러면 모든 필드를 zero value로 초기화하고 person 객체의 포인터(*person)를 리턴한다.

p := new(person)

p.name = "Lee"

struct의 필드값이 변하면, 별도로 새 개체를 만들지 않고 해당 개체 메모리에서 직접 변경된다. 하지만 struct 자체, 즉 객체를 다른 함수의 파라미터로 넘긴다면, pass by value에 따라 객체를 복사해서 전달하게 된다. 그래서 만약 pass by reference로 struct를 전달하고자 하면 "struct의 포인터"를 전달해야 한다!!!!!

 

 

//생성자 함수

때로 구조체 필드가 사용 전에 초기화되어야 하는 경우가 있다. 예를 들어, 구조체 필드가 맵 타입인 경우 이를 사전에 미리 초기화해 두면, 외부 구조체 사용자가 매번 map을 초기화 해야 한다는 것을 기억할 필요가 없다. 이런 목적을 위해 생성자 함수를 사용할 수 있다. 생성자 함수는 구조체를 리턴하는 함수로서 그 함수 본문에서 필요한 필드를 초기화한다.

아래 예제에서 생성자 함수 newDict()는 dict라는 struct의 map필드를 초기화한 후 struct 포인터를 리턴하고 있다. 이어 main()함수에서 ""구조체 객체를 만들 때 dict를 직접 생성하지 않고 대신 newDict()함수를 호출하여 이미 초기화된 data맵 필드를 사용하고 있다.""

type dict struct {

  data map[int]string

}

func newDict() *dict {

  d := dict{}

  d.data = map[int]string{}

  return &d

}

func main() {

  dic := newDict()

  dic.data[1] = "A"

}

 

 

 

16. method 메서드

함수 정의와 비슷한 양식을 사용하는데, func 키워드와 함수명 사이에 그 함수가 어떤 struct를 위한 메서드인지를 표시한다. 흔히 receiver라 불리는 이 부분은 메서드가 속한 struct 타입과 변수명을 지정하는데 이것의 변수명은 함수 내에서 마치 입력 파라미터처럼 사용된다.

고의 메서드는 func 키워드와 함수명 사이에 '그 함수가 어떤 struct를 위한 메서드인지'를 표시하게 된다. 이 부분을 receiver라고 한다. 그러면 아래 코드와 같이 사용할 수 있다.

type Rect struct {

  width, height int

}

 

func (r Rect) area() int {

  return r.width * r.height

}

 

func main() {

  rect := Rect{10,20}

  area := rect.area() //이제부터 이렇게 쓸 수 있단 사실 기억하기! 메서드 호출!!

  println(area)

}

/*위 코드는 value receiver를 표현한 것으로서, 이를 포인터 receiver로 변경하면 다음과 같다. 둘의 차이는 필드값을 진짜로 바꿀 수 있느냐 없느냐 차이. 알지?*/

func (r *Rect) area2() int {

  r.width++ //이러면 필드값 변경 반영된다!

  //이하 동문

} //이하 동문



 

17. 인터페이스

⭐️구조체가 필드들의 집합체라면, interface는 메서드들의 집합체이다.⭐

interface는 타입이 구현해야 하는 메서드 원형(prototype)들을 정의한다. 하나의 사용자 정의 type이 interface를 구현하기 위해선 단순히 그 interface를 갖는 '모든' 메서드들을 구현하면 된다. 또한 구조체와 마찬가지로 type문을 사용하여 정의한다.

type Shape interface {

  area() float64

  perimeter() float64

}

//위 인터페이스를 Rect, Circle 2가지 케이스(타입)로 구현한 예는 아래와 같다.

type Rect struct {

  width, height float64

}

func (r Rect) area() float64 { return r.width * r.height }

func (r Rect) perimeter() float64 {

  return 2*(r.width + r.height)

}

 

type Circle struct {

  radius float64

}

func (c Circle) area() float64 {

  return math.Pi * c.radius * c.radius

}

func (c Circle) perimeter() float64 {

  return 2 * math.Pi * c.radius

}

 

함수 파라미터가 interface인 경우, '어떤 타입이든' 해당 인터페이스를 구현하기만 하면 모두 입력 파라미터로 사용될 수 있다는 것을 의미한다. 이를테면 아래 showArea 함수의 경우 Shape 인터페이스를 구현한 타입 객체라면 모두 받을 수 있다! showArea함수 내에서 해당 인터페이스가 가진 메서드(여기선 area, perimeter)를 사용할 수 있다.

func showArea(shapes ...Shape) {

  for _, s := range shapes {

    a := s.area()

    println(a)

  }

}

 

//인터페이스 타입

고 플밍을 하다 보면 흔히 빈 인터페이스를 자주 접하게 되는데, 이를 인터페이스 타입이라 한다. 이를테면 여러 표준 패키지들의 함수 prototype을 살펴보면, 아래와 같이 빈 인터페이스가 자주 등장한다.

func Marshal (v interface{}) ([]byte, error);

func Println (a ...interface{}) (n int, err error);

/*empty in~는 메서드를 전혀 갖지 않는 빈 인터페이스로서, go의 모든 타입은 적어도 0개의 메서드를 구현하므로, 흔히 go에서 모든 타입을 나타내기 위해 빈 인터페이스를 사용한다. 즉 빈 인터페이스는 어떠한 타입도 담을 수 있는 컨테이너라고 볼 수 있고, 여러 다른 언어에서 흔히 일컫는 '동적 타입'과 같다.*/

func main() {

  var x interface{}

  x = 1

  x = "Tom"

  printIt(x) //Tom

}

func printIt(v interface{}) {

  fmt.Println(v) // Tom

}

 

//Type Assertion

/*interface type의 x와 타입 T에 대하여 x.(T)로 표현했을때, 이는 x가 nil이 아니며 x는 T타입에 속한다는 것을 확인(assert)하는 것이다. 둘중 하나라도 해당되지 않으면 런타임 에러가 발생한다. x가 T 타입인 경우는 T 타입의 x를 리턴한다. 즉, 아래 예제에서 변수 j는 a.(int)로부터 int형 변수 j가 된다.*/

var a interface {} = 1

i := a // a와 i는 동적 타입, 값은 1

j := a.(int) // j는 int타입, 값은 1

println(i) // 포인터주소 출력