Help us understand the problem. What is going on with this article?

Go言語の型とreflect

More than 1 year has passed since last update.

最近型ではまることがたびたびあったのでまとめてみました。

主に下記についてまとめています。

  • 型とインターフェース
  • 代入のルール
  • 型の変換
  • 型アサーション
  • reflectパッケージの使い方

インターフェースの実装などについては触れていません。

型とインターフェース

名前を持つ型と名前を持たない型

型には名前のある型(named types)とない型(unnamed types)がある。
代入の可否など名前の有無で動作に違いのあるケースがある。

ざっくりいえば、英数字で表されているものが名前のある型、括弧やアスタリスクなどの記号を使って表されている型が名前のない型

名前のある型

下記のようにtypeを使って宣言されている型は名前のある型。

type MyInt int  // int型をもとにしてMyIntと言う名前の型を宣言した

boolやintなどは事前に宣言されている名前のある型。
事前に宣言されている名前の型は以下の通り。

bool byte complex64 complex128 error float32 float64
int int8 int16 int32 int64 rune string
uint uint8 uint16 uint32 uint64 uintptr

名前のない型

[]byte*int など型の名前と記号を組み合わせて表現されている型は名前のない型。このような型の記述は型リテラル(type literal)と呼ばれている。

次のようなものは名前のない型。

[3]byte
map[string]string
interface{}

宣言により名前のない型に名前をつけることができる。

// *intをもとにして宣言したptrintは、ptrintという名前の名前のある型
type ptrint *int

基礎型

それぞれの型は基礎型(underlying type)を持っている。boolやintのような事前に宣言されている型や型リテラルの基礎型はその型自身。それ以外の場合、基礎型は宣言時に指定された型の基礎型となる。

type T1 string //T1の基礎型はstring
type T2 T1     //T2の基礎型はT1の基礎型のstring
type T3 []T1   //T3の基礎型は[]T1の基礎型の[]T1([]T1は型リテラルのため基礎型はそれ自身)
type T4 T3     //T4の基礎型はT3の基礎型の[]T1

変数の型とインターフェース

varを使って変数の名前とその型を宣言することができる。
宣言時に指定された型を静的な型(static type)または単にという。

後述するように、基本的にはある型の変数に値を代入する場合、変数の型と値の型が一致している必要がある。

var n int    // nの(静的な)型はint
var s string // sの(静的な)型はstring

n = 42 // int型に整数は代入できる
s = 42 // string型に整数は代入できない

一つの変数にいろいろ型の値を代入できると便利な場合がある。例えば、 fmt.Println は文字列だけでなく数値なども引数にとることができ、文字列以外の値を直接使用することができる。

fmt.Println("Hello") // Hello
fmt.Println(10)      // 10

fmt.Printlnの定義を見ると引数の型が interface{} というインターフェース型となっている。

func Println(a ...interface{}) (n int, err error)

このようにインターフェース型の変数にはいろいろな型の値を格納することができる。

インターフェース型の変数は静的な型(インスターフェース型)に加えて、格納されている値によって動的な型(dynamic type)を持つ。動的な型は実行中に変化する可能性がある。

var n int = 42         // nの(静的な)型はint
var s string = "hello" // sの(静的な)型はstring
var v interface{}      // vの(静的な)型はinterface{}
v = n                  // vの(静的な)型はinterface{}、動的な型はint
v = s                  // vの(静的な)型はinterface{}、動的な型はstring

reflectを使って型についての情報を得る

reflectパッケージを使用すると以下の例のように変数の型についての情報を取得することができる。

Typeメソッドで型の名前、Kindメソッドで値の種類を取得することができる。

http://play.golang.org/p/H1x4mLqPlR

package main

import (
    "fmt"
    "reflect"
)

func main() {
    type MyInt int
    var x MyInt
    v := reflect.ValueOf(x) // ValueOfでreflect.Value型のオブジェクトを取得
    fmt.Println(v.Type())   // Typeで変数の型を取得
    fmt.Println(v.Kind())   // Kindで変数に格納されている値の種類を取得
}

// output
// main.MyInt
// int

インターフェース型の変数にreflectを使用した場合には動的な型の情報を取得できる。

値を格納していない場合にはTypeメソッドを含めほとんどのメソッドがpanicを起こすので注意が必要。値を格納しているか否かはIsValidメソッドでチェックできる。

インターフェース型の変数に値が格納されていないことと、インターフェース型にnilが格納されていること(下記のようにnilポインタが格納されている場合など)は別なので注意。

http://play.golang.org/p/byqXAQhqri

package main

import (
    "fmt"
    "reflect"
)

func main() {
    type MyInt int
    var x MyInt
    var v interface{}
    var refv reflect.Value

    refv = reflect.ValueOf(v)
    fmt.Println("v:", v)
    fmt.Println("IsValid:", refv.IsValid()) // false
    // fmt.Println("IsNil:", refv.IsNil())  // panic
    // fmt.Println("Type:", refv.Type())    // panic
    fmt.Println("Kind:", refv.Kind())       // invalid
    fmt.Println()

    v = nil
    refv = reflect.ValueOf(v)
    fmt.Println("v:", v)
    fmt.Println("IsValid:", refv.IsValid()) // false
    // fmt.Println("IsNil:", refv.IsNil())  // panic
    // fmt.Println("Type:", refv.Type())    // panic
    fmt.Println("Kind:", refv.Kind())       // invalid
    fmt.Println()

    x = 42
    v = x
    refv = reflect.ValueOf(v)
    fmt.Println("v:", v)
    fmt.Println("IsValid:", refv.IsValid()) // true
    // fmt.Println("IsNil:", refv.IsNil())  // panic
    fmt.Println("Type:", refv.Type())       // main.MyInt
    fmt.Println("Kind:", refv.Kind())       // int
    fmt.Println()

    v = &x
    refv = reflect.ValueOf(v)
    fmt.Println("v:", v)
    fmt.Println("IsValid:", refv.IsValid()) // true
    fmt.Println("IsNil:", refv.IsNil())     // false
    fmt.Println("Type:", refv.Type())       // *main.MyInt
    fmt.Println("Kind:", refv.Kind())       // ptr
    fmt.Println()

    var y *MyInt
    y = nil
    v = y
    refv = reflect.ValueOf(v)
    fmt.Println("v:", v)
    fmt.Println("IsValid:", refv.IsValid()) // true
    fmt.Println("IsNil:", refv.IsNil())     // true
    fmt.Println("Type:", refv.Type())       // *main.MyInt
    fmt.Println("Kind:", refv.Kind())       // ptr
    fmt.Println()
}

代入のルール

下記のいずれかの条件を満たしている場合に変数に値を代入できる。

  1. 変数と値の(静的な)型が一致している場合
  2. 変数と値の型の基礎型が一致しており、変数・値の型のうち少なくとも一方が名前のない型の場合
  3. 変数がインターフェース型で値の型がそのインターフェースを実装している場合
  4. 値が双方向のチャネルで変数がチャネル型で要素型が一致しており、変数・値の型のうち少なくとも一方が名前のない型の場合
  5. 変数がポインタ、関数、スライス、マップ、チャネル、インターフェースのいずれかで値がnilの場合
  6. 変数の値として表現可能な型なし定数を代入する場合

いくつか例を上げてみます。

整数を表す型同士でも異なる型であれば代入できない。
(byteとuint8, runeとint32は実際には同じ型のため互いに代入できる)

var i8 int8    
var i16 int16
i8 = 1
i16 = i8 // 型が一致していないため代入不可

基礎型が同じでも変数と値の型が異なっている場合には代入できない。

type MyInt1 int
type MyInt2 int

var m1 MyInt1 = 10
var m2 MyInt2

m2 = m1 // 基礎型は一致しているが、値と変数がどちらも名前を持つ型のため代入不可

値が代入先のインターフェース型を実装していれば代入可能。

var v interface{} // vは空のインターフェース型
var n int
n = 42
v = n             // 全ての型の値はinterface{}を実装しているので代入可能

名前のない型については、基礎型が一致していれば代入できる。

type Map1 map[string]string
type Map2 map[string]string

// 次の例は基礎型が一致しており、右辺は名前なし型(map[string]string)のため代入可能
var m1 Map1 = map[string]string{"a": "A", "b": "B"} 

// 逆にMap1型からmap[string]string型の変数にも代入可能。(左辺が名前なしのため)
var unmanned map[string]string = m1

// 値と変数がともに名前ありの場合には、基礎型が一致していても代入不可
var m2 Map2 = m1

Go言語には型がある定数とない定数がある。
型がある場合の代入のルールは通常の変数と同じ。型がない場合、定数が変数の型で表現可能であればその型として代入できる。

http://play.golang.org/p/hFOUEJ8DsL

import "fmt"

const (
    untypedInt     = 1 // 型がない定数
    typedInt   int = 1 // int型の定数
)

func main() {
    var i32 int32
    i32 = untypedInt // 型がない場合は代入可能
    // i32 = typedInt // int型の定数をint32型の変数に代入できない
    fmt.Println(i32)

    type MyBool bool
    var b bool
    var myb MyBool
    b = true
    // myb = b // MyBool型にbool型の変数は代入できない
    myb = (b == true) // 比較の結果は型がないブール値なので代入可能
    fmt.Println(myb)
}

型の変換と型アサーション

型を変換したい場合には、大きく分けて次の場合がある。

  • ある型を別の型へ変換する
    • 数値型の変換、[]byteとstringの変換など
  • インターフェース型から格納されている値の動的な型への変換

前者は型の変換で、後者は型アサーションで行うことができる。

型の変換

型の変換(conversion)は以下の記法で行うことができる。

型の名前(式)

以下のいずれかの条件を満たす場合に、値xを型Tに変換可能できる。

  • xがTの変数に代入可能な場合
  • xの型とTの基礎型が一致している場合
  • xの型とTが名前を持たないポインタ型であり、ポインタ型のベース型が一致している場合
  • xの型とTの両者が整数型または浮動小数点型の場合
  • xの型とTの両者が複素数型の場合
  • xの型が整数型かbyteかruneのスライスでTがstring型の場合
  • xの型がstringでTがbyteかruneのスライスの場合
var i8 int8    
var i16 int16
i8 = 1
i16 = int16(i8) // int8, int16はどちらも整数型なので変換可能
type MyInt1 int
type MyInt2 int

var m1 MyInt1 = 10
var m2 MyInt2

m2 = MyInt2(m1) // 基礎型が一致しているので変換可能
type MyInt1 int
type MyInt2 int
type PtrMyInt2 *MyInt2

var p1 *MyInt1 
var p2 *MyInt2
var p3 PtrMyInt2

x := MyInt1(42)    // xの型はMyInt1
p1 = &x            // &xとp1の型はどちらも*MyInt1なので代入可能
p2 = p1            // p1とp2は異なる型なので代入不可
p2 = (*MyInt2)(p1) // p1とp2はどちらも名前を持たないポインタ型の変数で、どちらもベース型はintなので変換すれば代入可能
p3 = PtrMyInt2(p1) // p3は名前を持つポインタ型なので変換不可

型アサーション

型アサーション(type assertion)と呼ばれる次の記法で、インターフェース型の変数xに格納されている値の型がTであることを確かめることができる。

v = x.(T)
v, ok = x.(T)

下記の例のように、インターフェース型で包まれている値の型を取り出すようなイメージ。

var v interface{}
var n1, n2 int
n1 = 42
v = n1       // interface{}型の変数にはint型の値を代入できる
// n2 = v    // int型の変数にinterface{}型の値は代入できない
n2 = v.(int) // 型アサーションによってint型の値が格納されていることを示せば代入できる

型アサーションの成功条件は下記の通り。

  • Tがインターフェース型でなく、xの動的な型がTである場合
  • Tがインターフェース型であり、xの動的な型がTを実装している場合

型アサーションが成功するとxに格納されている値が型Tとして返る。

失敗した場合には、v = x.(T) の記法の場合panicを起こす。v, ok = x.(T) の記法の場合にはvがT型のゼロ値、okがfalseとなる。

var v interface{}
var n int
var s string
var ok bool

n = 42
v = n

// s = v.(string) // 型アサーションが失敗するのでpanicを起こす
s, ok = v.(string)
fmt.Println("s =", s)   // sは空文字列
fmt.Println("ok =", ok) // ok = false

次のようにswitchステートメント(型switch)を使用して型により処理を分岐させることができる

var v interface{}
v = 10

switch x := v.(type) {
    case int:
        fmt.Println("int", x)    // int 10
    case string:
        fmt.Println("string", x)
    default:
        fmt.Println("other type", x)
}

型アサーションと型の変換の違い

型アサーションと変換はどちらも(静的な)型を変えると言う意味では似ているが全く別の操作。次の例でわかるように、型アサーションによって型の変換を行うことはできない。

var v interface{}
var i8 int8
var i16 int16
i8 = 10
v = i8                 // vの型はinterface{}、動的な型はint8

i16 = v.(int16)          // vの動的な型(int8)とint型が一致しないため型アサーションに失敗
i16 = int16(v)           // interface{}型とint16型は変換不可なので失敗
i16 = int16(v.(int8))     // 型アサーションでint8型にすれば、int16型に変換して代入可能

reflectパッケージを用いた型の変換

型アサーションを使えばインターフェース型の変数に格納された値の型を動的な型に変えることができます。しかし、型アサーションの書き方から分かるように、型アサーションするためには格納されている可能性のある動的な型があらかじめ分かっている必要があります。

例えば、下記のような2つの数値を表す型が引数でfloat64で合計を返す関数を実装しようとした場合、MyIntのように独自に定義された型についてアサーションで対応することは困難です。

http://play.golang.org/p/ZVHm3UpB2D

package main

import (
    "fmt"
)

func toFloat64(v interface{}) float64 {
    switch x := v.(type) {
    case int8:
        return float64(x)
    case int16:
        return float64(x)
    default:
        return 0
    }
}

func add(a, b interface{}) float64 {
    return toFloat64(a) + toFloat64(b)
}

func main() {
    type MyInt int
    var i8 int8 = 8
    var i16 int16 = 16
    var f64 float64 = 64.0
    var myint = 42

    // fmt.Println(i8 + i16)  // int8とint16を直接足し合わせることはできない
    fmt.Println(add(i8, i16))   // 24
    fmt.Println(add(i8, f64))   // 8 float64には対応していない
    fmt.Println(add(i8, myint)) // 8 MyIntには対応していない
}

このような場合には、reflectパッケージを利用してインターフェース型の変数から値を取得することができます。

MyInt型の値が引数として渡された場合には次のように動作します。

  • MyInt型はint型をもとにしているのでKindはInt
  • KindがIntの場合、Intメソッドにより格納された値をint64型として取得できる
  • int64型はfloat64型へ変換可能

(Floatメソッドを使えれば楽だが、FloatメソッドはKindがFloat32かFloat64以外の場合panicを起こすので不可。)

http://play.golang.org/p/CC22V3lGdq

package main

import (
    "fmt"
    "reflect"
)

func toFloat64(v interface{}) float64 {
    r := reflect.ValueOf(v)
    if r.IsValid() {
        switch r.Kind() {
        case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
            return float64(r.Int()) // Intで取得したint64型をfloat64型に変換
        case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
            return float64(r.Uint()) // Uintで取得したuint64型をfloat64型に変換
        case reflect.Float32, reflect.Float64:
            return r.Float() // Floatはfloat64型を返すので変換不要
        default:
            return 0
        }
    }
    return 0
}

func add(a, b interface{}) float64 {
    return toFloat64(a) + toFloat64(b)
}

func main() {
    type MyInt int
    var i8 int8 = 8
    var i16 int16 = 16
    var f64 float64 = 64.0
    var myint = 42

    fmt.Println("Kind of myint:", reflect.ValueOf(myint).Kind()) // int

    // fmt.Println(i8 + i16)  // int8とint16を直接足し合わせることはできない
    fmt.Println(add(i8, i16))   // 24
    fmt.Println(add(i8, f64))   // 72
    fmt.Println(add(i8, myint)) // 50
}

また、MyInt型はfloat64型へ変換できる(型の変換を参照)ため、指定した型への型変換を行うConvertメソッドを使用して次のようにtoFloat関数を書くこともできます。Convertメソッドはreflect.Value型を返すので、いったんInterfaceメソッドでinteface{}型にしてから型アサーションでfloat64型にしています。

http://play.golang.org/p/ABnNZW2Ed_

func toFloat64(v interface{}) float64 {
    r := reflect.ValueOf(v)
    if r.IsValid() {
        switch r.Kind() {
        case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
            reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
            reflect.Float32, reflect.Float64:
            var x float64
            // return r.Convert(reflect.TypeOf(x)).Float() // この例ではこちらでもOK
            return r.Convert(reflect.TypeOf(x)).Interface().(float64)
        default:
            return 0

        }
    } 
    return 0
}

スライスやマップとインターフェース

要素として複数の型を格納できるスライスは次のように記述できる。

var vs []interface{}

ここで[]interface{}と[]float64型の間で変換する方法を考えてみる。vsそれ自体はインターフェースではないので、下記のようにそれぞれの要素毎に代入や型アサーションを行う必要がある。

http://play.golang.org/p/6G8b5NVXDZ

package main

import "fmt"

func main() {
    var vs []interface{}
    var xs, ys []float64

    xs = []float64{1.0, 2.0, 3.0}

    // vs = xs // vsはインターフェース型ではないので直接代入することはできない

    // vsの個々の要素はinterface{}型なのでfloat64の値を代入できる
    vs = make([]interface{}, len(xs))
    for i, x := range xs {
        vs[i] = x
    }

    // []interface{}から[]float64にする際もそれぞれの要素について型アサーションする必要がある
    ys = make([]float64, len(vs))
    for i, v := range vs {
        ys[i] = v.(float64)
    }

    fmt.Println("vs:", vs)
    fmt.Println("ys:", ys)
}

vsをinterface{}型とした場合には、要素ごとの操作は必要なく非常にシンプルになる。

http://play.golang.org/p/amqDaCj_A4

package main

import (
    "fmt"
    // "reflect"
)

func main() {
    var vs interface{}
    var xs, ys []float64

    xs = []float64{1.0, 2.0, 3.0}

    vs = xs // vsには全ての型の値を格納できる
    ys = vs.([]float64)

    fmt.Println("vs:", vs)
    fmt.Println("ys:", ys)
}

上記の例のように、返り値をインターフェース型の配列([]interface{}など)にするより、インターフェース型(interface{}など)にした方があとの処理が少なくて済む場合がある。

reflectには任意の型を指定してスライスやマップを作る関数が用意されている。

例えば、任意の要素xと整数nを引数にとり、要素xをn個並べたスライスを返す関数は以下のように書ける。

http://play.golang.org/p/zAmIifiOGt

package main

import (
    "fmt"
    "reflect"
)

func repeat(x interface{}, n int) interface{} {
    v := reflect.ValueOf(x)
    ys := reflect.MakeSlice(reflect.SliceOf(v.Type()), n, n) // 動的な型のスライスを作成
    for i := 0; i < n; i++ {
        ys.Index(i).Set(v) // ysとvはreflect.Value型なのでys[i]=vとはできない
    }
    return ys.Interface() // interface{}として返す
}

func main() {
    var xs []int = repeat(3, 10).([]int)
    fmt.Println(reflect.ValueOf(xs).Type(), xs)

    var ss []string = repeat("hello", 3).([]string)
    fmt.Println(reflect.ValueOf(ss).Type(), ss)

    type MyInt int
    var ms []MyInt = repeat(MyInt(5), 5).([]MyInt)
    fmt.Println(reflect.ValueOf(ms).Type(), ms)
}

// output
// []int [3 3 3 3 3 3 3 3 3 3]
// []string [hello hello hello]
// []main.MyInt [5 5 5 5 5]

参考資料

下記の記事のreflectionの説明が非常に分かりやすい

任意の型で使用できるmap関数の実装を例にしたreflectionの解説。コードが多く参考になる。

Go言語の代入や型の変換のルールは言語仕様をみるのがおすすめ。英語があまり苦でなければ、英語の方がわかりやすいと思います。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした