[Golang] Interface

2021-11-17 hit count image

GolangでInterface(インターフェース)について概念を確認して、使う方法について説明します。

概要

今回のブログポストでは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でインターフェースは次のようなルールを持っています。

  1. メソッドは必ずメソッド名が定義する必要があります。
  2. パラメーターとリターンが異なっても同じ名前のメソッドは使えません。
  3. インターフェースではメソッドの実装は含まれません。
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構造体タイプはNameAgeと言う変数を持っていますし、WalkGetAgeと言うメソッドを持っています。

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タイプがどのタイプであるかどのように実装されたかとは関係なくそのタイプがWalkTalkを持っていると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インターフェースはReaderWriterインターフェースを両方持っていて、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で開発されています。

興味がある方はアプリをダウンロードしてアプリを使ってくれると本当に助かります。

Posts