問題の発生
これはよくある罠です。[]string のスライスがあり、[]interface{} を受け取る関数があるとします。単一の string は interface{} に完全に適合するため、スライス版も同様に動作すると想定しがちです。しかし、Go コンパイラはこれを必ず拒否します。
データベースドライバ、JSON エンコーダ、またはカスタムログ用ラッパーにデータを渡す際に、この壁に突き当たることがあります。以下は、この不満を引き起こす典型的なコードスニペットです:
package main
import "fmt"
func process(items []interface{}) {
for _, item := range items {
fmt.Println(item)
}
}
func main() {
data := []string{"apple", "banana", "cherry"}
// ここでエラーが発生します:
// cannot use data (type []string) as type []interface {} in argument to process
process(data)
}
なぜ Go はこれを拒否するのか
Go は「魔法を使わない(no magic)」という哲学で有名です。string は interface{} を満たしますが、[]string のメモリレイアウトは []interface{} とは根本的に異なります。
このように考えてみてください。64ビットシステムでは、string ヘッダーは16バイト(データへのポインタと長さ)です。interface{} も16バイトですが、型ポインタとデータポインタで構成されています。内部構造が異なるため、Go は一方をもう一方として単純に扱うことができません。
もし Go がこの変換を自動的に行った場合、スライス全体をループして各要素を再割り当てする必要があります。これは O(n) の操作です。パフォーマンスを予測可能に保つため、Go は単純な代入の裏にこのコストを隠すのではなく、コード上でこのコストを明示的にさせるようにしています。
解決策1:「for ループ」による方法(標準的)
最も直接的な修正方法は、手動で新しいスライスを割り当てて要素をコピーすることです。これにより、新しいメモリの割り当てと CPU サイクルの消費というパフォーマンス上のコストが、コードを読む誰にとっても明白になります。
func main() {
data := []string{"apple", "banana", "cherry"}
// 正しい型の新しいスライスを割り当てる
interfaceSlice := make([]interface{}, len(data))
for i, v := range data {
interfaceSlice[i] = v
}
process(interfaceSlice)
}
解決策2:可変長引数
関数のシグネチャを変更できる場合は、可変長引数(...interface{})を使用します。これにより、要素を個別に渡すことができます。変換自体を回避できるわけではありませんが、少量のデータを扱う場合、呼び出し側のコードがすっきりすることがよくあります。
// []interface{} の代わりに ...interface{} を使用する
func process(items ...interface{}) {
for _, item := range items {
fmt.Println(item)
}
}
func main() {
data := []string{"apple", "banana", "cherry"}
// 引き続きスライスの手動変換は必要
vals := make([]interface{}, len(data))
for i, v := range data {
vals[i] = v
}
// スプレッド演算子を使用して要素を渡す
process(vals...)
}
解決策3:モダンな方法(ジェネリクス)
Go 1.18 以降を使用している場合、このために []interface{} を使用するのはやめましょう。ジェネリクスが優れた選択肢です。データを汎用的なインターフェースコンテナに強制的に入れる代わりに、関数が任意の型 T のスライスを受け取れるようにします。
// 任意の型 T のスライスを受け取るジェネリック関数
func process[T any](items []T) {
for _, item := range items {
fmt.Println(item)
}
}
func main() {
data := []string{"apple", "banana", "cherry"}
nums := []int{10, 20, 30}
// 手動変換は不要!
process(data)
process(nums)
}
検証とパフォーマンス
- **コンパイルチェック:** `go build` を実行します。エラーが消えれば、型が正しく調整されています。
- **メモリチェック:** 1,000,000個の文字列を含むスライスを変換する場合、解決策1ではインターフェーススライスのために約16MBの新しいメモリが割り当てられます。高頻度のループ内でこれが発生する場合は、ベンチマークを行ってください。
- **型安全性:** 解決策3(ジェネリクス)が最も安全です。元の型情報を保持しますが、`interface{}` では型アサーションを行うまでその情報は失われます。
重要なまとめ
- スライスは共変(covariant)ではありません。`T` が何であれ、`[]T` は決して `[]interface{}` ではありません。
- 明示的な変換は Go の機能であり、バグではありません。これにより、隠れた O(n) のパフォーマンス低下を防いでいます。
- モダンな Go プロジェクトで柔軟な関数を作成する場合、ジェネリクス(`[T any]`)を第一の選択肢にすべきです。

