Go言語 – 5.構造体

2019-01-29Go言語

Go言語 構造体

今回は「構造体 (structure) 」について説明します。
Go言語の構造体は、基本的にはC言語の構造体と同じですが、このほかにも「メソッド (method) 」の定義や構造体の「埋め込み」といった機能があります。
Go言語の場合、クラスや継承といったオブジェクト指向機能はサポートされていません。
メソッドや埋め込み、インターフェースを使うと、Go 言語でもオブジェクト指向プログラミングが可能になります。

構造体とは

Go言語のデータ構造をざっくり分類すると、
「基本型」「配列型」「関数型」「参照型」「ポインタ型」
といったものに分けられます。
「構造体」は、こうした多様なデータ構造を1つの「型」として取り扱うことができるものです。
=> 「複数の型の値を1つにまとめられるもの」
ということです。
Go言語で何かを作ろうとしたとき、
この構造体と関数を色々と使って、開発することが多くなるのではないかと思います。

構造体の定義

構造体は下記のように type と struct を使用して定義します。

type 型名 struct {
    フィールド名1 型1
      .....
    フィールド名n 型n
}

<例>

package main

import "fmt"

type Person struct {
name string 
age int
}

構造体の初期化

構造体の初期化方法は複数存在します。

  • 変数定義後にフィールドを設定する方法
  • {} で順番にフィールドの値を渡す方法
  • フィールド名を  で指定する方法
  • コンストラクタ関数を使用した方法

①変数定義後にフィールドを設定する方法

type Person struct {
   name string 
   age int
}

func main(){
  var hoge Person
  mike.name = "Mike"
  mike.age = 20
  fmt.Println(hoge.name, hoge.age) //=> Mike 20
}

②{}で順番にフィールドの値を渡す方法

type Person struct {
name string 
age int
}

func main(){
bob := Person{"Bob", 30}
fmt.Println(bob.name, bob.age) //=>Bob 30
}

③フィールド名をで指定する方法

type Person struct {
name string 
age int
}

func main(){
sam := Person{age: 15, name: "Sam"}
fmt.Println(sam.name, sam.age) //=>Sam 15
}

④コンストラクタ関数を使用した方法

type Person struct {
name string 
age int
}

func newPerson(name string, age int) *Person{
person := new(Person)
person.name = name
person.age = age
return person
}

func main(){
var jen *Person = newPerson("Jennifer", 40)
fmt.Println(jen.name, jen.age) //=>Jennifer 40
}

構造体の実装パターン

  • エクスポートによるアクセス許可
  • インターフェースによるポリモフィズム
  • 構造体によるポリモフィズム
  • 構造体によるサブクラス・レスポンシビリティ
  • 構造体による移譲
  • 関数による移譲

①エクスポートによるアクセス許可

Go言語ではパッケージ外へのアクセス許可は、名前の先頭を大文字にすることで行います(エクスポート)。
これを利用することでシングルトンのようなインスタンス生成の制御を行うことが可能です。

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()
}

ただし、この場合、利用側が初期化時に埋込構造体を意識する必要があるため、
前述のコンストラクタ関数を用意するとよいでしょう。

④構造体によるサブクラス・レスポンシビリティ

そのため、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
    })
}

 

株式会社システムトラスト

人材募集中です。

システムトラストでは、システムエンジニア、プログラマーなどを随時募集中です。気軽にご相談ください。

お問合せ