title: 【転載】Go 言語基礎のインターフェース
date: 2021-08-09 16:41:33
comment: false
toc: true
category:
- Golang
tags: - 転載
- Go
- 基礎
- インターフェース
この記事は転載です:Go 言語基礎のインターフェース | 李文周のブログ
インターフェース(interface)はオブジェクトの振る舞いの規範を定義し、規範を定義するだけで実装はしません。具体的なオブジェクトが規範の詳細を実装します。
インターフェース#
インターフェース型#
Go 言語においてインターフェース(interface)は一種の型であり、抽象的な型です。
interface
は一組のmethod
の集合であり、duck-type programming
の一種の表れです。インターフェースが行うことは、まるでプロトコル(ルール)を定義するようなもので、ある機械が洗濯と脱水の機能を持っていれば、それを洗濯機と呼びます。属性(データ)には関心を持たず、振る舞い(メソッド)にのみ関心を持ちます。
あなたの Go 言語のキャリアを守るために、インターフェース(interface)は一種の型であることを忘れないでください。
なぜインターフェースを使用するのか#
type Cat struct{}
func (c Cat) Say() string { return "ニャーニャーニャー" }
type Dog struct{}
func (d Dog) Say() string { return "ワンワンワン" }
func main() {
c := Cat{}
fmt.Println("猫:", c.Say())
d := Dog{}
fmt.Println("犬:", d.Say())
}
上記のコードでは猫と犬を定義し、それぞれが鳴きます。main 関数内には明らかに重複したコードがあり、もし今後豚やカエルなどの動物を追加する場合、コードはさらに重複してしまいます。それでは、彼らを「鳴く動物」として扱うことはできないでしょうか?
このような例はプログラミングの過程でよく遭遇します:
例えば、オンラインショッピングモールでは、Alipay、WeChat、UnionPay などの方法でオンライン決済を行うことができます。これらを「決済方法」として扱うことはできないでしょうか?
例えば、三角形、四角形、円形はすべて周囲の長さと面積を計算できます。これらを「図形」として扱うことはできないでしょうか?
例えば、営業、管理、プログラマーは皆月給を計算できます。彼らを「従業員」として扱うことはできないでしょうか?
Go 言語では、上記のような問題を解決するためにインターフェースという概念が設計されています。インターフェースは、これまでの具体的な型とは異なり、抽象的な型です。インターフェース型の値を見ると、それが何であるかはわかりませんが、唯一知っているのは、そのメソッドを通じて何ができるかです。
インターフェースの定義#
Go 言語はインターフェース指向プログラミングを推奨しています。
各インターフェースは複数のメソッドで構成され、インターフェースの定義形式は以下の通りです:
type インターフェース型名 interface{
メソッド名1( パラメータリスト1 ) 戻り値リスト1
メソッド名2( パラメータリスト2 ) 戻り値リスト2
…
}
ここで:
- インターフェース名:
type
を使用してインターフェースをカスタム型名として定義します。Go 言語のインターフェースは命名時に一般的に単語の後にer
を追加します。例えば、書き込み操作を行うインターフェースはWriter
と呼ばれ、文字列機能を持つインターフェースはStringer
と呼ばれます。インターフェース名は、そのインターフェースの型の意味を強調することが望ましいです。 - メソッド名:メソッド名の最初の文字が大文字であり、このインターフェース型名の最初の文字も大文字である場合、このメソッドはインターフェースが存在するパッケージ(package)外のコードからアクセス可能です。
- パラメータリスト、戻り値リスト:パラメータリストと戻り値リスト内のパラメータ変数名は省略可能です。
例を挙げると:
type writer interface{
Write([]byte) error
}
このインターフェース型の値を見ると、それが何であるかはわかりませんが、唯一知っているのは、その Write メソッドを通じて何かを行うことができるということです。
インターフェースを実装する条件#
オブジェクトがインターフェース内のメソッドをすべて実装すれば、そのインターフェースを実装したことになります。言い換えれば、インターフェースは実装が必要なメソッドのリストです。
Sayer
インターフェースを定義してみましょう:
// Sayer インターフェース
type Sayer interface {
say()
}
dog
とcat
の 2 つの構造体を定義します:
type dog struct {}
type cat struct {}
Sayer
インターフェースにはsay
メソッドが 1 つしかないため、dog
とcat
にそれぞれsay
メソッドを実装すればSayer
インターフェースを実装できます。
// dogはSayerインターフェースを実装しました
func (d dog) say() {
fmt.Println("ワンワンワン")
}
// catはSayerインターフェースを実装しました
func (c cat) say() {
fmt.Println("ニャーニャーニャー")
}
インターフェースの実装は非常に簡単で、インターフェース内のすべてのメソッドを実装すれば、そのインターフェースを実装したことになります。
インターフェース型変数#
インターフェースを実装することにはどんな意味があるのでしょうか?
インターフェース型変数は、そのインターフェースを実装したすべてのインスタンスを格納できます。例えば、上記の例では、Sayer
型の変数はdog
とcat
型の変数を格納できます。
func main() {
var x Sayer // Sayer型の変数xを宣言
a := cat{} // catをインスタンス化
b := dog{} // dogをインスタンス化
x = a // catインスタンスを直接xに代入できます
x.say() // ニャーニャーニャー
x = b // dogインスタンスを直接xに代入できます
x.say() // ワンワンワン
}
ヒント: 以下のコードを観察し、ここでの_
の巧妙な使い方を感じ取ってください
// ginフレームワークのroutergroup.goから抜粋
type IRouter interface{ ... }
type RouterGroup struct { ... }
var _ IRouter = &RouterGroup{} // RouterGroupがIRouterインターフェースを実装していることを確認
値受信者とポインタ受信者によるインターフェースの実装の違い#
値受信者を使用してインターフェースを実装することと、ポインタ受信者を使用してインターフェースを実装することにはどのような違いがあるのでしょうか?次に、例を通じてその違いを見てみましょう。
Mover
インターフェースとdog
構造体があります。
type Mover interface {
move()
}
type dog struct {}
値受信者によるインターフェースの実装#
func (d dog) move() {
fmt.Println("犬は動ける")
}
この時、インターフェースを実装しているのはdog
型です:
func main() {
var x Mover
var wangcai = dog{} // 旺財はdog型
x = wangcai // xはdog型を受け取ることができます
var fugui = &dog{} // 富貴は*dog型
x = fugui // xは*dog型を受け取ることができます
x.move()
}
上記のコードからわかるように、値受信者を使用してインターフェースを実装した場合、dog 構造体でも構造体ポインタ * dog 型の変数でも、そのインターフェース変数に代入できます。Go 言語にはポインタ型変数の評価に関する構文糖があるため、dog ポインタfugui
は内部で自動的に*fugui
を評価します。
ポインタ受信者によるインターフェースの実装#
同じコードを使ってポインタ受信者の違いをテストしてみましょう:
func (d *dog) move() {
fmt.Println("犬は動ける")
}
func main() {
var x Mover
var wangcai = dog{} // 旺財はdog型
x = wangcai // xはdog型を受け取ることができません
var fugui = &dog{} // 富貴は*dog型
x = fugui // xは*dog型を受け取ることができます
}
この時、Mover
インターフェースを実装しているのは*dog
型なので、x
にdog
型の wangcai を渡すことはできません。この時、x は*dog
型の値のみを格納できます。
面接問題#
注意: これは「できる」または「できない」と答える必要がある問題です!
まず、以下のコードを観察し、このコードがコンパイルできるかどうかを答えてください。
type People interface {
Speak(string) string
}
type Student struct{}
func (stu *Student) Speak(think string) (talk string) {
if think == "sb" {
talk = "あなたは大イケメンです"
} else {
talk = "こんにちは"
}
return
}
func main() {
var peo People = Student{}
think := "bitch"
fmt.Println(peo.Speak(think))
}
型とインターフェースの関係#
一つの型が複数のインターフェースを実装する#
一つの型は同時に複数のインターフェースを実装でき、インターフェース同士は互いに独立しており、相手の実装を知りません。例えば、犬は鳴くこともでき、動くこともできます。私たちはそれぞれ Sayer インターフェースと Mover インターフェースを定義します: Mover
インターフェース。
// Sayer インターフェース
type Sayer interface {
say()
}
// Mover インターフェース
type Mover interface {
move()
}
犬は Sayer インターフェースも実装でき、Mover インターフェースも実装できます。
type dog struct {
name string
}
// Sayerインターフェースを実装
func (d dog) say() {
fmt.Printf("%sはワンワンワンと鳴きます\n", d.name)
}
// Moverインターフェースを実装
func (d dog) move() {
fmt.Printf("%sは動けます\n", d.name)
}
func main() {
var x Sayer
var y Mover
var a = dog{name: "旺財"}
x = a
y = a
x.say()
y.move()
}
複数の型が同じインターフェースを実装する#
Go 言語では異なる型が同じインターフェースを実装することもできます。まず、Mover
インターフェースを定義し、move
メソッドを持つ必要があります。
// Mover インターフェース
type Mover interface {
move()
}
例えば、犬は動けますし、自動車も動けます。この関係を次のように実装できます:
type dog struct {
name string
}
type car struct {
brand string
}
// dog型がMoverインターフェースを実装
func (d dog) move() {
fmt.Printf("%sは走れます\n", d.name)
}
// car型がMoverインターフェースを実装
func (c car) move() {
fmt.Printf("%sは70マイルの速度で走ります\n", c.brand)
}
この時、コード内で犬と自動車を動く物体として扱うことができ、具体的に何であるかを気にせず、彼らのmove
メソッドを呼び出すことができます。
func main() {
var x Mover
var a = dog{name: "旺財"}
var b = car{brand: "ポルシェ"}
x = a
x.move()
x = b
x.move()
}
上記のコードの実行結果は以下の通りです:
旺財は走れます
ポルシェは70マイルの速度で走ります
さらに、インターフェースのメソッドは、必ずしも一つの型によって完全に実装される必要はありません。インターフェースのメソッドは、他の型や構造体を埋め込むことによって実装できます。
// WashingMachine 洗濯機
type WashingMachine interface {
wash()
dry()
}
// 乾燥機
type dryer struct{}
// WashingMachineインターフェースのdry()メソッドを実装
func (d dryer) dry() {
fmt.Println("脱水します")
}
// ハイアール洗濯機
type haier struct {
dryer // 乾燥機を埋め込む
}
// WashingMachineインターフェースのwash()メソッドを実装
func (h haier) wash() {
fmt.Println("洗います")
}
インターフェースのネスト#
インターフェース同士はネストすることで新しいインターフェースを作成できます。
// Sayer インターフェース
type Sayer interface {
say()
}
// Mover インターフェース
type Mover interface {
move()
}
// インターフェースのネスト
type animal interface {
Sayer
Mover
}
ネストされたインターフェースの使用は通常のインターフェースと同じで、ここでは cat が animal インターフェースを実装します:
type cat struct {
name string
}
func (c cat) say() {
fmt.Println("ニャーニャーニャー")
}
func (c cat) move() {
fmt.Println("猫は動けます")
}
func main() {
var x animal
x = cat{name: "花花"}
x.move()
x.say()
}
空インターフェース#
空インターフェースの定義#
空インターフェースは、何のメソッドも定義していないインターフェースです。したがって、任意の型が空インターフェースを実装しています。
空インターフェース型の変数は、任意の型の変数を格納できます。
func main() {
// 空インターフェースxを定義
var x interface{}
s := "Hello 沙河"
x = s
fmt.Printf("type:%T value:%v\n", x, x)
i := 100
x = i
fmt.Printf("type:%T value:%v\n", x, x)
b := true
x = b
fmt.Printf("type:%T value:%v\n", x, x)
}
空インターフェースの応用#
空インターフェースを関数の引数として使用#
空インターフェースを使用すると、任意の型の関数引数を受け取ることができます。
// 空インターフェースを関数の引数として使用
func show(a interface{}) {
fmt.Printf("type:%T value:%v\n", a, a)
}
空インターフェースを map の値として使用#
空インターフェースを使用すると、任意の値を保存できる辞書を作成できます。
// 空インターフェースをmapの値として使用
var studentInfo = make(map[string]interface{})
studentInfo["name"] = "沙河娜扎"
studentInfo["age"] = 18
studentInfo["married"] = false
fmt.Println(studentInfo)
型アサーション#
空インターフェースは任意の型の値を格納できますが、どのようにしてその格納された具体的なデータを取得するのでしょうか?
インターフェース値#
インターフェースの値(略してインターフェース値)は、具体的な型
と具体的な型の値
の 2 つの部分で構成されています。これら 2 つの部分はそれぞれインターフェースの動的型
と動的値
と呼ばれます。
具体的な例を見てみましょう:
var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil
以下の図を参照してください:
空インターフェース内の値を判断したい場合は、型アサーションを使用できます。その構文形式は:
x.(T)
ここで:
- x:
interface{}
型の変数を示します - T:アサーションされる
x
の可能性のある型を示します。
この構文は 2 つのパラメータを返します。最初のパラメータはx
をT
型に変換した後の変数で、2 番目の値はブール値で、true
であればアサーションが成功したことを示し、false
であればアサーションが失敗したことを示します。
例を挙げると:
func main() {
var x interface{}
x = "Hello 沙河"
v, ok := x.(string)
if ok {
fmt.Println(v)
} else {
fmt.Println("型アサーションに失敗しました")
}
}
上記の例では、アサーションを複数回行う必要がある場合は、複数のif
判断を書く必要があります。この時、switch
文を使用して実現できます:
func justifyType(x interface{}) {
switch v := x.(type) {
case string:
fmt.Printf("xは文字列です,値は%vです\n", v)
case int:
fmt.Printf("xは整数です,値は%vです\n", v)
case bool:
fmt.Printf("xはブール値です,値は%vです\n", v)
default:
fmt.Println("サポートされていない型です!")
}
}
空インターフェースは任意の型の値を格納できるため、Go 言語における空インターフェースの使用は非常に広範です。
インターフェースについて注意すべきことは、2 つ以上の具体的な型が同じ方法で処理される必要がある場合にのみインターフェースを定義する必要があるということです。インターフェースのためにインターフェースを書くべきではなく、そうすると不必要な抽象が増え、不要な実行時のコストを引き起こすことになります。
練習問題#
インターフェースを使用して、端末にログを書き込むことも、ファイルにログを書き込むこともできる簡易ログライブラリを実装してください。