Goの学習を行った記録としてこの記事に残す。
この記事では Goのメソッドとインターフェースについてまとめていく。
メソッド
Goでもユーザー定義の方に付随する関数を定義できる。
これを型メソッドあるいはメソッドと呼ぶ。
package main
import "fmt"
// 型Personを定義 //liststart1
type Person struct {
LastName string // 姓
FirstName string // 名
Age int // 年齢
} //listend1
// 型Personに付随するメソッドStringを定義(PersonにメソッドStringを付加) //liststart2
func (p Person) String() string { // 「(p Person)」がレシーバの指定
return fmt.Sprintf("%s %s:年齢%d歳", p.LastName, p.FirstName, p.Age)
} //listend2
キーワードfuncとメソッド名の間にレシーバが追加される。
レシーバに型名を書くことで、このメソッドStringが型Personに紐づくことになる
(他の型の変数がこのメソッドを使うことはできず、この方専用のものとなる)
一つの型について複数のメソッドに同じ名前を使うことはできない(オーバーロードできない)
メソッドの使い方は他の言語と変わらない。
func main() { //liststart3
p := Person { // Person型の変数pの宣言と初期化
LastName: "大谷",
FirstName: "翔平",
Age: 30,
}
output := p.String() // Personに付随するメソッドStringを呼び出す
fmt.Println(output) // 大谷 翔平:年齢30歳
} //listend3
-
ポインタレシーバと値レシーバ
レシーバにはポインタレシーバと値レシーバがある。
どちらのレシーバを使うかのルールは以下のようになる。- メソッドがレシーバを変更するならば、ポインタレシーバを使わなければならない
- メソッドがnilを扱う必要があれば、ポインタレシーバを使わなければならない
- メソッドが値を変更しないならば、値レシーバを使うことができる
型にポインタレシーバのメソッドが一つでもあるならば、レシーバを変更しないものも含め全てポインタレシーバを使って形式を揃えるのが一般的。
package main //liststart1
import (
"fmt"
"time"
)
type Counter struct { // Counter型を定義
total int // 合計
lastUpdated time.Time // 最終更新時刻
}
func (c *Counter) Increment() { // ポインタレシーバ(cはポインタ)
c.total++
c.lastUpdated = time.Now()
}
func (c Counter) String() string { // 値レシーバ(cにはコピーが渡される)
return fmt.Sprintf("合計: %d, 更新: %v", c.total, c.lastUpdated)
} //listend1
func main() { //liststart2
var c Counter
fmt.Println(c.String())
c.Increment() //「(&c).Increment()」と書かなくてもよい
fmt.Println(c.String())
} //listend2
上記のコードで、cがポインタでなくても、ポインタレシーバのメソッドを呼び出せる。
ポインタレシーバに対してポインタではないローカル変数を渡すと、Goは自動的に変数をポインタ型にしてくれる。
c.Increment()が(&c).Increment()に変換されている。
nilへの対応
nilを引数にしてメソッドを呼び出すとどうなるか。
Goではメソッドを起動しようとする。
値レシーバのメソッドの場合はパニックになり、ポインタレシーバのメソッドならメソッドがnilを処理できるようになっていれば有効な呼び出しになる。
関数にnilが渡された場合と同様、コピーのポインタを変更してもオリジナルは変更されない。
つまりnilを受け取ってnilでないものに変更するようなポインタレシーバはかけない。ポインタレシーバを持つメソッドがnilに対応していないのならば、nilの場合はエラーを返すようにする。
関数とメソッドの使い分け
関数として実装するかメソッドとして実装するかはどう決めればいいのか。
鍵となるのは、関数が他のデータに依存するかどうか。
パッケージレベルの状態はイミュータブルであるべき。プログラムのロジックが、起動時に設定された値や実行中に変更された値に依存する場合、そういった値は構造体に保存されるべきでロジックはメソッドとして実装されるべき。ロジックが入力引数にのみ依存するなら関数で良い。
型宣言と継承の違い
自分ですでに定義した型をベースに新たな型を定義できる。
type Score int //liststart1
type HighScore Score
type Person struct { // 人
LastName string // 姓
FirstName string // 名
Age int // 年齢
}
type Employee Person // 従業員 //listend1
「オブジェクト指向」の特徴と考えられる概念の中で「継承」が中心的なものであるというのは多くの人が賛同するだろう。
2つのクラス(オブジェクト)に継承関係があると、親の方の状態やメソッドが子の型でも使用でき、子の方の変数を親の方の変数に代入できるといった特徴がある。
Goである型をベースに別の型を宣言すると、継承関係にあるように見えるかもしれないが、実際はそうではない。両方の型が同じ型をベースにしているというだけで、型の間に階層関係はない。
継承機能のある言語では、親のインスタンスが使われている場面ではいつでも子のインスタンスを使うことができる。また、このインスタンスが親のインスタンスが持つ全てのメソッドとデータ構造を使える。
Goでは異なり、上記例のHighScore型の変数のScore型の変数への代入も、その逆も型変換なしではできない、またどちらの型の変数も型変換をしなければint型の変数に代入することもできない。
package main
import (
"fmt"
)
type Score int //liststart1
type HighScore Score
type Person struct { // 人
LastName string // 姓
FirstName string // 名
Age int // 年齢
}
type Employee Person // 従業員 //listend1
func (s Score) Double() Score { //liststart3
return s * 2
} //listend3
func main() {
// 型のない定数の代入は認められる //liststart2
var i int = 300
var s Score = 100
var hs HighScore = 200
// hs = s // コンパイル時のエラー!
// s = i // コンパイル時のエラー!
s = Score(i) // 型変換後に代入
hs = HighScore(s) // 型変換後に代入
fmt.Println(s, hs) // 300 300
hhs := hs + 20 //基底型(int)に対して使える演算子(+)は使える
fmt.Println(hhs) // 320 //listend2
s = 200 //liststart4
hs = 300
fmt.Println(s.Double()) // 400
fmt.Println(Score(hs).Double()) // 600
// fmt.Println(hs.Double()) // コンパイル時のエラー //listend4
}
埋め込みによる合成
Goには継承はないが、合成や昇格が組み込まれており、これを使ったコードの再利用が推奨されている。
package main
import (
"fmt"
)
type Employee struct { // 従業員 //liststart1
Name string
ID string
}
func (e Employee) Description() string { // 従業員に関する記述
return fmt.Sprintf("%s (%s)", e.Name, e.ID)
}
type Manager struct { // マネージャ
Employee // 型のみ書く(埋め込みフィールド) NameとIDが加わる
Reports []Employee // 部下(報告の対象者) Employeeのスライス
}
func (m Manager) FindNewEmployees() []Employee { // 新しい従業員を見つける
newEmployees := []Employee{ // Employee(従業員)のスライス
Employee{
"石田三成",
"13112",
},
Employee{
"徳川家康",
"13115",
},
}
return newEmployees
}
//listend1
func main() {
m := Manager{ //liststart2
Employee: Employee{
Name: "豊臣秀吉",
ID: "12345",
},
Reports: []Employee{},
}
fmt.Println(m.ID) // 12345
fmt.Println(m.Description()) // 豊臣秀吉 (12345)
m.Reports = m.FindNewEmployees()
fmt.Println(m.Employee) // {豊臣秀吉 12345}
fmt.Println(m.Reports) // [{石田三成 13112} {徳川家康 13115}]
//listend2
}
上記の例のように、Managerには Employee型のフィールドがあるが、そのフィールドには名前がつけられていない。こうするとEmployeeは埋め込みフィールドとなる。
埋め込みフィールドで宣言されているフィールドやメソッドは、それを埋め込んでいる上位の構造体に昇格し、その構造体から呼び出せる。
上記の例で言うと、m.ID , m.Description()と言うようにManegerから呼び出せる。
埋め込みと継承の違い
埋め込みを組み込みの機能としてサポートしているのはGo言語しかない。
Maneger型の変数をEmployee型の変数に代入できない。
ManagerのフィールドEmployeeにアクセスしたければ、明示しなければならない。
import (
"fmt"
)
type Employee struct { //liststart0
Name string
ID string
}
type Manager struct {
Employee
Reports []Employee
} //listend0
func (e Employee) Description() string {
return fmt.Sprintf("%s (%s)", e.Name, e.ID)
}
func main() {
m := Manager{ //liststart1
Employee: Employee{
Name: "大谷翔平",
ID: "17",
},
Reports: []Employee{},
}
var eOK Employee = m.Employee // OK!
fmt.Println(eOK) // {大谷翔平 17}
var eFail Employee = m // コンパイル時のエラー! //listend1
fmt.Println(eFail)
}
インターフェースとは
Goでのインターフェースは、Goで唯一の抽象型(実装を提供しない型)です。
- メソッドの集合 - 他言語のインターフェースのように、特定のクラスが満たすべき要件(実装するべき一群のメソッド)を示す。
- 型 - 変数がインターフェースを基盤とする型をもつことで、さまざまなことができる。例えば、任意の型の値を代入できる変数を定義する。
2.の例として特によく使われるのがinterface{}。これは、「0個のメソッドが定義された型」となるので、任意の型がこの条件を満たすことになる。したがってinterface{}と宣言された変数には、任意の型を記憶できる。
Goの1.18からはanyと書けるようになった。
type Stringer interface {
Stting() string //実装するメソッドのリスト
}
インターフェースの特徴は以下
- インターフェース型は型の集合を定義する。インターフェースも型の一種。
- インターフェース型の変数は、そのインターフェースが特定する型の集合に属する任意の型の値を保持できる。
- インターフェースリテラルにはインターフェースを満たすために実装するメソッドセット(メソッドのリスト)を書く
- インターフェースの名前は通常、「er」で終わる。
インターフェースが特別なのは、それが「暗黙的に」実装されるから。
具象型CのメソッドセットがインターフェースIのメソッドセットを完全に含めば、具象型CはインターフェースIを実装することになる。したがって具象型Cの変数などは、インターフェース型Iを持つと宣言された変数やフィールドに代入できる。
Goのインターフェースは「型の安全性」、「デカップリング」の両方を満たすことができ、静的言語と動的言語の両方の機能を併せ持つようなものとなる。
- 暗黙のインターフェースによる依存性の注入
デカップリングを容易にするために開発されたテクニックの一つに依存性注入(Dependency Injection : DI)ある。
Goでは依存性の注入の実装が容易で、追加のライブラリも必要ない。
依存性注入によって依存を外部化すると、コードが移管経過と共に進化していくときに必要な変更を小さくまとめることができる。
依存性注入もテストを容易にする。ユニットテストの作成の実態は、別の環境でのコードの再利用。機能を検証するための入力と出力に絞った環境で再利用する。
依存性注入のコードを実装するのが大変すぎると感じるなら、Googleが作成した依存性注入ヘルパーの「Wire」を使うことができる。