4.Structオリエンテッド - iruma-tea/go_programming GitHub Wiki

4. Structオリエンテッド

Goにはクラスや継承といったオブジェクト指向(Obuject Oriented)プログラムのための機能がない。
しかし、データ型にメソッドを追加したり、構造体の中に構造体を持たせて擬似的な継承をしたりすることができる。

4.1 メソッドを作成しよう

Goでは、構造体を含むデータ型に紐づいた関数のことをメソッドという。

4.1.1 型に紐づくメソッドを作成しよう

構造体にメソッドを作成して呼び出してみよう。まず、int型のXとYを持つ構造体Vertexを宣言する。
例えば、引数がVertexの変数v、戻り値がint型のArea関数を作る。

type Vertex struct {
	X, Y int
}

func Area(v Vertex) int {
	return v.X * v.Y
}

func main() {
	v := Vertex{3, 4}
	fmt.Println(Area(v))
}
// 実行結果
// 12

Area関数を構造体Vertexに結びついたメソッドとして定義する。
メソッド作成時には「func (v Vertex) Area() int」のようにfuncの後に()をつけ、その中にレシーバーと呼ばれる、引数の名前と型を宣言する。
これにより、Vertex型のレシーバーを持ち、返り値がint型のAreaメソッドが作成される。レシーバーが型とメソッドを紐づける役割を持つ。
メソッドを呼び出すには「v.Area()」のように、メソッドを結びつけた構造体の変数とメソッド名を.(ドット)でつないで実行する。

type Vertex struct {
	X, Y int
}

func (v Vertex) Area() int {
	return v.X * v.Y
}

func Area(v Vertex) int {
	return v.X * v.Y
}

func main() {
	v := Vertex{3, 4}
	fmt.Println(Area(v))
	fmt.Println(v.Area())
}
// 実行結果
// 12
// 12

メソッドを使う利点

関数として作成した場合、Areaという関数を把握していないと関数が使えない。一方、メソッドとして作成すると、
「v.Area()」のように、構造体とメソッドの紐づけがコードから分かりやすくなる。
コード上で変数vを書くさいにVScodeなどのエディタやIDEの機能でメソッド名が補完されて表示される。

4.1.2 ポインタレシーバーと値レシーバー

メソッドで紐づけられた構造体の値を書き換えたい場合は、メソッド作成時にレシーバーに*をつける
ここでは、ScaleというVertexのメソッドを「func (v *Vertex) Scale()」のように書いて作成し、
XとYの値を書き換えてみまよう。iのintという引数を取り、メソッドの中でXとYの値をそれぞれ、「v.X = v.X * i」「v.Y = v.Y * i」に書き換える。

type Vertex struct {
	X, Y int
}

func (v Vertex) Area() int {
	return v.X * v.Y
}

func (v *Vertex) Scale(i int) {
	v.X = v.X * i
	v.Y = v.Y * i
}

func Area(v Vertex) int {
	return v.X * v.Y
}

func main() {
	v := Vertex{3, 4}
	v.Scale(10)
	fmt.Println(v.Area())
}
// 実行結果
// 1200
  • ポインタレシーバー
    • レシーバーにポインタの*を書くことで、上記の例のようにVertexに紐づけられたScaleというメソッドの中で、構造体の中身を書き換えることできる。
  • 値レシーバー
    • レシーバーに*を付けない場合は、構造体の参照ではなく値をコピーして渡していること

4.1.3 Newで初期化時の処理を実行しよう

GoではNewという関数を作成して、他の言語のコンストラクタを実現する。

  1. VertexのXとYを小文字に置き換える。小文字すると、他のパッケージからは操作することはできない、このパッケージ内からの書き換えできるようになる
  2. New関数の作成。引数にint型のx,y、戻り値の型に*Vertex(Vertexのポインタ)を設定し、処理にはreturn &Vertex{x, y}とする。
type Vertex struct {
	x, y int
}

func New(x, y int) *Vertex {
	return &Vertex{x, y}
}

func (v *Vertex) Scale(i int) {
	v.X = v.X * i
	v.Y = v.Y * i
}

func (v Vertex) Area() int {
	return v.X * v.Y
}

func main() {
	v := New(3, 4)
	v.Scale(10)
	fmt.Println(v.Area())
}
// 実行結果
// 1200

Newのパターン

他のパッケージから、インポートしたパッケージを呼び出すときにNewを使って呼び出すことが多い。これはGoのパターンなので覚えておくと吉。

4.1.4 構造体以外の型メソッド

typeを使うと、組み込み型に新しい名前を付けた独自の型を作ることが可能。
独自の型にメソッドを持たせることができる。

type MyInt int

func (i MyInt) Double() int {
	return int(i * 2)
}

func main() {
	myInt := MyInt(10)
	fmt.Println(myInt.Double())
}

// 実行結果
// 20

4.2 構造体を埋め込みましょう

Goでは、構造体の中に構造体を持たせることで、オブジェクト指向プログラミングのおける継承のようなことができる。

4.2.1 構造体の中に構造体を埋め込もう

Goの埋め込み(Embedded)という仕組みについて記載。他のプログラミング言語では継承などと呼ばれる処理にあたる。

Goで構造体を埋め込む

Pythonの継承と同じコードをGoで書く場合、構造体を構造体の中に埋め込む。

type Vertex struct {
	x, y int
}

func (v Vertex) Area() int {
	return v.x * v.y
}

func (v *Vertex) Scale(i int) {
	v.x = v.x * i
	v.y = v.y * i
}

type Vertex3D struct {
	Vertex
	z int
}

func (v Vertex3D) Area3D() int {
	return v.x * v.y * v.z
}

func (v *Vertex3D) Scale3D(i int) {
	v.x = v.x * i
	v.y = v.y * i
	v.z = v.z * i
}

func New(x, y, z int) *Vertex3D {
	return &Vertex3D{Vertex{x , y}, z}
}

func main() {
	v := New(3, 4, 5)
	v.Scale(10)
	fmt.Println(v.Area())
	fmt.Println(v.Area3D())
}
// 実行結果
// 1200
// 6000

func main() {
	v := New(3, 4, 5)
	v.Scale3D(10)
	fmt.Println(v.Area3D())
}
// 実行結果
// 60000

4.3 インタフェースを使ったプログラムを作ろう

Goのインタフェースは、メソッドの名前のみを宣言したもので、そのメソッドを持つ型は、インタフェースを実装していると判定される。
ここでは、Goのインタフェースの使い方について記載する。

4.3.1 インタフェースを作成しよう

  1. Humanというインタフェースを作成する
    1. 「Say()」というメソッドを書く。
    2. インタフェースではメソッド名のみ宣言して、処理のコードは書かない
  2. 構造体を作成してインタフェースに当てはめる。
    1. string型のNameというフィールドを持つPersonという
    2. Personに紐づくメソッドとしてSayを作成する
  3. Human型(インタフェース型)の変数mikeを宣言し、構造体にPersonに{"Mike"}を代入する
    1. 「mike.Say()」と実行すると、「Mike」と表示される。
  4. Human型の変数にPersonという構造体を代入する場合
    1. PersonはSayというメソッドを持っている必要がある。
    2. PersonはSayというメソッドを持っていない場合、エラーとなる
type Human interface {
	Say()
}

type Person struct {
	Name string
}

func (p Person) Say() {
	fmt.Println(p.Name)
}

func main() {
	var mike Human = Person{"Mike"}
	mike.Say()
}
// 実行結果
// Mike

インタフェースのメソッドで構造体の中身を書き換える場合

  1. Sayメソッドの処理に、p.Nameに「Mr.」を加える
    1. 構造体の中身を書き換えることになるので、Personの前に「*」をつけてレシーバーにする必要がある。
    2. Sayメソッドを変更しただけではエラーとなる。
      1. Sayメソッドはポインタレシーバーとなるので、main関数からのSayメソッドを呼び出す際にアドレスとして渡す必要がある。
func (p *Person) Say() {
	p.Name = "Mr." + p.Name
	fmt.Println(p.Name)
}

func main() {
	var mike Human = &Person{"Mike"}
	mike.Say()
}

// 実行結果
// Mr.Mike

4.3.2 ダックタイピング

  1. Humanというインタフェースを、引数として受け取る関数DriveCarを作る。
    1. 引数humanのSayメソッドの戻り値が「Mr.Mike」であれば、「Run」
    2. 引数humanのSayメソッドの戻り値が「Mr.Mike」以外は「Get out」
  2. 構造体PersonはHumanインタフェースのメソッドをすべて実装しているので、Humanインタフェースを実装したことになる。
  3. ダックタイピング
    1. 「もしもアヒルのように歩き、アヒルのように鳴くなら、それはアヒルであろう」という考えかたが由来
    2. つまり、構造体Personは「Humanインタフェースのメソッドをすべて実装しているから、それはHumanインタフェースを実装したものであろう」という考え方になる。
  4. インタフェースを実装していない場合
    1. DriveCarは引数にHumanインタフェースを指定しているので、Sayメソッドがない構造体は引数に使えない。
type Human interface {
	Say() string
}

type Person struct {
	Name string
}

func (p *Person) Say() string {
	p.Name = "Mr." + p.Name
	fmt.Println(p.Name)
	return p.Name
}

func DriveCar(human Human) {
	if human.Say() == "Mr.Mike" {
		fmt.Println("Run")
	} else {
		fmt.Println("Get out")
	}
}

func main() {
	var mike Human = &Person{"Mike"}
	var x Human = &Person{"X"}
	DriveCar(mike)
	DriveCar(x)
}

// 実行結果
// Mr.Mike
// Run
// Mr.X
// Get out

4.4 型アサーションとswitch typeを使う

Goでは、メソッドを持たない空のインターフェースを作成できる。
空のインターフェースには、どんな型の値でも入れることができるので、引数にどんな型が入るのか分からないときに活用できる。
そして、空のインターフェースに入れた値を特定の値として使うときに必要なのが型アサーションである。

4.4.1 型アサーションについて学ぼう

  1. 最初に、iというインターフェースを引数にするdoという関数を作ります。
  2. 引数の「interface{}」の空のインターフェースで、どのような型でも引数として受付します。
  3. do関数で引数iを2倍にして変数iiに代入、初期化し変数iiを表示する
  4. main関数でdo関数に10を渡して実行するとエラーとなる。
    1. これはiがインターフェース型なので、int型の数値と計算できないため。
func do(i interface{}) {
	ii := i * 2
	fmt.Println(ii)
}
func main() {
	do(10)
}
// 実行結果
// invalid operation: i * 2 (mismatched types interface{} and int) (exit status 1)

空のインタフェースが持つ値をintやstringといった具体的な型として扱うため、**型アサーション(Type Assertion)**という仕組みを使う。
「ii := i.(int)」とすることで、iの値をint型として扱い、変数iiに代入する。

func do(i interface{}) {
	ii := i.(int) 
	ii *= 2
	fmt.Println(ii)
}
func main() {
	do(10)
}
// 実行結果
// 20

空のインターフェースへの代入

「do(10)」を分かりやすく書くと、「var i interface{} = 10」と空のインターフェースに10を代入してから、「do(i)」でdo関数にiを渡していることになる。

文字列への型アサーション

func do(i interface{}) {
	ss := i.(string)
	fmt.Println(ss + "!")
}

func main() {
	do("Mike")
}
// 実行結果
// Mike!

4.4.2 switch typeで型ごとに処理を実行しよう

switch文で、実行結果に「v := i.(type)」と書き、iをtypeで型アサーションした結果をvに代入する。
i.(type)という書き方は、switchと一緒でなければ使えない。そのため、i.(type)を単独で書くとエラーとなる。
switchとtypeはセットであると覚えておくこと。

func do(i interface{}) {
	switch v := i.(type) {
		case int:
			fmt.Println(v * 2)
		case string:
			fmt.Println(v + "!")
		default:
			fmt.Printf("I don't know %T\n", f)
	}
}

func main() {
	do(10)
	do("Mike")
	do(true)
}
// 実行結果
// 20
// Mike!
// I don't know bool

4.5 Stringerで表示内容を変更しよう

Goでよく使われるインターフェースについて説明。
fmt.Printlnなどで変数の内容を表示することがあるが、この内容を変更することができる。
Stringerは、fmtパッケージに含まれるインターフェースです。
このインターフェースにあるStringメソッドを実行すると、fmt.Printlnなどによる表示が変更される。

4.5.1 Stringerインターフェースを利用しよう

Stringerはfmtパッケージのprint.goに書かれているインターフェースです。
Stringerインターフェースを実装することで、fmt.Println関数の表示内容を自由に変えられる。
Stringerについては、「A Tour of Go」という公式のチュートリアルに記載がある。

  1. Personに紐づくStringメソッドを作る
    1. 戻り値として、「return fmt.Sprintf("My name is %v.", p.Name)」
type Person struct {
	Name string
	Age int
}

func (p Person) String() string {
	return fmt.Sprintf("My name is %v.", p.Name)
}

func main() {
	mike := Person{"Mike", 22}
	fmt.Println(mike)
}
// 実行結果
// My name is Mike.

4.6 カスタムエラーを作成しよう

独自でエラーを作成する場合、Errorメソッドを作成してerrorとうインターフェースを実装することで、
エラー内容の表示を変更できる。
自分で作成したエラーを戻り値として返す場合、ポインタとして返すことが推奨されている。

4.6.1 errorインターフェースでカスタムエラーを作成しよう

  • エラーを独自で作成するには、errorインターフェースを利用する。
  • GoのErrorsのチュートリアルを確認すると、errorインターフェースがどのように書かれているか確認できる
  • Errorメソッドを作ることで、fmt.Printlnでエラーの中身を表示したいときに自分なりのエラーを表示することできる
type UserNotFound struct {
	Username string
}

func (e *UserNotFound) Error() string {
	return fmt.Sprintf("User not found: %v", e.Username)
}

func myFunc() error {
	// something wrong
	ok := false
	if ok {
		return nil
	}
	return &UserNotFound{Username: "mike"}
}

func main() {
	if err := myFunc(); err != nil {
		fmt.Println(err)
	}
}
// 実行結果
// User not found: mike

4.6.2 エラーはポインタで返そう

Errorは&と*をつけてポンタレシーバーとすることが推奨とされる。
エラー内容を比較する際に問題が起きる可能性があるため。

⚠️ **GitHub.com Fallback** ⚠️