- Go言語
Go interfaceの使い方|型アサーション・型スイッチ・anyを図解
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期連続の黒字決算。