Goのinterface(インターフェース)とは、「このメソッドを持っていること」という約束を表す型です。 約束されたメソッドをすべて持つ型は、特別な宣言を書かなくても自動的にその interface として扱えます。そして、interface に入れた値から元の型を取り出すのが型アサーション v.(T)、型ごとに処理を分けるのが型スイッチ switch x := v.(type)、どんな値でも入る interface が any です。

この記事では、interface の定義から、型アサーション、型スイッチ、any、ジェネリクスとの使い分けまでを順に並べ、すべてのコードに go1.27.1 で動かした実行結果を付けています。ok を使わない型アサーションで出るpanicの文面と、nil ポインタを入れた interface が != nil になる現象も、出力つきで再現しました。仕組みは図と表で整理しています。

interfaceの早見表

書き方の早見表

やりたいこと コード 結果・ポイント
interface を定義する type Shape interface { Area() float64 } メソッドの名前・引数・戻り値を並べる
interface を満たす func (r Rect) Area() float64 { ... } 宣言は不要。メソッドがそろえば自動で満たす
interface 型の変数に入れる var s Shape = Rect{3, 4} メソッドが足りないとコンパイルエラー
元の型を取り出す(止まらない形) s, ok := v.(string) 合えば ok=true、合わなければゼロ値と ok=false
元の型を取り出す(短い形) s := v.(string) 合わないとpanic
型で分岐する switch x := v.(type) { case int: ... } case ごとに x がその型になる
何でも入る変数 var a any = 1 any は interface{} の別名
動的な型を表示する fmt.Printf("%T\n", v) int、main.Rect など
満たすことをコンパイル時に確かめる var _ ReadWriter = (*Memo)(nil) 足りないとコンパイルエラー
interface を組み合わせる type ReadWriter interface { Reader; Writer } 埋め込みで両方のメソッドを求める
型を保ったまま共通化する func Max[T cmp.Ordered](a, b T) T ジェネリクス。戻り値も同じ型

型アサーション・型スイッチ・ジェネリクスの使い分け

方法 型が決まる時点 型が合わないとき 向いている場面
型アサーション v.(T) 実行時 panic 型が確実にわかっているとき
型アサーション v, ok := v.(T) 実行時 ok=false 1つの型かどうかを確かめたいとき
型スイッチ 実行時 default に進む 複数の型で処理を分けたいとき
ジェネリクス コンパイル時 コンパイルエラー 同じ処理を複数の型に使いたいとき

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Go interfaceの使い方|型アサーション・型スイッチ・anyを図解」

この記事の前提と動かし方

コードはどれも1ファイルで完結していて、main.go に貼って go run main.go で動きます。

項目 内容
実行したGo go1.27.1 linux/amd64
実行コマンド go run main.go
使うパッケージ 標準ライブラリだけ(fmt、math、errors、cmp、strings、io、os)
必要な最小バージョン any とジェネリクスは Go 1.18 以降、cmp パッケージは Go 1.21 以降
出力の扱い 出力はそのまま掲載。panicのスタックトレースに出るファイルのパスだけ /home/taro/sample/ に置き換えています

nil ポインタを入れた interface が nil と等しくならない理由は、Go公式サイトのFAQ(go.dev/doc/faq)の「Why is my nil error value not equal to nil?」を今回開いて確かめました。any とジェネリクスが Go 1.18 で入ったこと、cmp が Go 1.21 で入ったことは、一般に知られた仕様として書いています(今回リリースノートは開いていません)。

interfaceを定義して使う:暗黙の実装

Goの interface は「暗黙の実装」です。 ほかの言語の implements にあたる宣言を書かず、必要なメソッドを持っているかどうかだけで判断されます。

package main

import (
    "fmt"
    "math"
)

// 「面積を返せるもの」という約束
type Shape interface {
    Area() float64
}

type Rect struct{ W, H float64 }
type Circle struct{ R float64 }

// implements の宣言は書かない。メソッドがそろえば自動で Shape になる
func (r Rect) Area() float64   { return r.W * r.H }
func (c Circle) Area() float64 { return math.Pi * c.R * c.R }

// Shape を受け取る関数は、Rect でも Circle でも動く
func describe(s Shape) {
    fmt.Printf("%T の面積は %.2f\n", s, s.Area())
}

func main() {
    describe(Rect{W: 3, H: 4})
    describe(Circle{R: 1})

    shapes := []Shape{Rect{1, 2}, Circle{2}, Rect{5, 5}}
    total := 0.0
    for _, s := range shapes {
        total += s.Area()
    }
    fmt.Printf("合計 %.2f\n", total)
}

実行結果:

main.Rect の面積は 12.00
main.Circle の面積は 3.14
合計 39.57

describe は Shape を受け取る関数なので、Rect を渡しても Circle を渡しても動きます。%T で表示した型が main.Rect、main.Circle と変わっていることから、interface 型の変数は「中に入っている値の型」を覚えていることがわかります。

用語 意味 上のコードでの例
interface 型 メソッドの約束 Shape
具体的な型 実際のデータとメソッドを持つ型 Rect、Circle
動的な型 interface の変数に、いま入っている値の型 main.Rect など(%T で見える)
暗黙の実装 宣言なしで、メソッドがそろえば満たすこと Rect に Area() を書いただけ

interfaceの値は「型」と「値」の組

仕組みを図にします。interface 型の変数は、中に「型」と「値」の2つを持っています。 この図は、あとで出てくる型アサーションと nil の話の土台になります。

flowchart LR
    subgraph s1["var s Shape = Rect{3, 4}"]
        T1["型: main.Rect"]
        V1["値: {3 4}"]
    end
    subgraph s2["var s Shape(何も入れていない)"]
        T2["型: なし"]
        V2["値: なし"]
    end
    s1 -- "s.Area()" --> M["Rect の Area() が呼ばれる"]
    s2 -- "s == nil" --> N["true"]
状態 型 値 == nil
何も入れていない interface なし なし true
Rect{3, 4} を入れた main.Rect {3 4} false
nil ポインタを入れた *main.NotFoundError nil false(後半で再現)

メソッドが足りないとコンパイルエラーになる

約束のメソッドが1つでも足りないと、代入の時点でコンパイラが知らせてくれます。

package main

import "fmt"

type Shape interface {
    Area() float64
    Perimeter() float64
}

type Rect struct{ W, H float64 }

func (r Rect) Area() float64 { return r.W * r.H }

func main() {
    var s Shape = Rect{3, 4}
    fmt.Println(s.Area())
}

実行結果(コンパイルエラー):

# command-line-arguments
./main.go:15:16: cannot use Rect{…} (value of struct type Rect) as Shape value in variable declaration: Rect does not implement Shape (missing method Perimeter)

missing method Perimeter と、足りないメソッドの名前まで出ます。暗黙の実装でも、型の確認はコンパイル時に行われます。

interfaceは小さく作って組み合わせる

Goの標準ライブラリには、io.Reader、io.Writer、fmt.Stringer、error のように、メソッドが1つだけの interface が多くあります。小さな interface を作り、必要なら埋め込みで組み合わせるのがGoらしい書き方です。

package main

import (
    "fmt"
    "io"
    "os"
    "strings"
)

// 小さな interface を組み合わせる(埋め込み)
type Reader interface{ Read() string }
type Writer interface{ Write(s string) }
type ReadWriter interface {
    Reader
    Writer
}

type Memo struct{ text string }

func (m *Memo) Read() string   { return m.text }
func (m *Memo) Write(s string) { m.text += s }
func (m *Memo) String() string { return "Memo(" + m.text + ")" }

// コンパイル時に「*Memo は ReadWriter を満たす」ことを確かめる書き方
var _ ReadWriter = (*Memo)(nil)

func main() {
    var rw ReadWriter = &Memo{}
    rw.Write("hello")
    fmt.Println(rw.Read())

    // fmt.Stringer を満たすと Println の表示が変わる
    fmt.Println(rw)

    // interface から別の interface への型アサーション
    if s, ok := rw.(fmt.Stringer); ok {
        fmt.Println("Stringer でもある:", s.String())
    }
    var r Reader = rw // 大きい interface から小さい interface へは代入できる
    _, isWriter := r.(Writer)
    fmt.Println("Reader の中身は Writer でもある:", isWriter)

    // 標準ライブラリの例:io.Writer を受け取る関数にいろいろ渡せる
    var sb strings.Builder
    fmt.Fprintf(&sb, "%d件", 3)
    fmt.Fprintln(os.Stdout, sb.String())
    var w io.Writer = &sb
    _, isStringer := w.(fmt.Stringer)
    fmt.Println(isStringer)

    // interface の値は (型, 値) の組
    var sh fmt.Stringer
    fmt.Printf("%T %v\n", sh, sh)
    sh = &Memo{"a"}
    fmt.Printf("%T %v\n", sh, sh)
}

実行結果:

hello
Memo(hello)
Stringer でもある: Memo(hello)
Reader の中身は Writer でもある: true
3件
true
<nil> <nil>
*main.Memo Memo(a)
出力 わかること
hello ReadWriter は Reader と Writer の両方のメソッドを使える
Memo(hello) String() string を持つ型は fmt.Stringer を満たし、Println の表示が変わる
Stringer でもある: ... interface から別の interface への型アサーションもできる
Reader の中身は Writer でもある: true 変数の型が Reader でも、中の値(*Memo)は Write を持っている
3件 fmt.Fprintf は io.Writer を受け取るので、strings.Builder にも os.Stdout にも書ける
<nil> <nil> と *main.Memo Memo(a) 何も入れていない interface は型も値も nil。入れると型が付く

var _ ReadWriter = (*Memo)(nil) は、「*Memo が ReadWriter を満たす」ことをコンパイル時に確かめるための書き方です。変数名を _ にして値を捨てているので、実行時には何もしません。メソッドを消したり引数を変えたりして約束から外れると、この行でコンパイルエラーになります。

型アサーションで元の型を取り出す

型アサーション v.(T) は、interface の中の値を型 T として取り出します。 2つの形があり、違いは「型が合わなかったとき」の動きです。

package main

import "fmt"

func main() {
    var v any = "hello"

    // 1. v, ok 形式:合わなくても止まらない
    s, ok := v.(string)
    fmt.Printf("%q %t\n", s, ok)

    n, ok := v.(int)
    fmt.Printf("%d %t\n", n, ok)

    // 2. if と組み合わせる定番の形
    if s, ok := v.(string); ok {
        fmt.Println("文字列の長さ:", len(s))
    }

    // 3. ok を使わない形:型が合えばそのまま取り出せる
    s2 := v.(string)
    fmt.Println(s2)

    // 4. ok を使わず、型が合わないと panic
    n2 := v.(int)
    fmt.Println("ここには来ない", n2)
}

実行結果:

"hello" true
0 false
文字列の長さ: 5
hello
panic: interface conversion: interface {} is string, not int

goroutine 1 [running]:
main.main()
    /home/taro/sample/main.go:25 +0x1d8
exit status 2
形 型が合うとき 型が合わないとき
s, ok := v.(string) s に値、ok=true s はゼロ値、ok=false。プログラムは続く
s := v.(string) s に値 panic で止まる

出力の2行目 0 false が、v.(int) を ok つきで試した結果です。string が入っているので合わず、n は int のゼロ値の 0 になり、プログラムは次の行へ進んでいます。

最後の n2 := v.(int) は ok を使っていないので、ここで止まりました。

panic: interface conversion: interface {} is string, not int

メッセージは「interface の中身は string で、int ではない」という意味です。interface {} の部分には変数の interface 型が入ります(any は interface{} の別名なので、このように表示されます)。

v, ok形式を基本にする

場面 おすすめの形
外から来た値(JSONを読んだ結果、map[string]any の値など) v, ok := x.(T) で確かめる
直前の処理で型が確定している x.(T) でもよい
複数の型がありうる 型スイッチ
あるメソッドを持つかどうかを知りたい s, ok := x.(fmt.Stringer) のように interface へアサーション

if s, ok := v.(string); ok { ... } の形にすると、s と ok の範囲が if の中に収まり、取り出しと分岐を1行で書けます。

型スイッチで型ごとに処理を分ける

型スイッチは、型アサーションを複数の型に対してまとめて書く構文です。 switch x := v.(type) と書くと、case ごとに x がその型の値として使えます。

package main

import (
    "errors"
    "fmt"
)

type Shape interface{ Area() float64 }
type Rect struct{ W, H float64 }

func (r Rect) Area() float64 { return r.W * r.H }

func kind(v any) string {
    switch x := v.(type) {
    case nil:
        return "nil"
    case int:
        return fmt.Sprintf("int(2倍すると %d)", x*2)
    case string:
        return fmt.Sprintf("string(長さ %d)", len(x))
    case bool:
        return fmt.Sprintf("bool(%t)", x)
    case int64, float64:
        return fmt.Sprintf("int64 か float64(%v)", x) // 複数の型を並べると x は any のまま
    case []string:
        return fmt.Sprintf("[]string(要素 %d 個)", len(x))
    case error:
        return "error(" + x.Error() + ")"
    case Shape:
        return fmt.Sprintf("Shape(面積 %.1f)", x.Area())
    case fmt.Stringer:
        return "Stringer(" + x.String() + ")"
    default:
        return fmt.Sprintf("その他(%T)", x)
    }
}

func main() {
    values := []any{42, "go", true, 3.14, int64(7), []string{"a", "b"},
        errors.New("not found"), Rect{2, 3}, nil, map[string]int{}}
    for _, v := range values {
        fmt.Println(kind(v))
    }
}

実行結果:

int(2倍すると 84)
string(長さ 2)
bool(true)
int64 か float64(3.14)
int64 か float64(7)
[]string(要素 2 個)
error(not found)
Shape(面積 6.0)
nil
その他(map[string]int)
書き方 動き
case int: x は int。x*2 のように計算できる
case int64, float64: 複数の型を並べると、x は元の interface 型のまま
case nil: interface に何も入っていないとき
case error:、case Shape: interface も case に書ける。そのメソッドを持つ値が当てはまる
default: どれにも合わないとき。%T で型を表示すると調べやすい

caseは上から順に調べられる

型スイッチは上から順に調べ、最初に合った case に進みます。出力の4〜5行目を見ると、3.14(float64)と int64(7) はどちらも case int64, float64 に進んでいます。

interface を case に書くときは、順番に気を配ります。具体的な型を上に、interface を下に置くと、意図どおりに分かれます。たとえば Rect に String() も定義した場合、case fmt.Stringer を case Shape より上に書くと、Rect は Stringer の方に進みます。

なお、型スイッチの x := v.(type) という書き方は switch の中だけで使えます。.(type) を通常の式として書くことはできません。

anyは「何でも入る」interface

any は、メソッドを1つも求めない interface(interface{})の別名です。 メソッドを求めないので、すべての型が満たします。Go 1.18 で導入され、それ以前のコードでは interface{} と書かれています。意味は同じです。

package main

import (
    "fmt"
    "strings"
)

func show(v any) {
    fmt.Printf("値=%v 型=%T\n", v, v)
}

func main() {
    var a any
    fmt.Println(a == nil)
    a = 1
    show(a)
    a = "文字列も入る"
    show(a)
    a = []int{1, 2}
    show(a)

    // interface{} と any は同じ型
    var b interface{} = 10
    var c any = b
    fmt.Println(c == b)

    // いろいろな型をまとめて持つ
    row := map[string]any{"id": 1, "name": "ペン", "tags": []string{"文具"}, "price": 120.5}
    if name, ok := row["name"].(string); ok {
        fmt.Println(strings.ToUpper("name=") + name)
    }
    price := row["price"].(float64)
    fmt.Println(price * 2)

    // any 同士の比較
    var x, y any = 1, 1
    var z any = int64(1)
    fmt.Println(x == y, x == z)
}

実行結果:

true
値=1 型=int
値=文字列も入る 型=string
値=[1 2] 型=[]int
true
NAME=ペン
241
true false
出力 わかること
true 何も入れていない any は nil
値=1 型=int など3行 同じ変数に、数値・文字列・スライスを入れ直せる
true(5行目) interface{} の値を any の変数にそのまま代入でき、比較もできる
NAME=ペン map[string]any から取り出した値は、型アサーションで string に戻して使う
241 row["price"].(float64) で取り出してから計算
true false any 同士の == は型と値の両方を比べる。int の 1 と int64 の 1 は等しくない

anyのままではできないこと

any に入れた値は、取り出すまで元の型の操作ができません。 コンパイルエラーで確かめます。

package main

import "fmt"

func main() {
    var a any = 10
    fmt.Println(a + 1)
    var s string = a
    nums := []int{1, 2, 3}
    var list []any = nums
    fmt.Println(s, list)
}

実行結果(コンパイルエラー):

# command-line-arguments
./main.go:7:14: invalid operation: a + 1 (mismatched types any and untyped int)
./main.go:8:17: cannot use a (variable of interface type any) as string value in variable declaration: need type assertion
./main.go:10:19: cannot use nums (variable of type []int) as []any value in variable declaration
エラー文 意味 直し方
invalid operation: a + 1 (mismatched types any and untyped int) any のままでは足し算できない a.(int) + 1(または v, ok 形式で取り出す)
cannot use a (variable of interface type any) as string value ...: need type assertion any を string の変数に直接入れられない s, ok := a.(string)
cannot use nums (variable of type []int) as []any value []int は []any ではない for で1つずつ append して []any を作る

2つ目のメッセージは、末尾で need type assertion(型アサーションが必要) と直し方まで教えてくれます。3つ目は見落としやすい点で、int は any に入りますが、[]int というスライス全体を []any として渡すことはできません。 要素を1つずつ入れ直します。

anyを使う場面

場面 例
本当にどんな型でも受け取る関数 fmt.Println(a ...any)
形が決まっていないデータ JSONを map[string]any に読み込む
型の異なる値をまとめて持つ []any{1, "a", true}

形がわかっているデータは、構造体や具体的な型で受け取る方が、コンパイル時に確かめられる範囲が広がります。any は「どうしても型を決められない場所」に絞って使うと、型アサーションの数も減ります。

nilポインタを入れたinterfaceは nil と等しくならない

interface は「型と値の両方が無いとき」だけ nil と等しくなります。 nil のポインタを interface に入れると、値は nil でも型の情報が付くため、== nil が false になります。エラーを返す関数で出会いやすい現象なので、出力つきで再現します。

package main

import "fmt"

type NotFoundError struct{ Key string }

func (e *NotFoundError) Error() string { return e.Key + " が見つかりません" }

// うまくいかない例:*NotFoundError 型の変数をそのまま返す
func findBad(key string) error {
    var err *NotFoundError // nil ポインタ
    if key == "" {
        err = &NotFoundError{Key: "(空)"}
    }
    return err // 型情報 *NotFoundError つきで interface に入る
}

// 直した例:何もなければ nil そのものを返す
func findGood(key string) error {
    if key == "" {
        return &NotFoundError{Key: "(空)"}
    }
    return nil
}

func main() {
    err := findBad("pen")
    fmt.Println("findBad :", err == nil)
    fmt.Printf("  型=%T 値=%v\n", err, err)
    if err != nil {
        fmt.Println("  エラーなしのはずが、エラー扱いの分岐に入った")
    }

    err = findGood("pen")
    fmt.Println("findGood:", err == nil)
    fmt.Printf("  型=%T 値=%v\n", err, err)

    // 仕組みを直接見る
    var p *NotFoundError
    var e error
    fmt.Println(p == nil, e == nil)
    e = p
    fmt.Println(p == nil, e == nil)
    if pe, ok := e.(*NotFoundError); ok {
        fmt.Println("中のポインタは nil か:", pe == nil)
    }
}

実行結果:

findBad : false
  型=*main.NotFoundError 値=<nil>
  エラーなしのはずが、エラー扱いの分岐に入った
findGood: true
  型=<nil> 値=<nil>
true true
true false
中のポインタは nil か: true

findBad("pen") はエラーを返していないつもりですが、err == nil は false になり、エラー扱いの分岐に入りました。%T で見ると、型が *main.NotFoundError と付いています。一方、findGood が返した値は型も値も <nil> で、err == nil は true です。

後半の4行が仕組みそのものです。

コード 出力 説明
var p *NotFoundError、var e error true true ポインタも interface も nil
e = p のあと true false p は nil のまま。e は「型 *NotFoundError、値 nil」になり nil と等しくない
pe, ok := e.(*NotFoundError) 中のポインタは nil か: true 取り出したポインタは nil
flowchart TB
    subgraph g1["return nil(findGood)"]
        A1["型: なし"]
        A2["値: なし"]
    end
    subgraph g2["return err(err は nil の *NotFoundError)"]
        B1["型: *NotFoundError"]
        B2["値: nil"]
    end
    g1 --> R1["err == nil は true"]
    g2 --> R2["err == nil は false"]

Go公式FAQでも、interface の値は型 T と値 V の組であり、両方が設定されていないときだけ nil になること、*int の nil ポインタを入れると(T=*int、V=nil)となって nil ではなくなることが説明されています。

直し方:nilそのものを返す

直す前 直した後
具体的なポインタ型の変数を宣言し、最後にそれを return する エラーが無いときは return nil と書く
戻り値の型を *NotFoundError にして、呼び出し側で error に代入する 戻り値の型を最初から error にする

「エラーを返す関数の戻り値は error 型にし、エラーが無いときは nil と直接書く」と決めておくと、この現象には出会いません。

nilのinterfaceからメソッドを呼ぶと止まる

逆に、何も入れていない interface からメソッドを呼ぶとpanicになります。

package main

import "fmt"

type Speaker interface{ Speak() string }

func main() {
    var s Speaker
    fmt.Println(s == nil)
    fmt.Println(s.Speak())
}

実行結果:

true
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x499e2b]

goroutine 1 [running]:
main.main()
    /home/taro/sample/main.go:10 +0x4b
exit status 2

interface の変数は、宣言しただけでは中身がありません。呼ぶ前に値を入れるか、if s != nil で確かめます。

anyとジェネリクスの使い分け

any は「実行時に型を調べる」方法、ジェネリクスは「コンパイル時に型を決める」方法です。 2つの値の大きい方を返す関数を、両方の方法で書いて比べます。

package main

import (
    "cmp"
    "fmt"
)

// any 版:何でも受け取れるが、中で型を調べる必要がある
func MaxAny(a, b any) any {
    switch x := a.(type) {
    case int:
        if y, ok := b.(int); ok && y > x {
            return y
        }
        return x
    case string:
        if y, ok := b.(string); ok && y > x {
            return y
        }
        return x
    }
    return nil
}

// ジェネリクス版:型はコンパイル時に決まり、戻り値もその型
func Max[T cmp.Ordered](a, b T) T {
    if b > a {
        return b
    }
    return a
}

// 型制約としての interface
type Number interface {
    ~int | ~int64 | ~float64
}

func Sum[T Number](xs []T) T {
    var total T
    for _, x := range xs {
        total += x
    }
    return total
}

func main() {
    r := MaxAny(3, 7)
    fmt.Printf("%v (%T)\n", r, r)
    fmt.Println(r.(int) + 1)      // 使う側で型アサーションが必要
    fmt.Println(MaxAny(3, "7"))   // 型が混ざってもコンパイルは通る
    fmt.Println(MaxAny(1.5, 2.5)) // 対応していない型は nil

    g := Max(3, 7)
    fmt.Printf("%v (%T)\n", g, g)
    fmt.Println(g + 1) // そのまま計算できる
    fmt.Println(Max("apple", "banana"), Max(1.5, 2.5))

    fmt.Println(Sum([]int{1, 2, 3}), Sum([]float64{0.5, 1.5}))
}

実行結果:

7 (int)
8
3
<nil>
7 (int)
8
banana 2.5
6 2
出力 any 版 ジェネリクス版
7 (int) 戻り値の変数は any。中身が int 戻り値は int そのもの
8 r.(int) + 1 と型アサーションが必要 g + 1 とそのまま計算できる
3 MaxAny(3, "7") はコンパイルが通り、実行すると 3 が返る (下のコンパイルエラーを参照)
<nil> 対応を書いていない float64 は nil が返る Max(1.5, 2.5) は 2.5 が返る

MaxAny(3, "7") のように型が混ざった呼び出しは、any 版ではコンパイル時に気づけません。ジェネリクス版に同じ呼び出しを書くと、コンパイラが知らせてくれます。

package main

import (
    "cmp"
    "fmt"
)

func Max[T cmp.Ordered](a, b T) T {
    if b > a {
        return b
    }
    return a
}

type Number interface {
    ~int | ~float64
}

func main() {
    fmt.Println(Max(3, "7"))
    var n Number
    fmt.Println(n)
}

実行結果(コンパイルエラー):

# command-line-arguments
./main.go:20:21: in call to Max, mismatched types untyped int and untyped string (cannot infer T)
./main.go:21:8: cannot use type Number outside a type constraint: interface contains type constraints

1つ目は、2つの引数の型がそろっていないので T を決められないというメッセージです。2つ目は、~int のような型の並びを含む interface は型制約としてだけ使え、変数の型には使えないことを示しています。Goの interface には、この記事の前半で見た「メソッドの約束」としての使い方と、ジェネリクスの「型制約」としての使い方の2つがあります。

比較表

比べる点 any ジェネリクス
型が決まる時点 実行時 コンパイル時
型が合わないとき 実行時にpanic、または ok=false コンパイルエラー
戻り値を使うとき 型アサーションが必要 そのまま使える
1つのスライスに違う型を混ぜる できる([]any) できない([]T は1つの型)
新しい型への対応 関数の中に case を足す 型制約を満たせばそのまま使える
導入されたバージョン Go 1.18(interface{} は最初から) Go 1.18
向いている場面 形が決まっていないデータ、型の違う値の入れ物 同じ処理を複数の型に使う関数やデータ構造

どれを選ぶかの判断表

やりたいこと 選ぶもの 例
型ごとに「ふるまい」が違うものを同じように扱いたい メソッドを持つ interface Shape、io.Writer、error
同じ処理を、いくつかの型にそのまま使いたい ジェネリクス Max、Sum、slices.Contains
型のわからないデータをいったん受け取りたい any と型アサーション・型スイッチ JSONの読み込み、ログの引数
型の違う値を1つのスライスやmapに入れたい any、または共通のメソッドを持つ interface []any、[]Shape

迷ったときは「メソッドで表せるか」を先に考えます。 Area() のように共通の操作があるなら、メソッドを持つ interface が最も読みやすくなります。操作ではなく「型だけが違って処理は同じ」ならジェネリクス、どちらでも表せないときに any を使う、という順番です。

つまずきやすい点のチェックリスト

症状・エラー文 原因 対応
does not implement X (missing method Y) メソッドが足りない、または名前・引数・戻り値が違う 約束どおりのメソッドを定義する
does not implement X (method Y has pointer receiver) ポインタレシーバのメソッドを持つ型を、値のまま入れている &v を渡す
panic: interface conversion: interface {} is string, not int ok なしの型アサーションで型が合わなかった v, ok := x.(T) にする
need type assertion any を具体的な型の変数に直接入れた 型アサーションで取り出す
cannot use ... as []any value []int を []any として渡した 要素を1つずつ入れ直す
エラーが無いのに err != nil になる nil ポインタを error に入れて返している return nil と直接書く
invalid memory address or nil pointer dereference 何も入っていない interface からメソッドを呼んだ 値を入れる、!= nil を確かめる
cannot use type X outside a type constraint 型の並びを含む interface を変数の型に使った 型制約として [T X] の形で使う
型スイッチで想定と違う case に進む 上にある interface の case が先に合っている 具体的な型を上、interface を下に並べる

よくある質問

Q. interface{} と any はどちらを書けばよいですか? A. 意味は同じなので、Go 1.18 以降で書くコードは短い any で統一すると読みやすくなります。既存のコードに interface{} があっても動きは変わりません。panic のメッセージなどでは interface {} と表示されます。

Q. 型アサーションと型変換は何が違いますか? A. 型変換 float64(n) は、値を別の型の値に作り替えます。型アサーション v.(T) は、interface の中に入っている値を、入れたときの型のまま取り出します。any に入れた int を v.(float64) で取り出すことはできず、float64(v.(int)) のように、取り出してから変換します。

Q. interfaceはいつ定義すればよいですか? A. 「同じように扱いたい型が2つ以上出てきたとき」が目安です。Goは暗黙の実装なので、あとから interface を定義しても、既存の型を書き換えずにそのまま当てはめられます。最初は具体的な型で書き、必要になった時点で使う側に小さな interface を定義する進め方が合っています。

Q. ある型がinterfaceを満たしているか、動かす前に確かめるには? A. var _ インターフェース名 = (*型名)(nil) をパッケージの上の方に1行書きます。値レシーバで満たす型なら var _ インターフェース名 = 型名{} です。満たしていなければコンパイルエラーになるので、メソッドの変更で約束から外れたときにすぐ気づけます。

まとめ

  • interface は「このメソッドを持つ」という約束。メソッドがそろえば宣言なしで満たす(暗黙の実装)
  • interface の値は「型」と「値」の組。%T で中の型を確かめられる
  • 型アサーションは v, ok := x.(T) を基本にする。ok なしで型が合わないと panic: interface conversion
  • 型スイッチは複数の型で処理を分けるときに使う。具体的な型を上、interface を下に並べる
  • any は interface{} の別名。取り出すまで元の型の操作はできない
  • nil ポインタを入れた interface は nil と等しくならない。エラーが無いときは return nil と直接書く
  • 「ふるまい」が違うならメソッドを持つ interface、処理が同じならジェネリクス、型がわからないデータは any

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Go interfaceの使い方|型アサーション・型スイッチ・anyを図解」

株式会社RJCでは、Goのinterfaceの使い方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?