この記事ではGo言語において数値の入ったinterface型変数を整数型に変換するコードを紹介します。ジェネリクスを利用することで、関数一つで多くの整数型(int64, uint32など)へのキャストが可能になります。コードは自由に利用、改変、再配布していただいて構いませんが、正しい挙動は保証しません。また、float型からの変換は丸め誤差によって異なる値に変換される場合がありますので注意してください。
どういう時に必要か
まず、Go言語のinterface型は他の言語のany型のようになんでも代入することができる型です。なの、interface型のエイリアスとして、1.18からはanyが追加されていますので、そちらに読み替えてもらっても問題ないです。
Go言語の型システムはそこそこ厳しく、interface型からキャストする際に実態の型が判明している必要があります。
逆にいうと、実態を知っていれば型アサーションを利用してinterface型からその型に変換することができます。
例えば、以下のコードでma["num"]は確定でfloat64型になります。なぜならjson.Unmarshal関数ではjsonをinterface型へ変換する場合、数値はfloat64として保持されるからです。
参考: https://pkg.go.dev/encoding/json#Unmarshal
var ma map[string]interface{}
json.Unmarshal([]byte("{\"num\": 10}"), &ma)
fmt.Println(ma["num"], reflect.TypeOf(ma["num"])) // 10 float64
ということで、interface型変数の実際の型が事前に確定しているのであれば、この記事で紹介する関数は必要ありません。
ですが、実行まで型が確定しないという状況があります。例えば、以下の例ではxはfloat64とint64のどちらの可能性もあります。
var x any
if rand.Int()%2 == 0 {
x = float64(10.0)
} else {
x = int64(10)
}
コード
一言で書くと、型swtichで入力された値の型ごとに処理を分けて範囲チェックを行ったのちにキャストして返します
作成したコードは以下のようになります。
func InterfaceToInteger[T constraints.Signed | constraints.Unsigned](i interface{}) (T, error) {
var zero T
if i == nil {
return zero, nil
}
var maxValue = ^T(0)
var isSigned = maxValue < 0
if isSigned {
maxValue ^= 1 << (unsafe.Sizeof(maxValue)*8 - 1)
}
switch v := i.(type) {
case uint64:
if v > uint64(maxValue) {
return zero, fmt.Errorf("value out of range: %d", v)
}
return T(v), nil
case uint32:
if uint64(v) > uint64(maxValue) {
return zero, fmt.Errorf("value out of range: %d", v)
}
return T(v), nil
case uint16:
if uint64(v) > uint64(maxValue) {
return zero, fmt.Errorf("value out of range: %d", v)
}
return T(v), nil
case uint8:
if uint64(v) > uint64(maxValue) {
return zero, fmt.Errorf("value out of range: %d", v)
}
return T(v), nil
case uint:
if uint64(v) > uint64(maxValue) {
return zero, fmt.Errorf("value out of range: %d", v)
}
return T(v), nil
case int64:
if uint64(Abs(v)) > uint64(maxValue) || (!isSigned && v < 0) {
return zero, fmt.Errorf("value out of range: %d", v)
}
return T(v), nil
case int32:
if int64(Abs(v)) > int64(maxValue) || (!isSigned && v < 0) {
return zero, fmt.Errorf("value out of range: %d", v)
}
return T(v), nil
case int16:
if int64(Abs(v)) > int64(maxValue) || (!isSigned && v < 0) {
return zero, fmt.Errorf("value out of range: %d", v)
}
return T(v), nil
case int8:
if int64(Abs(v)) > int64(maxValue) || (!isSigned && v < 0) {
return zero, fmt.Errorf("value out of range: %d", v)
}
return T(v), nil
case int:
if int64(Abs(v)) > int64(maxValue) || (!isSigned && v < 0) {
return zero, fmt.Errorf("value out of range: %d", v)
}
return T(v), nil
case float64:
if Abs(v) > float64(maxValue) || (!isSigned && v < 0) {
return zero, fmt.Errorf("value out of range: %f", v)
}
return T(v), nil
case float32:
if float64(Abs(v)) > float64(maxValue) || (!isSigned && v < 0) {
return zero, fmt.Errorf("value out of range: %f", v)
}
return T(v), nil
default:
return zero, fmt.Errorf("unsupported type: %T", i)
}
}
func Abs[T ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~float32 | ~float64](x T) T {
if x < 0 {
return -x
}
return x
}
やっていることとしては、
- interface型変数
iを取得し、nilなら0を返す - 変更先の型Tの最大値を
maxValueに格納する iの型に応じて範囲チェックを行い、キャストして返す。ここでは type switch を利用している。
汎用性を高めるためにジェネリクスを使っています。これにより、一つの関数で様々な整数型へのキャストができるようになっています。コードではT ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64となっていますが、constraintsパッケージを利用するなら、T constraints.Signed | constraints.Unsignedとしても良いでしょう。その場合、AbsについてはT constraints.Signed | constraints.Floatですね。
また、以下の部分についてはunsafeを使わず、最大値を定数として定義しても良いかもしれません。
if isSigned {
maxValue ^= 1 << (unsafe.Sizeof(maxValue)*8 - 1)
}
一通りテストも書いてみました。
package main
import (
"testing"
)
func TestInterfaceToIntegerAllTypes(t *testing.T) {
tests := []struct {
Name string
input interface{}
expected interface{}
hasError bool
}{
// uint64への変換
{Name: "uint64 input as uint64", input: uint64(12345), expected: uint64(12345), hasError: false},
{Name: "uint32 input as uint64", input: uint32(54321), expected: uint64(54321), hasError: false},
{Name: "int64 input as uint64", input: int64(1000000000), expected: uint64(1000000000), hasError: false},
{Name: "float64 input as uint64", input: float64(1145141919811), expected: uint64(1145141919811), hasError: false},
{Name: "float32 input as uint64", input: float32(3.14), expected: uint64(3), hasError: false},
{Name: "negative int input as uint64", input: int(-100), expected: uint64(0), hasError: true},
{Name: "negative float64 input as uint64", input: float64(-123.45), expected: uint64(0), hasError: true},
{Name: "string input as uint64", input: "string", expected: uint64(0), hasError: true},
// int64への変換
{Name: "uint64 input as int64", input: uint64(12345), expected: int64(12345), hasError: false},
{Name: "int32 input as int64", input: int32(54321), expected: int64(54321), hasError: false},
{Name: "float64 input as int64", input: float64(987654321), expected: int64(987654321), hasError: false},
{Name: "negative int input as int64", input: int(-100), expected: int64(-100), hasError: false},
{Name: "negative float64 input as int64", input: float64(-123.45), expected: int64(-123), hasError: false},
// uint32への変換
{Name: "uint16 input as uint32", input: uint16(12345), expected: uint32(12345), hasError: false},
{Name: "int8 input as uint32", input: int8(127), expected: uint32(127), hasError: false},
{Name: "negative int input as uint32", input: int(-100), expected: uint32(0), hasError: true},
{Name: "large float64 input as uint32", input: float64(4000000000), expected: uint32(4000000000), hasError: false},
// int32への変換
{Name: "uint8 input as int32", input: uint8(255), expected: int32(255), hasError: false},
{Name: "int16 input as int32", input: int16(-12345), expected: int32(-12345), hasError: false},
{Name: "float32 input as int32", input: float32(21474836), expected: int32(21474836), hasError: false},
{Name: "large float64 input as int32", input: float64(2147483648), expected: int32(0), hasError: true},
{Name: "string input as int32", input: "string", expected: int32(0), hasError: true},
}
for _, tc := range tests {
t.Run(tc.Name, func(t *testing.T) {
t.Parallel()
var err error
var result interface{}
switch tc.expected.(type) {
case uint64:
result, err = InterfaceToInteger[uint64](tc.input)
case int64:
result, err = InterfaceToInteger[int64](tc.input)
case uint32:
result, err = InterfaceToInteger[uint32](tc.input)
case int32:
result, err = InterfaceToInteger[int32](tc.input)
default:
t.Fatalf("Unexpected type: %T", tc.expected)
}
if tc.hasError {
if err == nil {
t.Errorf("Expected an error but got none")
}
} else {
if err != nil {
t.Errorf("Unexpected error: %v", err)
} else if result != tc.expected {
t.Errorf("Unexpected result: got %v, want %v", result, tc.expected)
}
}
})
}
}
使用例
最後に使用例を乗っけて終わります。以下はuint64をint32に変換しています。
var x interface{}
x = uint64(12345)
var y = InterfaceToInteger[int32](x)
fmt.Println(y, reflect.TypeOf(y)) // 12345 int32








