4.Structオリエンテッド - iruma-tea/go_programming GitHub Wiki
Goにはクラスや継承といったオブジェクト指向(Obuject Oriented)プログラムのための機能がない。
しかし、データ型にメソッドを追加したり、構造体の中に構造体を持たせて擬似的な継承をしたりすることができる。
Goでは、構造体を含むデータ型に紐づいた関数のことをメソッドという。
構造体にメソッドを作成して呼び出してみよう。まず、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の機能でメソッド名が補完されて表示される。
メソッドで紐づけられた構造体の値を書き換えたい場合は、メソッド作成時にレシーバーに*をつける
ここでは、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というメソッドの中で、構造体の中身を書き換えることできる。
-
値レシーバー
- レシーバーに*を付けない場合は、構造体の参照ではなく値をコピーして渡していること
GoではNewという関数を作成して、他の言語のコンストラクタを実現する。
- VertexのXとYを小文字に置き換える。小文字すると、他のパッケージからは操作することはできない、このパッケージ内からの書き換えできるようになる
- 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を使って呼び出すことが多い。これはGoのパターンなので覚えておくと吉。
typeを使うと、組み込み型に新しい名前を付けた独自の型を作ることが可能。
独自の型にメソッドを持たせることができる。
type MyInt int
func (i MyInt) Double() int {
return int(i * 2)
}
func main() {
myInt := MyInt(10)
fmt.Println(myInt.Double())
}
// 実行結果
// 20
Goでは、構造体の中に構造体を持たせることで、オブジェクト指向プログラミングのおける継承のようなことができる。
Goの埋め込み(Embedded)という仕組みについて記載。他のプログラミング言語では継承などと呼ばれる処理にあたる。
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
Goのインタフェースは、メソッドの名前のみを宣言したもので、そのメソッドを持つ型は、インタフェースを実装していると判定される。
ここでは、Goのインタフェースの使い方について記載する。
- Humanというインタフェースを作成する
- 「Say()」というメソッドを書く。
- インタフェースではメソッド名のみ宣言して、処理のコードは書かない
- 構造体を作成してインタフェースに当てはめる。
- string型のNameというフィールドを持つPersonという
- Personに紐づくメソッドとしてSayを作成する
- Human型(インタフェース型)の変数mikeを宣言し、構造体にPersonに{"Mike"}を代入する
- 「mike.Say()」と実行すると、「Mike」と表示される。
- Human型の変数にPersonという構造体を代入する場合
- PersonはSayというメソッドを持っている必要がある。
- 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
- Sayメソッドの処理に、p.Nameに「Mr.」を加える
- 構造体の中身を書き換えることになるので、Personの前に「*」をつけてレシーバーにする必要がある。
- Sayメソッドを変更しただけではエラーとなる。
- 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
- Humanというインタフェースを、引数として受け取る関数DriveCarを作る。
- 引数humanのSayメソッドの戻り値が「Mr.Mike」であれば、「Run」
- 引数humanのSayメソッドの戻り値が「Mr.Mike」以外は「Get out」
- 構造体PersonはHumanインタフェースのメソッドをすべて実装しているので、Humanインタフェースを実装したことになる。
-
ダックタイピング
- 「もしもアヒルのように歩き、アヒルのように鳴くなら、それはアヒルであろう」という考えかたが由来
- つまり、構造体Personは「Humanインタフェースのメソッドをすべて実装しているから、それはHumanインタフェースを実装したものであろう」という考え方になる。
- インタフェースを実装していない場合
- 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
Goでは、メソッドを持たない空のインターフェースを作成できる。
空のインターフェースには、どんな型の値でも入れることができるので、引数にどんな型が入るのか分からないときに活用できる。
そして、空のインターフェースに入れた値を特定の値として使うときに必要なのが型アサーションである。
- 最初に、iというインターフェースを引数にするdoという関数を作ります。
- 引数の「interface{}」の空のインターフェースで、どのような型でも引数として受付します。
- do関数で引数iを2倍にして変数iiに代入、初期化し変数iiを表示する
- main関数でdo関数に10を渡して実行するとエラーとなる。
- これは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!
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
Goでよく使われるインターフェースについて説明。
fmt.Printlnなどで変数の内容を表示することがあるが、この内容を変更することができる。
Stringerは、fmtパッケージに含まれるインターフェースです。
このインターフェースにあるStringメソッドを実行すると、fmt.Printlnなどによる表示が変更される。
Stringerはfmtパッケージのprint.goに書かれているインターフェースです。
Stringerインターフェースを実装することで、fmt.Println関数の表示内容を自由に変えられる。
Stringerについては、「A Tour of Go」という公式のチュートリアルに記載がある。
- Personに紐づくStringメソッドを作る
- 戻り値として、「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.
独自でエラーを作成する場合、Errorメソッドを作成して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
Errorは&と*をつけてポンタレシーバーとすることが推奨とされる。
エラー内容を比較する際に問題が起きる可能性があるため。