Goのインターフェースとは?
なぜこれを書くのか?
今回はGo言語におけるインターフェースに関しての記事を書いていこうと思います。
Go言語を学習している人ならば必ず通るインターフェースなのですが、学習していると必ずと言っていいくらいに最初に苦しむのがこの概念だと思います。
私もこれまでGoの勉強をしてきた中で一番理解に苦しんだものと言っても過言ではないくらいに苦しみました...
この記事では、私のようにインターフェースに苦しんでいる人に
Goのインターフェースって他の言語のインターフェースと何が違うのか? とか、
実務のコードではどんなふうに使用されるのか? などをお伝えできる記事になれば幸いだと思います!
それではみていきましょう〜
この記事の対象者
この記事は基本的なGo言語のコードを読める人を対象にしています。
結構基本的な内容を噛み砕いて表現しているのでそこまで厳密な定義や曖昧な表現などあるかと思いますが、ご了承ください。
そもそもGo言語のインターフェースとは?
Go言語でのインターフェースを端的に言い表すと以下のようになります。
Interfaces in Go provide a way to specify the behavior of an object: if something can do this, then it can be used here. We've seen a couple of simple examples already; custom printers can be implemented by a String method while Fprintf can generate output to anything with a Write method. Interfaces with only one or two methods are common in Go code, and are usually given a name derived from the method, such as io.Writer for something that implements Write.
日本語訳
Goのインターフェースは、オブジェクトの振る舞いを指定する方法を提供する。カスタム・プリンターはStringメソッドで実装できるし、FprintfはWriteメソッドで何にでも出力を生成できる。メソッドが1つか2つしかないインターフェースはGoのコードでは一般的で、Writeを実装するものにはio.Writerというように、メソッドから派生した名前がつけられるのが普通です。
まあこれで理解できたら何も困らないです。
正直全くわからないと思うのでもう少し噛み砕いた表現にしてみていきましょう。
ずばり、Go言語でのインターフェースを一言でもう少し噛み砕いて言うと
Goでは構造体をレシーバとしてメゾットを定義することができる。
そして、そのメゾットがインターフェースの条件を満たしていればそれは自動的にインターフェースを実現したこととみなされる。
と言い換えることができます。
もう少し考えやすいように実際の活用例をみていきましょう。
実際のコード
インターフェースの定義
この定義で振る舞いを表現します。
具体的にはどのメゾットがどの引数を受け取り、どのような返り値を持っているかを示したものです。
以下のように定義します。
// greeterと言うインターフェースの定義
type Greeter interface {
// メゾットの"Greet"と言う関数は引数に何ももたないで、返り値として文字列を返す
Greet() string
}
このようにしてどのメゾットがどのような振る舞いをするのかを定義します。
構造体の定義
ここでは構造体を定義しておきます。今回はName
フィールドを持ったPerson
構造体を定義しておきます。構造体の詳しい説明などは省略します。
type Person struct {
Name string
}
構造体にメゾットを定義
ここで先ほど定義した構造体にメゾットGreet
メゾットを定義していきます。
メンバー関数に関する説明はここでは省略します。
func (p Person) Greet() string {
return "Hello, I am " + p.Name
}
実際にインターフェースを使用する
ここではmain関数で実際にPerson
のインスタンスを作成してそこからインターフェースを通じて呼び出しています。
func SayHello(g Greeter) {
fmt.Println(g.Greet())
}
func main() {
p := Person{Name: "UserName"}
SayHello(p) // ✅ PersonはGreeterの条件を満たしてるのでOK!
}
これまでのコードのまとめ
Go言語のインターフェースの実装は暗黙的な実装であることが言えます。
これはここまでのコードで見てきたものでもわかりますが、Person
はGreeter
を実装するなどと書かなくてもGreet()
を持っていることだけで自動的に型が実装されています。
またインターフェースで定義した内容は少しメゾットの名前と同じようなものでわかりにくかったですが、インターフェースのGreeter
はGreet()
できる何か であることを宣言しているに過ぎないのです。なので、仮にこれがPerson
構造体ではなく、Animal
構造体になってもフィールドにstring型の何かしらのフィールドを含んでいればGreeter
をインターフェースとして実装できるのです。一応以下にその例も示しておきます。
こんな感じのコードの例を他の方の記事で見た気がしなくもないですが...笑
package main
import "fmt"
// インターフェース定義
type Greeter interface {
Greet() string
}
// Person 構造体とそのメソッド
type Person struct {
Name string
}
func (p Person) Greet() string {
return "Hello, I am " + p.Name
}
// Animal 構造体とそのメソッド
type Animal struct {
Species string
}
func (a Animal) Greet() string {
return "Hello from a " + a.Species
}
// インターフェース型を受け取る関数
func SayHello(g Greeter) {
fmt.Println(g.Greet())
}
// main関数
func main() {
p := Person{Name: "User"}
a := Animal{Species: "Cat"}
SayHello(p) // Hello, I am User
SayHello(a) // Hello from a Cat
}
他の言語のインターフェースと比較
ここまでGoでのインターフェースとその使用例を見てきましたが、同様の振る舞いをするコードをTypeScriptでも実装してみたものを見てみましょう。
最近はTypeScriptでコードを書くこともあると思うので比較対象に選択しました。
余談ですが、筆者自身はフルスタックTSを使用するのは非常に好きです。特にHono+Reactの選択肢は非常にいいと思います。ただ、フロントエンドとバックエンドに同じ言語を選んだとしても全く違う書き方ですし、ほぼ違う言語として認識した方がいいかと思います。学生の作成するアプリケーションのレベルであればCloudflareに全て簡単にデプロイできます。
バックエンド-Cloudflare Workers
フロントエンド-Cloudflare Pages
RDB-Cloudflare D1
を使用することで無料でそれなりの規模感のものを作成できます。以下の記事を参考にしてみるといいかと思います。(私はprismaではなくDrizzleを使用しましたが...)
Next.jsでのフルスタック開発はあまり好きではないです笑
話を戻してTypeScriptのコードを見ていきましょう。
// インターフェースの定義。greetメゾットは文字列を返す
interface Greeter {
greet(): string;
}
// Personクラスの宣言。inplementsを使用して明示的にPersonはGreeterを実装すると宣言している。
class Person implements Greeter {
name: string;
constructor(name: string) {
this.name = name;
}
greet(): string {
return `Hello, I am ${this.name}`;
}
}
先ほどのGo言語でのコードと比べると私の感覚としては、非常にわかりやすくなったと感じます。
これはおそらくどこでインターフェースとクラスが結びついているのか?が明示的に宣言されているからだと思います。
ここではそこまでコードの深掘りはしませんが、簡単な認識の違いを以下に示しておきます。
比較項目 | Go | TypeScript |
---|---|---|
インターフェースを満たす方法 | 暗黙的で、実装するとは書かない | 明示的でimplements キーワードが必要 |
メゾットの定義場所 | 構造体の外側に構造体のメンバ関数として定義 | クラスの中に直接定義 |
型の柔軟性 | 動作の詳細ではなく、引数と返り値の方のみ宣言 | 型名や継承構造が必要 |
ここまでの比較をまとめると
Goは暗黙的なインターフェース実装(暗黙的)
TypeScriptはインターフェースを明示的にimplements(実装)しているか
と言う違いがあります。
なんとなくわかってきたでしょうか?
もう一息、実務のコードでどんな感じでインターフェースが使用されているのかを見てみましょう。
実際に使用されているコードの例
これはGoを実務で始めて1ヶ月弱の学生の感想なのでもっと使用されているところがあるかもしれないのでご了承ください。また、これは簡単に考えたコードを書いているだけなので動作はしないかと思います。
ここでは説明を省略しますがクリーンアーキテクチャを使用している想定です。
簡単のため、UsecaseとRepositoryの間にインターフェースを挟んでいると言う状況の想定です。
アーキテクチャに関しての知識がない方は私が書いた以下の記事も読んでもらえると嬉しいです!これはレイヤードアーキテクチャに関しての内容ですが...笑
また、他の方のクリーンアーキテクチャの記事も貼っておきます。
リポジトリの実装インターフェース
ここではUserRepositoryと言うインターフェースを定義しています。
package repository
// UserRepositoryのFindByIDメゾットの返り値の型に使用している。
type User struct {
ID int
Name string
}
// UserRepositoryのインターフェースを定義
type UserRepository interface {
// FindByIDメゾットはstring型のidを引数に持ってUserとerror型のインスタンスを返す。
FindByID(id int) (*User, error)
}
先ほどのインターフェースで実装したリポジトリの具体的な実装
上で記述したものの実際の実装(FindByID
の具体的な処理の流れ)を記述していきます。
package repositoryimpl
import (
"fmt"
"gorm.io/gorm"
)
type UserRepositoryImpl struct {
// DBに接続したインスタンス
db *gorm.DB
}
// NewUserRepositoryImpl は UserRepository を実装した構造体を生成する関数
// ここでUserRpositoryとUserRepositoryImplの間で実装するメゾットの制約を課している。
// 具体的にはインターフェースで宣言したメゾット、その引数、返り値を縛っている。
func NewUserRepositoryImpl(db *gorm.DB) repository.UserRepository {
return &UserRepositoryImpl{
db: db,
}
}
func (r *UserRepositoryImpl) FindByID(id int) (*User, error) {
// 実際のユーザー取得の処理
var user User
err := r.db.First(&user, id).Error
if err != nil {
return nil, err
}
return &user, nil
}
リポジトリの呼び出しもとのインターフェースの実装
package usecase
import (
"fmt"
"yourproject/repository"
)
type UserUseCase struct {
repo repository.UserRepository
}
func NewUserUseCase(repo repository.UserRepository) *UserUseCase {
return &UserUseCase{repo: repo}
}
func (u *UserUseCase) ShowUser(id int) {
user, err := u.repo.FindByID(id)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Printf("User found: ID=%d, Name=%s\n", user.ID, user.Name)
}
ここでは色々New~~~
などの形式の関数が出てきているのですが、Goでは正式ではコンストラクタ関数などは存在してないですが、コンストラクタ関数という名前で呼ばれていることが多いです。
アプリケーションの初期化の際に依存性注入 などを行う必要がありそこで使用している関数です。
他の方の記事でいい感じのものがあったので貼っておきます。
上のコードで一番注視するべきなのは
func NewUserRepositoryImpl(db *gorm.DB) repository.UserRepository {
return &UserRepositoryImpl{
db: db,
}
}
この部分です。ここでインターフェースとその具体的な実装をつなぎ合わせているのです。
ちなみに、インターフェースその実装で引数や返り値の型がうまくあっていない状態ではGoは静的解析の時点でエラーを出してくれます。これも開発している上では嬉しい点ですね。スキーマの修正や、エンティティなどのフィールドの変更があった際にはこの型エラーは非常に強い効力を発揮します。ありがたい!
こんな感じでGo言語のインターフェースを見てきました。
具体的な実務でのGoの使用方法の説明はかなり簡潔になってしまいましたが、どうでしたか?
なんとなくのイメージができてくれたら嬉しいです!
またTypeScriptとのインターフェースの比較をしてGoのインターフェースの癖の強さみたいなものに気づいてくれましたら嬉しいです!
それではこの記事はここまでです。
ありがとうございました。
Discussion