Go言語インターフェース、なんだか難しそう…プログラミング初心者の方は特にそう感じるかもしれませんね。
この記事では、Go言語のインターフェースの基本のキから、実際の使い方、ちょっとした応用まで、あなたの「?」を「!」に変えるお手伝いをします。
他の言語とは少し違うGo言語ならではの特徴も、サンプルコードを見ながら楽しく学んでいきましょう。この記事を読めば、インターフェースの仕組みがスッキリ理解できて、あなたのGo言語コードがもっと柔軟になるはず。
この記事で学べること
Go言語のインターフェースの基本的な考え方
インターフェースを使うとどんないいことがあるか
インターフェースの書き方と実装の仕方
実際のコード例(サンプル付き)
空インターフェースって何?
型アサーションとタイプスイッチの使い方
インターフェースを使う上での豆知識や注意点
Go言語のインターフェースとは?
さて、まず「インターフェース」って一体何者なんでしょう?
Go言語でのインターフェースは、メソッドの「集まり」を定義するもの、と考えると分かりやすいかもしれません。
具体的にどんな処理をするかは決めずに、「こんな名前で、こんな引数を受け取って、こんな結果を返すメソッドが必要ですよ」という「型」だけを決めるイメージです。
身近な例でいうと、家電の「コンセントプラグ」と壁の「コンセント」の関係に似ています。
壁のコンセント(インターフェース)は「ここにプラグを挿せますよ」という形だけが決まっていて、どんな家電(具体的な型、例えばドライヤーやテレビ)のプラグでも、形さえ合っていれば挿し込めますよね。
【イメージ】 壁のコンセント(インターフェース:電気を受け取る機能) ↑ 挿し込める ↑ ドライヤーのプラグ(型:電気を受け取る機能を実装) テレビのプラグ (型:電気を受け取る機能を実装) スマホ充電器のプラグ(型:電気を受け取る機能を実装)
Go言語の面白いところは、JavaやC#みたいに「この型は、このインターフェースを使います!」とわざわざ宣言(`implements`みたいなやつ)をしなくても、インターフェースが必要とするメソッドを全部持っていれば、自動的に「そのインターフェースを満たしている」と判断される点です。
これを「ダックタイピング」と呼んだりもします。「アヒルのように歩き、アヒルのように鳴けば、それはアヒルだ」という考え方ですね。このおかげで、コードがスッキリする場面が多いのです。
なぜGo言語でインターフェースが重要なのか?メリットを理解しよう
インターフェースがどんなものか、なんとなく掴めてきましたか?
では、なぜGo言語でインターフェースがよく使われるのでしょう?それには、ちゃんと理由があります。主なメリットを2つ見てみましょう。
コードが柔軟になる(ポリモーフィズム)
これがインターフェースの大きな強みの一つです。ポリモーフィズムとは「多態性」とも呼ばれ、同じインターフェースを満たす異なる型のオブジェクトを、同じように扱えることを指します。プログラムの部品同士のつながりを緩やかにする(疎結合)
インターフェースを使うと、プログラムの部品(パッケージや型)同士が、お互いの具体的な中身を知らなくても連携できるようになります。例えば、ある機能Aが、別の機能Bの詳細(どんな構造体かとか)を知らなくても、機能Bが特定のインターフェースさえ満たしていれば利用できる、という状況を作れます。
部品同士の結びつきが弱まる(疎結合になる)と、片方の部品を変更しても、もう片方への影響が少なくなり、修正や機能追加がしやすくなる、テストもしやすくなる、といった良い効果が期待できます。
つまり、インターフェースをうまく使うと、変化に強く、メンテナンスしやすい、読みやすいコードを書く手助けになるってわけですね。
Go言語インターフェースの基本的な書き方
ここではインターフェースの具体的な書き方を見ていきましょう。まずは定義の仕方、そして型への実装方法です。
インターフェース型の定義
インターフェースを定義するには、`type`キーワードと`interface`キーワードを使います。こんな感じです。
package main import "fmt" // Speakerというインターフェースを定義 // Speak()というメソッドを持つことを要求する type Speaker interface { Speak() string // メソッド名() 戻り値の型 } // --- ここはまだ実装のコードではないです --- func main() { // インターフェース自体はインスタンス化できない // var s Speaker // これはOKだけど、中身はnil fmt.Println("インターフェースを定義しました!") }
この例では、`Speaker`という名前のインターフェースを定義しました。このインターフェースは、`Speak()`という名前で、引数がなく、戻り値が`string`型のメソッドを一つだけ持つことを要求しています。
ポイントは、メソッドの中身(具体的な処理)はここでは書かない、ということです。あくまで「こういうメソッドが必要ですよ」という仕様を決めるだけです。
型へのインターフェース実装
次に、定義したインターフェースを、具体的な型(例えば構造体)に実装してみましょう。
Go言語では、特別なキーワードは使いません。インターフェースが要求するメソッドを、その型がすべて持っていれば、自動的に実装したことになります。
package main import "fmt" // Speakerインターフェース (再掲) type Speaker interface { Speak() string } // Dogという構造体を定義 type Dog struct { Name string } // Dog型にSpeakメソッドを実装する // これでDog型はSpeakerインターフェースを実装したことになる func (d Dog) Speak() string { return d.Name + " says Woof!" } // Catという構造体を定義 type Cat struct { Name string } // Cat型にもSpeakメソッドを実装する // これでCat型もSpeakerインターフェースを実装したことになる func (c Cat) Speak() string { return c.Name + " says Meow!" } func main() { // Dog型の変数を作成 dog := Dog{Name: "Buddy"} // Cat型の変数を作成 cat := Cat{Name: "Lucy"} // Speakerインターフェース型の変数に、Dog型の値を入れることができる! var speaker Speaker speaker = dog fmt.Println(speaker.Speak()) // Buddy says Woof! が出力される // Speakerインターフェース型の変数に、Cat型の値も入れることができる! speaker = cat fmt.Println(speaker.Speak()) // Lucy says Meow! が出力される }
このコードでは、`Dog`型と`Cat`型が、それぞれ`Speak()`メソッドを持っていますね。引数なしで`string`を返す、という`Speaker`インターフェースが要求する仕様とピッタリ一致しています。
だから、Go言語は「`Dog`も`Cat`も`Speaker`インターフェースを実装しているね!」と判断してくれるのです。`main`関数の中で、`Speaker`型の変数`speaker`に、`Dog`型の値`dog`や`Cat`型の値`cat`を代入できているのが確認できますね。
これがインターフェースの基本的な使い方です。
Go言語インターフェースの実践的な使い方を学ぼう
インターフェースの基本的な書き方が分かったところで、もう少し具体的な使い道を見ていきましょう。
ここでは、よく例に出される「図形」を使って、インターフェースのメリットであるポリモーフィズム(多態性)を体験してみます。
図形を例にしたポリモーフィズム実装
円(Circle)と長方形(Rectangle)という、形の違う図形があるとします。
でも、どちらも「面積を計算する」という共通の操作ができますよね。この「面積を計算する」という共通の操作をインターフェースとして定義してみましょう。
package main import ( "fmt" "math" ) // Shapeインターフェースを定義 // Area()というメソッド(面積を計算する)を持つことを要求 type Shape interface { Area() float64 } // Circle構造体を定義 type Circle struct { Radius float64 } // Circle型にAreaメソッドを実装 func (c Circle) Area() float64 { return math.Pi * c.Radius * c.Radius } // Rectangle構造体を定義 type Rectangle struct { Width float64 Height float64 } // Rectangle型にAreaメソッドを実装 func (r Rectangle) Area() float64 { return r.Width * r.Height } // 図形の面積を表示する関数 // 引数にShapeインターフェース型を受け取る func PrintArea(s Shape) { fmt.Printf("この図形の面積は %.2f です\n", s.Area()) } func main() { // 円を作成 circle := Circle{Radius: 5.0} // 長方形を作成 rectangle := Rectangle{Width: 4.0, Height: 6.0} // PrintArea関数に円を渡す PrintArea(circle) // 出力: この図形の面積は 78.54 です // PrintArea関数に長方形を渡す PrintArea(rectangle) // 出力: この図形の面積は 24.00 です fmt.Println("--- スライスに入れてまとめて処理 ---") // Shapeインターフェース型のスライスを作成 shapes := []Shape{circle, rectangle} // スライスの中身をループで処理 for _, shape := range shapes { PrintArea(shape) } // 出力: // この図形の面積は 78.54 です // この図形の面積は 24.00 です }
このコードでは、`Shape`というインターフェースを定義し、`Area()`メソッドを持つことを要求しています。
そして、`Circle`型と`Rectangle`型が、それぞれ自分の形の面積を計算する`Area()`メソッドを実装しました。これで、どちらの型も`Shape`インターフェースを満たしたことになります。
サンプルコード解説と実行結果
上のコードのポイントは`PrintArea`関数と`main`関数での`shapes`スライスです。
`PrintArea`関数は、引数として`Shape`インターフェース型の値`s`を受け取ります。この関数の中では、渡されてきたものが円なのか長方形なのか、具体的な型を気にしていません。
ただ`Shape`インターフェースが保証する`s.Area()`メソッドを呼び出しているだけです。それでも、実際に渡されたのが`Circle`型の値なら円の面積計算が、`Rectangle`型の値なら長方形の面積計算がちゃんと実行されます。これがポリモーフィズムの力です!
さらに`main`関数では、`Shape`インターフェース型のスライス`shapes`を作っています。
このスライスには、`Circle`型と`Rectangle`型という異なる型の値を、`Shape`インターフェースを満たしているという共通点だけで、一緒に入れることができています。
そして、`for`ループでスライスの中身を一つずつ取り出し、`PrintArea`関数に渡すことで、それぞれの図形の面積を統一的に表示できていますね。
実行結果を見ると、ちゃんとそれぞれの面積が計算されているのが分かります。
# 実行結果 この図形の面積は 78.54 です この図形の面積は 24.00 です --- スライスに入れてまとめて処理 --- この図形の面積は 78.54 です この図形の面積は 24.00 です
このように、インターフェースを使うと、異なる型のものを共通の枠組みで扱えるようになり、コードが非常にすっきりと、そして柔軟になるのです。
Go言語インターフェース - 空インターフェースとは?
Go言語には、ちょっと特別なインターフェースがあります。
それが「空インターフェース」、`interface{}`です。
名前の通り、空っぽ、つまり、何のメソッドも要求しないインターフェースです。
何のメソッドも要求しないということは…?そう、Go言語のどんな型でも、空インターフェースを満たしていることになるんです!
package main import "fmt" // 空インターフェースを受け取る関数 func PrintAnything(a interface{}) { fmt.Printf("受け取った値: %v, 型: %T\n", a, a) } func main() { PrintAnything(123) // int型 PrintAnything("hello") // string型 PrintAnything(3.14) // float64型 PrintAnything(true) // bool型 PrintAnything([]int{1, 2, 3}) // スライス型 }
この例の`PrintAnything`関数は、引数に空インターフェース`interface{}`を取ります。
そのため、`main`関数で呼び出すときに、数値(`int`)、文字列(`string`)、浮動小数点数(`float64`)、真偽値(`bool`)、スライス(`[]int`)など、どんな型の値でも渡すことができています。
標準パッケージの`fmt.Println`関数などが、どんな型の値でも表示できるのは、内部でこの空インターフェースが使われているからです。
とても便利な空インターフェースですが、注意点もあります。何でも受け入れられるということは、その変数に実際どんな型の値が入っているのか、コードを書いている時点では分からない、ということです。
そのため、受け取った値に対して特定の操作(例えば数値として計算するなど)を行いたい場合は、後述する「型アサーション」や「タイプスイッチ」が必要になります。
何でもかんでも空インターフェースを使うと、かえってコードが分かりにくくなることもあるので、使いどころを見極めるのがコツです。
Go言語インターフェース - 型アサーションタイプスイッチの使い方
空インターフェースや、他のインターフェース型の変数を受け取ったとき、
「この変数には、実際にはどんな型の値が入っているんだろう?」
「もし特定の型だったら、その型が持つメソッドを使いたいな」
など疑問に思うことがあるかもしれません。
そんなときに使うのが「型アサーション」と「タイプスイッチ」です。
型アサーション
型アサーションは、インターフェース型の変数の「中身」が、期待する特定の型であるかどうかをチェックし、もしそうであれば、その特定の型として値を取り出す操作です。
書き方は `変数.(期待する型)` となります。
package main import "fmt" func main() { var i interface{} = "hello" // 空インターフェースに文字列を入れる // 型アサーションでstring型として取り出す s := i.(string) fmt.Println(s) // 出力: hello // 型アサーションには、成功したかどうかを確認する形式もある (推奨) s, ok := i.(string) if ok { fmt.Printf("文字列でした: %s\n", s) // 出力: 文字列でした: hello } else { fmt.Println("文字列ではありませんでした") } // 違う型でアサーションしてみる(失敗する例) f, ok := i.(float64) if ok { fmt.Printf("float64でした: %f\n", f) } else { // okがfalseになるので、こちらの処理が実行される fmt.Println("float64ではありませんでした") // 出力: float64ではありませんでした } // ok を使わない形式で失敗すると panic (プログラムが強制終了) するので注意! // num := i.(int) // ここでpanicが発生する // fmt.Println(num) }
ポイントは、`変数.(期待する型)` の結果を2つの変数で受け取る `value, ok := ...` の形式を使うことです。
`ok`には、アサーションが成功したかどうかが`bool`値で入ります。もしアサーションが失敗してもプログラムが止まらず(panicせず)、`ok`が`false`になるので、`if ok { ... }` のように安全に処理を続けられます。
タイプスイッチ
もし、インターフェース変数の中身が、複数の型のうちのどれか、という可能性があって、型ごとに処理を分けたい場合は、「タイプスイッチ」が便利です。
`switch`文と型アサーションを組み合わせたような構文です。
package main import "fmt" func CheckType(a interface{}) { // タイプスイッチの基本形 switch v := a.(type) { // a.(type) が特別な書き方 case int: fmt.Printf("整数型の値です: %d\n", v) case string: fmt.Printf("文字列型の値です: %s\n", v) case bool: fmt.Printf("真偽値型の値です: %t\n", v) default: fmt.Printf("知らない型の値です: 型=%T, 値=%v\n", v, v) } } func main() { CheckType(100) // 出力: 整数型の値です: 100 CheckType("Gopher") // 出力: 文字列型の値です: Gopher CheckType(true) // 出力: 真偽値型の値です: true CheckType(3.14) // 出力: 知らない型の値です: 型=float64, 値=3.14 }
タイプスイッチでは、`switch`文の初期化ステートメント(ここでは `v := a.(type)`)で、変数`a`の型をチェックします。
そして、`case`節で具体的な型を指定し、一致した場合の処理を書きます。変数`v`には、その`case`節でチェックされた型の値が入るので、安全にその型の操作を行えます。どの`case`にも一致しなかった場合は`default`節が実行されます。
型アサーションとタイプスイッチは、インターフェース、特に空インターフェースを扱う上で欠かせないテクニックなので、ぜひ使い方を覚えておきましょう。
Go言語のインターフェースを使う際の注意点
インターフェースは便利ですが、いくつか知っておくと良い注意点や、よりGo言語らしい書き方のヒントがあります。
小さなインターフェースを心がける
Go言語では、たくさんのメソッドを持つ巨大なインターフェースよりも、メソッドが一つか二つ程度の小さなインターフェースを組み合わせる方が好まれる傾向があります。インターフェースは利用側で定義することも
他の言語では、インターフェースは提供側(実装する側)が定義するのが一般的かもしれません。ポインタレシーバと値レシーバ
メソッドを定義する際に、レシーバを値レシーバ(例: `func (d Dog) Speak()`)にするか、ポインタレシーバ(例: `func (d *Dog) ChangeName(newName string)`)にするかで、インターフェースの実装とみなされるかどうかに影響があります。値レシーバを持つ型の値は、値レシーバのメソッドもポインタレシーバのメソッドも(暗黙的に)呼び出せますが、ポインタレシーバを持つ型のポインタは、ポインタレシーバのメソッドしか呼び出せません。
インターフェースを満たすためには、そのインターフェースが要求するメソッドを「すべて」持っている必要があります。
もしインターフェースがポインタレシーバのメソッドを要求している場合、そのインターフェースを満たせるのは基本的にポインタ型になります。ちょっとややこしい部分ですが、メソッドのレシーバの型は意識しておくと良いでしょう。
これらの点を少し意識するだけで、よりGo言語らしい、読みやすく保守しやすいコードを書く助けになるはずです。
【まとめ】Go言語のインターフェースを理解して柔軟なコードを書こう
今回はGo言語のインターフェースについて、基本から応用、注意点まで駆け足で見てきました。
インターフェースは、
メソッドの集まりを定義する「型」であること
コードを柔軟にし(ポリモーフィズム)、部品間のつながりを緩やかにする(疎結合)メリットがあること
特別な宣言なしに、必要なメソッドを実装すれば自動的に満たされること
空インターフェース(`interface{}`)は何でも入れられる便利な型だけど、型アサーションやタイプスイッチが必要になること
小さなインターフェースを心がけるなどの使い方のコツがあること
といった点がポイントでしたね。
最初は少し取っ付きにくいかもしれませんが、インターフェースの考え方を理解し、実際にコードで使ってみることで、その便利さや強力さを実感できるはずです。
Go言語の標準ライブラリにも多くのインターフェースが活用されているので、そちらのコードを読んでみるのも、とても勉強になりますよ。
【関連記事】 Go言語とは?特徴・できること・将来性
0 件のコメント:
コメントを投稿
注: コメントを投稿できるのは、このブログのメンバーだけです。