golang - 구조체

아래 글은 golang을 공부할 목적으로 웹에서 본 글들을 정리한 것이다.

구조체 만들기

package main

import (
    "log"
)

type Money struct {
    amount   uint
    currency string
}

func main() {
    money := &Money{120, "yen"} // 포인터로 생성
    log.Printf("%#v", money)
}

실행 결과

2013/12/10 17:28:49 &main.Money{amount:0x78, currency:"yen"}

메소드 정의

package main

import (
    "fmt"
    "log"
)

type Money struct {
    amount   uint
    currency string
}

func (this *Money) Format() string {
    return fmt.Sprintf("%d %s", this.amount, this.currency)
}

func main() {
    money := &Money{120, "yen"}
    log.Printf("%#v", money)
    log.Print(money.Format())
}

실행 결과

2013/12/10 17:30:41 &main.Money{amount:0x78, currency:"yen"}
2013/12/10 17:30:41 120 yen
package main

import (
    "fmt"
    "log"
)

type Money struct {
    amount   uint
    currency string
}

func (this *Money) Format() string {
    return fmt.Sprintf("%d %s", this.amount, this.currency)
}

func (this *Money) Add(that *Money) {
    this.amount += that.amount
}

func main() {
    money := &Money{120, "yen"}
    log.Print(money.Format())
    money.Add(&Money{180, "yen"})
    log.Print(money.Format())
}

실행 결과

2013/12/10 17:34:41 120 yen
2013/12/10 17:34:41 300 yen

생성자 만들기

C++, Java와 같이 생성자가 없기 때문에 팩토리 함수를 정의한다.

package main

import (
    "fmt"
    "log"
)

type Money struct {
    amount   uint
    currency string
}

func NewMoney(amount uint) *Money {
    return &Money{amount, "yen"}
}

func (this *Money) Format() string {
    return fmt.Sprintf("%d %s", this.amount, this.currency)
}

func (this *Money) Add(that *Money) {
    this.amount += that.amount
}

func main() {
    money := NewMoney(120)
    log.Print(money.Format())
    money.Add(NewMoney(180))
    log.Print(money.Format())
}

실행 결과

2013/12/10 17:40:33 120 yen
2013/12/10 17:40:33 300 yen

구조체 내용을 보기 편하게 출력

  • go-spew
  • 표준 패키지의 fmt.Printf 함수에서도 %v로 구조체 내용을 볼 수 있지만 오브젝트가 들여 쓰기 하지 않고 1줄에 들어가 있어 내용을 확인하기 힘들다.
  • go-spew를 사용하면 좀 더 좋은 느낌으로 포맷해준다.(python의 pprint 같은 느낌).
package main

import (
  "fmt"

  "github.com/davecgh/go-spew/spew"
)

func main() {
  a := struct {
      A []string
      n int
  }{
      A: []string{"a", "b", "c"},
      n: 123,
  }

  fmt.Println("--- fmt.Printf ---")
  fmt.Printf("%#v\n", a)
  fmt.Println("--- spew.Dump ---")
  spew.Dump(a)
}

실행 결과

--- fmt.Printf ---
struct { A []string; n int }{A:[]string{"a", "b", "c"}, n:123}
--- spew.Dump ---
(struct { A []string; n int }) {
 A: ([]string) (len=3 cap=3) {
  (string) (len=1) "a",
  (string) (len=1) "b",
  (string) (len=1) "c"
 },
 n: (int) 123
}

구조체 내용 출력

함수 의미
fmt.Printf(“%v”, value) 기본 포맷을 적용한 값 &{bar baz}
fmt.Printf(“%+v”, value) 기본 포맷을 적용한 값으로 구조체의 경우 필드 이름이 표시한다 &{Bar:bar Baz:baz}
fmt.Printf(“%#v”, value) Go 언어 구문으로 표현한 값 &main.Foo{Bar:”bar”, Baz:”baz”}
fmt.Printf(“%T”, value) Go 언어 구문으로 표현한 형태 *main.Foo

참고로 fmt 뿐만 아닌 log 라는 패키지에도 Printf가 있다. 차이점은 다음:

함수 패키지 날짜
fmt.Printf import “fmt” 나오지 않는다 줄 바꾸지 않는다
log.Printf import “log” 나온다 줄 바꾼다

또 log와 fmt의 가장 큰 차이는 기본적으로 fmt가 stdout인 것에 비해 log는 stderr에 출력한다는 점이다.

사용법의 예

main.go
package main

import (
    "log"
)

type Foo struct {
    Bar string
    Baz string
}

func main() {
    foo := &Foo{"bar", "baz"}
    log.Printf("%v", foo)
    log.Printf("%+v", foo)
    log.Printf("%#v", foo)
    log.Printf("%T", foo)
}

출력 결과

2013/12/11 21:52:10 &{bar baz}
2013/12/11 21:52:10 &{Bar:bar Baz:baz}
2013/12/11 21:52:10 &main.Foo{Bar:"bar", Baz:"baz"}
2013/12/11 21:52:10 *main.Foo

구조체 덤프 예

package main

import (
    "fmt"
)

type Foo struct {
    Bar string
    Baz string
}

func main() {
    foo := &Foo{"bar", "baz"}
    var_dump(1, true, nil, "string", foo)
}

func var_dump(v ...interface{}) {
    for _, vv := range(v) {
        fmt.Printf("%#v\n", vv)
    }
}

출력 결과

1
true

"string"
&main.Foo{Bar:"bar", Baz:"baz"}
</pre>  

  

## spew 사용
github.com/davecgh/go-spew/spew를 사용하면 개행을 해주기 때문에 보기 편함    
  
```
main.go
package main

import (
    "github.com/davecgh/go-spew/spew"
)

type Name struct {
    FirstName string
    LastName string
}

type Address struct {
    Country string
    PostalCode string
    Address string
}

type Person struct {
    Name Name
    Address Address
}

func main() {
    person := &Person{
        Name{"Cynthia", "Stone"},
        Address{"us", "02110", "1091 Lyon Avenue, Boston"},
    }
    spew.Dump(person) //여기!
}
```  
출력 결과  
(*main.Person)(0x2102bd230)({
 Name: (main.Name) {
  FirstName: (string) "Cynthia",
  LastName: (string) "Stone"
 },
 Address: (main.Address) {
  Country: (string) "us",
  PostalCode: (string) "02110",
  Address: (string) "1091 Lyon Avenue, Boston"
 }
})
## 구조체 패턴 ### 구조체 구현 패턴 - 생성자 함수 - export에 의한 접근 허가 - 인터페이스에 의한 폴리모피즘 - 구조체에 의한 폴리모피즘 - 구조체에 의한 서브 클래스 책임 - 구조체에 의한 이양 - 함수에 의한 이양 ### 생성자 함수 Go 언어에는 구조체의 생성자가 없으며 구조체를 초기화 하려면 구조체가 속한 패키지에 생성자 함수를 정의한다. 관례에서는 함수 이름으로 New+구조체 이름()을 쓰고 있다. ``` type StructA struct { Name string } //초기화 때 하고 싶은 처리 func (self *StructA) SomeInitialize() { // Some initialize } // 생성자 함수 정의 func NewStructA(name string) *StructA { structA := &StructA{Name: name} structA.SomeInitialize() return structA } func main() { _ = NewStructA("name") } ``` 생성자 함수를 사용하면 아래의 이점이 있다. - 구조체의 생성과 동시에 할 처리를 이용 측에게 숨긴다 - 폐쇄 구조체를 이용 측에게 숨긴다(구조체의 구조를 이용 측에서 숨기다. 폐쇄 구조체의 존재를 감추는데도 사용할 수 있다. ### export에 의한 접근 허가 Go 언어에서는 패키지 외부로 접근 허가는 이름의 선두를 대문자로 한다(export). 이를 이용하는 것으로 싱글톤 같은 인스턴스 생성을 제어할 수 있다. ``` package singleton //구조체의 이름을 소문자로 패키지 외부로 내보내기를 안 한다 type singleton struct { } //인스턴스를 유지하는 변수도 소문자로 하여 외부에 노출하지 않는다 var instance *singleton // 인스턴스 취득용 함수만 내보내고 여기서 인스턴스가 하나임을 보증 func GetInstance() *singleton { if instance == nil { instance = &singleton{} } return instance } ``` 생략 서식 := 을 사용함으로써 구조체 자체를 내보내지 않아도 이용할 수 있다. singleton := singleton.GetInstance() ### 인터페이스에 의한 폴리모피즘 Go 언어에는 형의 계승 기능을 지원하지 않지만 인터페이스를 이용함으로써 폴리모피즘을 실현할 수 있다. ``` //인터페이스 정의 type SomeBehivor interface { DoSomething(arg string) string } //구조체 정의 type StructA struct { } //인터페이스 구현 func (self *StructA) DoSomething(arg string) string { return arg } //폴리모피즘 func main() { //인터페이스형 변수에 저장할 수 있다 var behivor SomeBehivor behivor = &StructA{} behivor.DoSomething("A") } ``` 구조체의 폐쇄에 의한 의사적인 계승할 수 있다고 설명하고 있는 것이 있지만 이는 어디까지나 투과적으로 구조체를 이용 할 뿐 ”타입”으로 폴리모피즘을 실현할 수는 없음을 주의한다. ### 구조체에 의한 폴리모피즘 폴리모피즘을 실현하면서 구조체에 공통의 구현 및 멤버를 정의하고 싶은 경우가 있다. 이 경우는 인터페이스를 구현한 구조체를 준비하고 그것을 심는 방법을 취한다. ``` //인터페이스 정의 type SomeBehivor interface { DoSomething() string } //공통 구조체 정의 type DefaultStruct struct { //공통 구성원 Name string } //공통 구조체에 인터페이스를 구현 func (self *DefaultStruct) DoSomething() string { return self.Name } //공통 구조체를 포함 type StructA struct { *DefaultStruct } func main() { //인터페이스형 변수에 저장할 수 있다 behivor := &StructA{&DefaultStruct{"A"}} //공통 구현과 멤버를 쓸 수 있다 behivor.DoSomething() } ``` 다만 이 경우 이용 측이 초기화 때에 폐쇄 구조체를 알 필요가 있으므로 전술한 생성 함수를 준비하면 좋다. ``` // 생성자 함수 func NewStructA(name string) *StructA { return &StructA{&DefaultStruct{"A"}} } //이용 측은 폐쇄 구조체의 존재를 의식하지 않는다 behivor := NewStructA("A") ``` ### 구조체에 의한 서브 클래스 책임 Go 언어에서는 내장된 구조체에서 구조체의 구현을 이용할 수 없다(Dispatch back 되지 않는다고 한다). 그래서 TemplateMethod 패턴처럼 자식 클래스에 처리 구현을 맡기는 기능을 만드는 경우, 내장된 구조체의 메소드에 대해 수신기로 원래의 구조체의 참조를 전달 필요가 있다. ``` type Behivor interface { DoSomethingByOther() } type Abstract struct { } // 내부에서 원래 구조체의 구현을 이용하는 메소드 func (self *Abstract) DoSomething(behivor Behivor) { behivor.DoSomethingByOther() //만일 여기서 Abstract에 Behivor를 내장 등 메소드가 있는 상태에서 컴파일을 통과하도록 해서 //self.DoSomethingByOther()이라고 해도 panic:runtime error:invalid memory address or nil pointer dereference가 //발생하고 Dispatch back 되지 않은 것을 알 수 있다 } //위에서 정의한 구조체를 포함 type Concrete struct { *Abstract } //인터페이스를 구현 func (self *Concrete) DoSomethingByOther() { // Do something } func main() { concrete := &Concrete{} //수신기가 되는 자신을 건넸다 concrete.DoSomething(concrete) } ``` 이 수신기를 스스로 지정하는 방법은 계승을 이용할 수 없는 언어에서 Client-specified self 라는 패턴 이름으로 쓰이는 것 같다. 다만 구조체 간의 의존 관계가 높아지기 때문에 아무래도 계승 관계가 필요한 경우를 제외하고, 후술의 구조체에 의한 전환, 또는 함수에 의한 이양을 검토하는 것이 좋다. ### 구조체에 의한 이양 Go 언어에서는 처리 구현을 적절한 책무를 가진 구조체에 맡기는 이양은 쉽게 실현할 수 있다. 구조체를 멤버로서 보유하고 필요에 따라 그 구조체의 메소드를 호출하도록 한다. 또 Strategy 패턴처럼 인터페이스에 대해서 이양하게 설계 해 두면 의존성을 더 낮출 수 있을 것이다. ``` type Strategy interface { DoSomething() } type ConcreteStrategy struct { } func (self *ConcreteStrategy) DoSomething() { // Do something } type StructA struct { //이양처의 구성원 유지 strategy Strategy } func (self *StructA) Operation() { // 넣은 구조체에 처리를 이양 self.strategy.DoSomething() } ``` ### 함수에 의해 이양 Go 언어에서는 일급 클래스 형태로 다루기 위해 이양하는 처리를 함수로서 전달 방법도 가능하다. Go 언어의 기본 패키지를 보면, 이양 처리를 외부에서 주입하는 기능을 제공할 경우 함수형을 넘겨주는 설계가 되어 있는 것이 많다고 생각된다. ``` // 함수형을 정의 type DoSomething func() type StructA struct { } // 함수형을 인수로 받는 메소드를 정의 func (self *StructA) Operation(strategy DoSomething) { // 받은 함수형으로 처리를 이양 strategy() } func main() { structA := &StructA{} //함수 리터럴에서 인수로서 전달 structA.Operation(func(){ // Do something }) } ``` ## 서로 같은 이름의 필드만 구조체 간에 복사 ``` type Src struct { Name string Age uint16 Address string } type Dst struct { Name string Age uint16 } ``` 예를들면 위와 같은 2개의 구조체가 있다고 하고 ``` s := &Src{"A", 12, "seoul"} d := new(Dst) copyStruct(s, d) fmt.Println(*d) // {A, 12} ``` 같은 필요한 필드만 복사하는 copyStruct() 함수를 생각하자 http://play.golang.org/p/0kPRs3vGUa ``` func copyStruct(src interface{}, dst interface{}) error { fv := reflect.ValueOf(src) ft := fv.Type() if fv.Kind() == reflect.Ptr { ft = ft.Elem() fv = fv.Elem() } tv := reflect.ValueOf(dst) if tv.Kind() != reflect.Ptr { return fmt.Errorf("[Error] non-pointer: %v", dst) } num := ft.NumField() for i := 0; i < num; i++ { field := ft.Field(i) if !field.Anonymous { name := field.Name srcField := fv.FieldByName(name) dstField := tv.Elem().FieldByName(name) if srcField.IsValid() && dstField.IsValid() { if srcField.Type() == dstField.Type() { dstField.Set(srcField) } } } } return nil } ``` ## struct {} https://code.i-harness.com/ja/q/15b71c2 struct{}{} struct{}은 데이터를 가지지 않으므로 비교를 할 수 없으므로 2개의 struct{}를 비교하기 위해서 https://dave.cheney.net/2014/03/25/the-empty-struct

이 글은 2018-12-02에 작성되었습니다.