目次
概要
今回のブログポストではGolangのInterface
(インターフェース)について詳しく調べて、使う方法について説明します。このブログポストで紹介するコードは次のリンクで確認できます。
インターフェース
Golangでインターフェースは具体化されたオブジェクト(Concrete object)ではなく抽象化された相互作用で関係を表現するために使います。
Golangでは次のようにインターフェースを宣言することが出来ます。
type INTERFACE_NAME interface {
METHOD_NAME(PARAMETER_NAME) RETURN_TYPE
...
}
Golangではタイプ宣言キーワード(type)を使ってインターフェースを宣言しますし、インターフェース名の後、インターフェース宣言キーワード(interface)を追加してインターフェースを定義します。
インターフェース中には実装がないメソッド(メソッド名、パラメーター、リターンタイプだけ宣言)を宣言します。このように宣言されたメソッドたちを持ってるタイプを私たちが定義したインターフェースで認識しますことの意味です。
Golangではインターフェースも1つのタイプで、インターフェースで変数を宣言することもできます。
これを確認するためmain.go
ファイルを生成して次のように修正します。
package main
import "fmt"
type SampleInterface interface {
SampleMethod()
}
func main() {
var s SampleInterface
fmt.Println(s)
}
これを実行すると次のような結果が表示されます。
# go run main.go
<nil>
インターフェースは内部動作を隠して抽象化(Abstraction)をするため使えます。抽象化を使って依存性を抑えててカップリングに主に使います。
インターフェースのデフォルト
Golangでインターフェースのデフォルトはnil
です。これを確認するためmain.go
ファイルを次のように修正します。
package main
import "fmt"
type SampleInterface interface {
SampleMethod()
}
func main() {
var s SampleInterface
fmt.Println(s)
s.SampleMethod()
}
このようにプログラムを作成したら、次のコマンドを実行してモジュールを生成して、プログラムをビルドしてみます。
go mod init github.com/dev-yakuza/study-golang/interface/nil
go mod tidy
go build
このようにビルドすると、main.go
ファイルがあるところにnil
と言う名前のファイルが生成されることが確認できます。
このファイルを実行すると次のような結果が表示されます。
# ./nil
<nil>
panic: runtime error: invalid memory address or nil pointer dereference
上の例題は文法的には問題がないので、ビルドはできます。しかし、インターフェース変数に値を設定してないので、ランタイムエラー(nil pointer エラー)が発生することが確認できます。
Golangでモジュールを使う方法について下記のブログポストを参考にしてください。
インターフェースのルール
Golangでインターフェースは次のようなルールを持っています。
- メソッドは必ずメソッド名が定義する必要があります。
- パラメーターとリターンが異なっても同じ名前のメソッドは使えません。
- インターフェースではメソッドの実装は含まれません。
type SampleInterface interface {
String() string
String(a int) string // Error: duplicated method name
_(x int) int // Error: no name method
}
これを確認するためmain.go
ファイルを次のように修正します。
package main
import "fmt"
type SampleInterface interface {
String() string
String(a int) string // Error: duplicated method name
_(x int) int // Error: no name method
}
func main() {
var s SampleInterface
fmt.Println(s)
}
これを実行すると次のような結果が表示されます。
# go run main.go
# command-line-arguments
./main.go:7:2: duplicate method String
./main.go:8:2: methods must have a unique non-blank name
例題
簡単な例題を作ってGolangでインターフェースを理解して見ましょう。main.go
ファイルを生成して次のように修正します。
package main
import "fmt"
type Human interface {
Walk() string
}
type Student struct {
Name string
Age int
}
func (s Student) Walk() string {
return fmt.Sprintf("%s can walk", s.Name)
}
func (s Student) GetAge() int {
return s.Age
}
func main() {
s := Student{Name: "John", Age: 20}
var h Human
h = s
fmt.Println(h.Walk())
// fmt.Println(h.GetAge()) // ERROR
}
これを実行すると次のような結果が表示されます。
# go run main.go
John can walk
ソースコードを詳しく見ると、Human
と言うインターフェースはWalk
と言う関数を持っています。
type Human interface {
Walk() string
}
そしてStudent
構造体タイプはName
とAge
と言う変数を持っていますし、Walk
とGetAge
と言うメソッドを持っています。
type Student struct {
Name string
Age int
}
func (s Student) Walk() string {
return fmt.Sprintf("%s can walk", s.Name)
}
func (s Student) GetAge() int {
return s.Age
}
次はStudent
構造体を使って変数を生成して、当該変数をHuman
インターフェースに割り当てました。
func main() {
s := Student{Name: "John", Age: 20}
var h Human
h = s
fmt.Println(h.Walk())
// fmt.Println(h.GetAge()) // ERROR
}
このように割り当てしたHuman
変数はWalk
と言う関数を持っていますので、Human
変数のWalk
関数をコールすることが出来ますが、GetAge
関数をコールすることが出来ないことは分かります。
ダックタイピング
Golangのインターフェースはダックタイピング(Duck typing)を実装しています。ダックタイピングとはもし、何かの鳥を見て、その鳥がアヒルように歩いて、アヒルように飛んで、アヒルように鳴くと、その鳥をアヒルとよぶと言う意味を持っています。
つまり、どんなオブジェクトがどんな変数を持っていて、どんな関数を持っていることとは関係なく、そのオブジェクトが使われてるところの方でこんな関数があったら、このタイプで見ることにしますと定義することが出来ます。
例題を使ってこの内容を確認して見ましょう。まず、user/main.go
ファイルを生成して次のように修正します。
package user
import "fmt"
type User struct {
Name string
}
func (u User) Walk() {
fmt.Println("Walking")
}
func (u User) Talk() {
fmt.Println("Talking")
}
そして次のようにモジュールを生成します。
# cd user
go mod init github.com/dev-yakuza/study-golang/interface/user
go mod tidy
このように生成したuser
パッケージはUser
タイプを持っていますし、そのUserタイプはWalk
メソッドとTalk
メソッドを持っています。次はこのように生成したuserパッケージをインターフェースを使ってダックタイピングをして見ましょう。
ダックタイピングをするためuser
フォルダと同じ位置にmain/main.go
ファイルを生成して次のように修正します。
package main
import (
"fmt"
"github.com/dev-yakuza/study-golang/interface/user"
)
type Human interface {
Walk()
Talk()
}
func main() {
u := user.User{Name: "John"}
fmt.Println(u)
h := Human(u)
fmt.Println(h)
}
そして次のコマンドを実行してモジュールを生成して、ローカルモジュールと連動します。
go mod init github.com/dev-yakuza/study-golang/interface/main
go mod edit -replace github.com/dev-yakuza/study-golang/interface/user=../user
go mod tidy
このように生成したモジュールを実行すると、次のような結果が表示されます。
# go run main.go
{John}
{John}
main/main.go
ファイルの内容を見るとUser
タイプがどのタイプであるかどのように実装されたかとは関係なくそのタイプがWalk
とTalk
を持っているとmain
パッケージではこれをHuman
として使いますと定義しました。
このようにインターフェースは外部のパッケージを使う時、自分のコードに合わせてオブジェクトを変更して使います。外部のパッケージを実装する立場ではタイプとメソッドを実装するとき、インターフェースを考える必要がありません。そのため、インターフェースの関数がそのタイプに実装されてない場合が発生する時もあり、その場合はコンパイルエラーが発生します。
ダックタイピングはコードを使うユーザ中心でコーディングができるようにしてくれます。外部のパッケージを提供する人は具体化されたオブジェクトを提供して、これを使うユーザは必要によってインターフェースを定義して自分のプログラムに合わせて変換して使えます。
Embedded interface
Golangでインターフェースは次のようにインターフェースを埋め込むことが出来ます。これをGolangではEmbedded interface
と言います。
type Reader interface {
Read() (n int, err error)
Close() error
}
type Writer interface {
Write() (n int, err error)
Close() error
}
type ReadWriter interface {
Reader
Writer
}
ReadWriter
インターフェースはReader
とWriter
インターフェースを両方持っていて、Read
, Write
, Close
関数を持つことになります。
タイプ変換
Golangでは次のようにインターフェースをタイプ(ConcreteType)に変換することが出来ます。
var a Interface
t := a.(ConcreteType)
インターフェースからタイプに変換する時、.(タイプ)
を使います。上のように使うとConcreteType
タイプの変数を生成してt
に保存します。
インターフェースをタイプで変換する時、同じインターフェースタイプの場合、タイプの変換が出来ますが、当該タイプが持ってない変数や関数を実行すると、ランタイムエラーが発生します。
これを確認するため、main.go
ファイルを次のように修正します。
package main
import "fmt"
type Human interface {
Learn()
}
type Teacher struct {
Name string
}
func (m *Teacher) Learn() {
fmt.Println("Teacher can learn")
}
func (m *Teacher) Teach() {
fmt.Println("Teacher can teach")
}
type Student struct {
Name string
}
func (m *Student) Learn() {
fmt.Println("Student can learn")
}
func Study(h Human) {
if h != nil {
h.Learn()
var s *Student
s = h.(*Student)
fmt.Printf("Name: %v\n", s.Name)
// s.Teach() // ERROR
}
}
func main() {
Study(&Student{Name: "John"})
}
これを実行すると次のような結果が表示されます。
# go run main.go
Student can learn
Name: John
インターフェースタイプ変換し、変換したいタイプが当該インターフェースではない場合コンパイルエラーが発生します。これを確認するため、main.go
ファイルを次のように修正します。
package main
import "fmt"
type Stringer interface {
String() string
}
type Student struct {
}
func main() {
var stringer Stringer
s := stringer.(*Student)
fmt.Println(s)
}
これを実行すると次のような結果が表示されます。
# go run main.go
# command-line-arguments
./main.go:14:15: impossible type assertion:
*Student does not implement Stringer (missing String method)
他のインターフェースタイプに変換
Golangではインターフェースを他のインターフェースでタイプ変換することが出来ます。文法的には全く問題がないですが、実際実行する時、変換されたインタフェースに当該関数がなくてランタイムエラーが発生する場合があります。
これを確認するためmain.go
を次のように修正します。
package main
import "fmt"
type Teacher interface {
Teach()
}
type Student interface {
Learn()
}
type Person struct {
}
func (f *Person) Learn() {
fmt.Println("Person can learn")
}
func Teach(s Student) {
t := s.(Teacher)
t.Teach()
}
func main() {
p := &Person{}
Teach(p)
}
これを実行すると次のような結果が表示されます。
# go run main.go
panic: interface conversion: *main.Person is not main.Teacher: missing method Teach
Person
タイプの変数をStudent
インスタンスで受けて、このように割り当てたStudent
インターフェースをTeacher
インターフェースで変換した後、Teacher
インターフェースのTeach
関数をコールしました。文法的には問題がないので、コンパイルエラーは発生しません。しかし、実際実行するとPerson
タイプではTeach
関数がないのでエラーが発生します。
Golangではこのような問題を解決するためタイプ変換が成功したかどうかをチェックする機能を提供してます。
タイプ変換が成功したかどうか
インターフェースのタイプ変換は文法的には問題がないので、ビルドする時にはエラーが発生しません。しかし、ランタイムでエラーが出る可能性があります。これを防ぐために、Golangはタイプを変換する時タイプがうまく変換されたかどうかを分かる方法を提供してます。
var a Interface
t, ok := a.(ConcreteType)
- t: タイプ変換結果
- ok: 変換が成功したかどうか
これを確認するためmain.go
ファイルを次のように修正します。
package main
import "fmt"
type Teacher interface {
Teach()
}
type Student interface {
Learn()
}
type Person struct {
}
func (f *Person) Learn() {
fmt.Println("Person can learn")
}
func main() {
p := &Person{}
s := Student(p)
s.Learn()
t, ok := s.(Teacher)
fmt.Println(ok)
if ok {
t.Teach()
}
}
これを実行すると次のような結果が表示されます。
# go run main.go
Person can learn
false
Person
タイプはStudent
インターフェースで割り当てることはできますが、Student
インターフェースはTeach
関数がないので、Teacher
インターフェースで変換することはできません。
したがって、インターフェースタイプ変換をする時、変換の結果がfalse
であることが確認できます。
このようにインターフェースのタイプ変換がうまくできたかを確認することができて、インターフェースタイプ変換をするときには主に次のように使います。
if t, ok := s.(Teacher); ok {
t.Teach()
}
空のインターフェース
Golangでは空のインターフェースを活用する場合があります。空のインターフェースはメソッドを持ってないので、全てのタイプを空のインターフェースで変換することが出来ます。
interface {}
このような空のインターフェースは次のように活用が出来ます。
package main
import "fmt"
type Student struct {
Age int
}
func Print(v interface{}) {
switch v := v.(type) {
case int:
fmt.Println("v is int", v)
case float64:
fmt.Println("v is float64", v)
case string:
fmt.Println("v is string", v)
default:
fmt.Printf("Not supported %T:%v", v, v)
}
}
func main() {
Print(10)
Print(3.14)
Print("Hello word")
Print(Student{Age: 10})
}
これを実行すると次のような結果が表示されます。
# go run main.go
v is int 10
v is float64 3.14
v is string Hello word
Not supported main.Student:{10}%
完了
これでGolangでインターフェースで何か、インターフェースを使う方法について見て見ました。また、インターフェースを使ってダックタイピングをとタイプ変換についても見て見ました。
私のブログが役に立ちましたか?下にコメントを残してください。それは私にとって大きな大きな力になります!
アプリ広報
Deku
が開発したアプリを使ってみてください。Deku
が開発したアプリはFlutterで開発されています。興味がある方はアプリをダウンロードしてアプリを使ってくれると本当に助かります。