Go言語インターフェースとは?基本から応用まで詳しく解説!

2025年4月17日木曜日

Go言語

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言語では、たくさんのメソッドを持つ巨大なインターフェースよりも、メソッドが一つか二つ程度の小さなインターフェースを組み合わせる方が好まれる傾向があります。

標準ライブラリの`io.Reader`や`io.Writer`などが良い例です。必要な機能だけを定義した小さなインターフェースの方が、再利用しやすく、実装する側の負担も軽くなります。

インターフェースは利用側で定義することも

他の言語では、インターフェースは提供側(実装する側)が定義するのが一般的かもしれません。

しかしGo言語では、インターフェースを利用する側(関数やパッケージ)が、「自分が必要とするメソッドはこれです」という形でインターフェースを定義することもよくあります。

これにより、利用側は自分が依存したい振る舞いだけを定義でき、提供側は利用側の都合を気にせず必要なメソッドを実装すれば良くなるため、より疎結合な設計がしやすくなります。

ポインタレシーバと値レシーバ

メソッドを定義する際に、レシーバを値レシーバ(例: `func (d Dog) Speak()`)にするか、ポインタレシーバ(例: `func (d *Dog) ChangeName(newName string)`)にするかで、インターフェースの実装とみなされるかどうかに影響があります。 

値レシーバを持つ型の値は、値レシーバのメソッドもポインタレシーバのメソッドも(暗黙的に)呼び出せますが、ポインタレシーバを持つ型のポインタは、ポインタレシーバのメソッドしか呼び出せません。 

インターフェースを満たすためには、そのインターフェースが要求するメソッドを「すべて」持っている必要があります。

もしインターフェースがポインタレシーバのメソッドを要求している場合、そのインターフェースを満たせるのは基本的にポインタ型になります。ちょっとややこしい部分ですが、メソッドのレシーバの型は意識しておくと良いでしょう。

これらの点を少し意識するだけで、よりGo言語らしい、読みやすく保守しやすいコードを書く助けになるはずです。

【まとめ】Go言語のインターフェースを理解して柔軟なコードを書こう

今回はGo言語のインターフェースについて、基本から応用、注意点まで駆け足で見てきました。

インターフェースは、

  • メソッドの集まりを定義する「型」であること

  • コードを柔軟にし(ポリモーフィズム)、部品間のつながりを緩やかにする(疎結合)メリットがあること

  • 特別な宣言なしに、必要なメソッドを実装すれば自動的に満たされること

  • 空インターフェース(`interface{}`)は何でも入れられる便利な型だけど、型アサーションやタイプスイッチが必要になること

  • 小さなインターフェースを心がけるなどの使い方のコツがあること

といった点がポイントでしたね。

最初は少し取っ付きにくいかもしれませんが、インターフェースの考え方を理解し、実際にコードで使ってみることで、その便利さや強力さを実感できるはずです。

Go言語の標準ライブラリにも多くのインターフェースが活用されているので、そちらのコードを読んでみるのも、とても勉強になりますよ。

【関連記事】 Go言語とは?特徴・できること・将来性

このブログを検索

  • ()

自己紹介

自分の写真
リモートワークでエンジニア兼Webディレクターとして活動しています。プログラミングやAIなど、日々の業務や学びの中で得た知識や気づきをわかりやすく発信し、これからITスキルを身につけたい人にも役立つ情報をお届けします。 note → https://note.com/yurufuri X → https://x.com/mnao111

QooQ