新しいことにはウェルカム

技術 | 電子工作 | ガジェット | ゲーム のメモ書き

Go言語でハマったことメモ(クラス・継承)

Golangを始めました。

始めてみて感じたのですが、Golangはできるだけ文法をシンプルにかつ、できるだけコードがシンプルになるように設計されています。

そしてそのために、Golangには若干トリッキーな構文がいくつかあります。

しかし、それらを知らずに、他の言語での先入観や勝手な思い込みで判断してしまって、ハマることがちょいちょいありました。

ここでは、Golangを始めてみて、個人的にハマったことや、勘違いしたことを、トピック別に備忘録としてメモしていこうと思います。

今回は、Golangにおけるクラスと継承についてのメモです。

Golangにはクラスがない

Golangにはクラスがないと言われますが、実際そうです。

しかし、クラスに近しいものはあります。

そもそも、クラスで主にやりたいことは、だいたい下記のようなことじゃないでしょうか。

  • 複数の変数を1つのグループにまとめる(メンバ変数)
  • まとめたものに紐づいて、それらを操作できる関数を定義する(メンバ関数)

Golangでは、複数の変数を1つのグループにまとめるのに「構造体」を使い、そのまとめた物を操作する関数は、構造体に紐づく特殊な関数を定義できるようにして、上記のやりたかったことを実現しています。

例えば、メンバ変数「x」「y」と、それらの合計値を取得するメンバ関数「test()」を持つクラスもどきは下記のようにして作れます。

package main

import "fmt"

// 変数を構造体でまとめる
type myStruct struct {
    x int
    y int
}

// 構造体を操作する関数 構造体の「.」演算子で呼び出す
func (p *myStruct) test() int {
    return p.x + p.y
}

func main() {
    var a myStruct
    a.x = 1
    a.y = 2
    fmt.Println(a.test())  // 3
}

「test()」関数がちょっと見慣れない書き方ですが、この書き方の関数が、構造体の「.」演算子から呼び出されるメンバ関数になります。

構造体の要素は、外から誰でもアクセスできるので、メンバ関数を定義しなくても、構造体を引数に取って、その構造体を操作する関数を書いても同じようなことは実現できます。

package main

import "fmt"

type myStruct struct {
    x int
    y int
}

// 構造体を引数に取って、構造体を操作する
func test(p *myStruct) int {
    return p.x + p.y
}

func main() {
    var a myStruct
    a.x = 1
    a.y = 2
    fmt.Println(test(&a))  // 3
}

ただ、構造体でも、クラスのように「.」演算子でそれらの操作関数が書けるように、「.」演算子で呼び出せる特殊な関数の書き方が用意されています。

こう書けると、ぱっと見クラスのメンバ関数のように見えますね。

実際は、関数に構造体(または構造体のポインター)を渡して操作しているのですが、構造体のメンバ関数のように書けるようにするのが、Golangにおけるクラスなのです。

継承

前述の通り、Golangにはクラスはなく、「構造体+構造体操作関数=クラスもどき」になります。

既に定義済みのクラスもどきに、後から変数や操作関数を追加して拡張したい時があります。いわゆるクラスにおける継承ってやつですね。

Golangでもそれらの継承的なことは行なえます。

子構造体の要素として、親構造体を、要素名無しで親構造体の名前だけで登録すると、親構造体名の要素名で、親構造体にアクセスできます。

文章で書くと分かりにくいので、コードにすると下記のような感じになります。

package main

import "fmt"

// 親構造体
type parentStruct struct {
    x int
    y int
}

// 親構造体の関数
func (p *parentStruct) testParent() {
    fmt.Println("Parent", p.x, p.y)
}

// 子構造体
type childStruct struct {
    z int
    parentStruct  // 親構造体
}

// 子構造体の関数
func (p *childStruct) testChild() {
    // 親構造体の変数にアクセス
    fmt.Println("Child", p.parentStruct.x, p.parentStruct.y, p.z)  
}

func main() {
    var a childStruct
    a.parentStruct.x = 1  // 親の変数にアクセス
    a.parentStruct.y = 2  // 親の変数にアクセス
    a.z = 3  // 子の変数にアクセス
    a.parentStruct.testParent() // Parent 1 2  親の関数にアクセス
    a.testChild()               // Child 1 2 3  子の関数にアクセス
}

継承もどき

前述のコードを見ると、子構造体は、親構造体の継承というより、親構造体の要素を持っているだけなので、まぁそうなって当然だよねっといった感じです。

しかし、ここからがキモです。

上記の構造体の書き方をした場合、子構造体の直下のメンバ変数・関数の書き方で、親構造体のメンバ変数・関数を呼び出すことができるようになります。

つまり、前述のコードは、親構造体名を省略して、下記のように書くこともできます。

package main

import "fmt"

// 親構造体
type parentStruct struct {
    x int
    y int
}

// 親構造体の関数
func (p *parentStruct) testParent() {
    fmt.Println("Parent", p.x, p.y)
}

// 子構造体
type childStruct struct {
    z int
    parentStruct
}

// 子構造体の関数
func (p *childStruct) testChild() {
    // 親構造体の変数にアクセス
    fmt.Println("Child", p.x, p.y, p.z)  // 親構造体の修飾を省略できる
}

func main() {
    var a childStruct
    a.x = 1  // 親の変数にアクセス 親構造体の修飾を省略できる
    a.y = 2  // 親の変数にアクセス 親構造体の修飾を省略できる
    a.z = 3
    a.testParent() // Parent 1 2  親の関数にアクセス 親構造体の修飾を省略できる
    a.testChild()  // Child 1 2 3
}

こう書けると、ぱっと見継承のように見えますね。

実際は、親構造体を要素に持っているだけで継承ではないのですが、継承されたように書けるようにするのが、Golangにおける継承なのです。

要素と関数の名前が被ったら?

子構造体で、親構造体と同じ名前の要素や関数を定義するとどうなるのでしょうか?

その場合は、子構造体の要素・関数が使われます。親構造体の要素・関数へのアクセスは、仕方ないので大人しく最初に記載したように、親構造体名を指定して行います。

package main

import "fmt"

type parentStruct struct {
    x int
    y int
}

func (p *parentStruct) test() {
    fmt.Println("Parent", p.x, p.y)
}

type childStruct struct {
    x int
    parentStruct
}

func (p *childStruct) test() {
    // xは重複するので、親構造体のxにアクセスするには、親構造体を指定する
    fmt.Println("Child", p.parentStruct.x, p.y, p.x) 
}

func main() {
    var a childStruct
     // xは重複するので、親構造体のxにアクセスするには、親構造体を指定する
    a.parentStruct.x = 1
    a.y = 2
    a.x = 3
    // test()は重複するので、親構造体のtest()にアクセスするには、親構造体を指定する
    a.parentStruct.test()  // Parent 1 2  
    a.test()              // Child 1 2 3
}

つまり、「子構造体の直下のメンバ変数・関数の書き方で、親構造体のメンバ変数・関数を呼び出すことができる」のは、コンパイラがコードを見て良しなに親構造体名を裏で補完してくれているからです。

継承の使い道

既にオブジェクトとして作成された構造体に、後から要素や関数を追加したい時があります。そういった時に継承を使うといい感じにできます。

例えば、http.Clinet構造体を拡張して、Google検索をしてその結果を保持するように、構造体を拡張したいとします。

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
)

// 新しい機能を持った構造体を継承する
type googleSearch struct {
    result string  // 結果
    *http.Client   // 継承元の構造体のポインター
}

// search()関数を追加 検索結果は result に保存
func (p *googleSearch) search(q string) {
    resp, err := p.Get(fmt.Sprintf("https://www.google.com?q=%s", q))
    if err != nil {
        return
    }
    defer resp.Body.Close()

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return
    }
    p.result = string(body)
}

func main() {
    c := &http.Client{}
    g := googleSearch{Client: c} // http.Clientを継承して、新しい要素・関数を追加する
    g.search("test")
    fmt.Println(g.result)
}

構造体をポインターで取得するケースは多く、そういった場合は、親構造体をポインターで受けるようにしておくのがポイントですね。

拡張した構造体を作成する際に、親構造体のポインターに、取得済みのポインターを代入します。

すると、子構造体から直接、親構造体の要素・関数にアクセスできるようになります。

感想など

構造体とその操作関数の定義が別々の所で独立してされるので、クラスと比べると、その構造体にはどんな操作関数があるのか見つけにくいですね。

実際、構造体は構造体、関数は関数で独立していて、コンパイラがコーディング上で紐付けているだけで、構造体は自分のメンバ関数が何かについては関与せずっといった感じです。

継承は継承ではなく、単に親構造体を要素に持つ構造体です。このあたりを勘違いしてハマりました。親構造体名を省略する方法を先に知ってしまうとハマりやすいです。

Golangの文法はプリミティブなのですが、色々な書き方、省略の仕方をできるようにして、高機能な言語と同等な書き方ができるようになっています。

う~ん。Golangは合理的でなかなか面白い言語です。

関連カテゴリー(Go言語)記事

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com