imu-note
いむたろ
いむたろ
@imutaroh
新卒エンジニア / データ基盤 × AI

インタフェース

「このメソッドを持っていること」という条件の一覧。

type RowSink interface {
	Write(row string) error
}
  • 「この型は RowSink を実装する」という宣言は書かない。

取りに行く仕事を外に出す

// Goらしくない:ファイル名を受け取り、自分で開ける
func CountRowsFile(path string) (int, error)

// Goらしい:「読めるもの」を受け取る
func CountRows(r io.Reader) (int, error)

満たしているか確かめる

type StdoutSink struct{}

func (s *StdoutSink) Write(row string) error {
	fmt.Println(row)
	return nil
}

var _ RowSink = (*StdoutSink)(nil) // Write が足りなければ、ここでコンパイルエラー

any

メソッドを1つも要求しないインタフェース interface{} の別名。どんな値でも入る。

  • 原則は使わない。中に何が入っているか、コンパイラーが確かめてくれなくなる
  • 出番は、中身の形が前もってわからないとき。たとえば外部 API の JSON の項目が日によって増えるとき、ひとまず map[string]any で受ける
  • 形がわかっているなら構造体で受ける

型アサーション

any やインタフェースの箱から、中身を「これは〇〇のはず」と言って取り出す書き方。

var box any = "カレー"

s, ok := box.(string) // 当たり:s = "カレー", ok = true
n, ok := box.(int)    // 外れ:n = 0, ok = false。止まらずに進む
var box any = "カレー"

n := box.(int) // 1つで受けると、外れたら panic
// panic: interface conversion: interface {} is string, not int

外から届くデータは、ok 付きで受けて外れたときの処理を書く。

n, ok := box.(int)
if !ok {
	return fmt.Errorf("int ではない: %T", box)
}

候補が何種類もあるときは型スイッチ。各 case の中では、x がその型として使える。

func describe(v any) string {
	switch x := v.(type) {
	case string:
		return "文字列:" + x
	case float64:
		return fmt.Sprintf("数値:%g", x)
	default:
		return fmt.Sprintf("その他:%T", x)
	}
}

describe(7) // その他:int

Minutes(n) のような型変換とは別物。型変換は値を作り変える。型アサーションは何も作り変えず、中身が本当にその型か確かめて取り出すだけ。

どんなときに使うか

自分で設計するコードなら、ほとんど書かない。書くのは、相手が any やインタフェースの箱で渡してくるとき。

場面頻度
自分の設計で any を使うまれ。避ける
ライブラリが any を返してくる(BigQuery の bigquery.Value、形の決まらない JSON、ctx.Value)ときどき
エラーの種類で処理を分ける(errors.As)よくある
var syntaxErr *json.SyntaxError
if errors.As(err, &syntaxErr) {
	// JSON の書き方が壊れていたときだけの処理
}

errors.As は「この error の中身は *json.SyntaxError か」を確かめる関数で、やっていることは型アサーションと同じ。書く側に回ることは少ないので、読んだときに意味がわかれば足りる。

合成

インタフェースの中に別のインタフェース名を書くと、両方のメソッドを要求するインタフェースになる。

type Fetcher interface {
	Fetch(table string) ([]string, error)
}

type Loader interface {
	Load(table string, rows []string) error
}

type FetchLoader interface { // 合成
	Fetcher
	Loader
}

func Preview(f Fetcher, table string) string     // 読むだけ → Fetcher だけ要求する
func Copy(fl FetchLoader, from, to string) error // 読んで書く → 合成したものを要求する

実装の切り替え

同じ名前のメソッドを持つ型を何種類か作っておき、どれを使うかを外で選んで渡す。使う側は、どの型が来たかを知らないまま動く。

type fakeSink struct{
  got []string
}

func (f *fakeSink) Write(row string) error {
	f.got = append(f.got, row) // 受け取ったものを取っておく
	return nil
}

// 使う側は RowSink しか知らない
func Ingest(sink RowSink, rows []string) error {
	for _, r := range rows {
		err := sink.Write(r)
		if err != nil {
			return err
		}
	}
	return nil
}

Ingest(&StdoutSink{}, rows) // 本番:本当に書く
f := &fakeSink{}
Ingest(f, rows) // テスト:f.got に渡された行が残る

関連