CLOVER🍀

That was when it all began.

Goで変数の型の名前を取得したい

これは、なにをしたくて書いたもの?

Goでプログラムを書いていて、「この変数の型はなに?」みたいな時にどうやって型の情報を取得するんでしたっけ?ということで。

今の動機は、デバッグ時とかに型の名前が知りたい、くらいです。

fmtパッケージの%T書式か、reflectパッケージを使うことになるようです。

環境

今回の環境は、こちらです。

$ go version
go version go1.15.6 linux/amd64

動作確認用のモジュール。

$ go mod init show-type
go: creating new go.mod: module show-type

go.mod

module show-type

go 1.15

require github.com/stretchr/testify v1.7.0

動作確認には、testify/assertを使います。

go.sum

github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

テストコードの雛形

動作確認は、テストコードで行います。

雛形をこんな感じで用意。
show_type_test.go

package main

import (
    "container/list"
    "fmt"
    "github.com/stretchr/testify/assert"
    "os"
    "reflect"
    "testing"
)

// ここに、テストを書く!

では、確認していきましょう。

%T書式を使う

%T書式を使うことで、文字列として型の名前を得ることができます。

%T a Go-syntax representation of the type of the value

Package fmt / Printing

こんな感じですね。

func TestUsingStringFormatType(t *testing.T) {
    intVar := 10
    intArrayVar := [3]int{1, 2, 3}
    stringSliceVar := []string{"one", "two", "three"}
    stringIntMapVar := map[string]int{"key1": 1, "key2": 2, "key3": 3}

    assert.Equal(t, "int", fmt.Sprintf("%T", intVar))
    assert.Equal(t, "[3]int", fmt.Sprintf("%T", intArrayVar))
    assert.Equal(t, "[]string", fmt.Sprintf("%T", stringSliceVar))
    assert.Equal(t, "map[string]int", fmt.Sprintf("%T", stringIntMapVar))

    file, _ := os.Open("show_type_test.go")
    defer file.Close()
    list := list.New()

    assert.Equal(t, "*os.File", fmt.Sprintf("%T", file))
    assert.Equal(t, "*list.List", fmt.Sprintf("%T", list))
    assert.NotEqual(t, "*container.list.List", fmt.Sprintf("%T", list)) // Not *container.list.List
}

配列の要素数や、スライス、マップの型情報も得られるんですね。

   assert.Equal(t, "[3]int", fmt.Sprintf("%T", intArrayVar))
    assert.Equal(t, "[]string", fmt.Sprintf("%T", stringSliceVar))
    assert.Equal(t, "map[string]int", fmt.Sprintf("%T", stringIntMapVar))

パッケージ名も含めて得ることができるようですが

   assert.Equal(t, "*os.File", fmt.Sprintf("%T", file))
    assert.Equal(t, "*list.List", fmt.Sprintf("%T", list))

Javaの時のようなFQCN的な感じではないんですね。

   assert.NotEqual(t, "*container.list.List", fmt.Sprintf("%T", list)) // Not *container.list.List

パッケージの仕様を見てみます。

The Go Programming Language Specification / Packages

パッケージの定義。

PackageClause  = "package" PackageName .
PackageName    = identifier .

The Go Programming Language Specification / Package clause

パッケージ名が取りうる文字は、identifierということで。

identifier = letter { letter | unicode_digit } .

The Go Programming Language Specification / Identifiers

確かに、.とか入る余地がなさそうですね。

Effective Goを見てみます。

By convention, packages are given lower case, single-word names; there should be no need for underscores or mixedCaps.

And don't worry about collisions a priori. The package name is only the default name for imports; it need not be unique across all source code, and in the rare case of a collision the importing package can choose a different name to use locally. In any case, confusion is rare because the file name in the import determines just which package is being used.

Moreover, because imported entities are always addressed with their package name, bufio.Reader does not conflict with io.Reader.

Package names

パッケージは簡潔な1語で表現され、パッケージ内の要素にアクセスするにはパッケージ名を常に付与する必要があり、さらにパッケージ名は
importする時に別名を付けられるので仮に衝突しても問題ない、ということみたいですね。

%#v書式を使う

%#v書式を使うことで、型と値を得られるものもあります。

%#v a Go-syntax representation of the value

Package fmt / Printing

こんな感じです。

func TestUsingStringFormatTypeValue(t *testing.T) {
    intVar := 10
    intArrayVar := [3]int{1, 2, 3}
    stringSliceVar := []string{"one", "two", "three"}
    stringIntMapVar := map[string]int{"key1": 1, "key2": 2, "key3": 3}

    assert.Equal(t, "10", fmt.Sprintf("%#v", intVar))
    assert.Equal(t, "[3]int{1, 2, 3}", fmt.Sprintf("%#v", intArrayVar))
    assert.Equal(t, "[]string{\"one\", \"two\", \"three\"}", fmt.Sprintf("%#v", stringSliceVar))
    assert.Equal(t, "map[string]int{\"key1\":1, \"key2\":2, \"key3\":3}", fmt.Sprintf("%#v", stringIntMapVar))

    file, _ := os.Open("show_type_test.go")
    defer file.Close()
    list := list.New()

    assert.Contains(t, fmt.Sprintf("%#v", file), "&os.File{file:(*os.file)")
    assert.Contains(t, fmt.Sprintf("%#v", list), "&list.List{root:list.Element")
}

intは、値そのものが出ています。

   assert.Equal(t, "10", fmt.Sprintf("%#v", intVar))

配列やスライス、マップは型と値が出力されます。

   assert.Equal(t, "[3]int{1, 2, 3}", fmt.Sprintf("%#v", intArrayVar))
    assert.Equal(t, "[]string{\"one\", \"two\", \"three\"}", fmt.Sprintf("%#v", stringSliceVar))
    assert.Equal(t, "map[string]int{\"key1\":1, \"key2\":2, \"key3\":3}", fmt.Sprintf("%#v", stringIntMapVar))

構造体の場合は、アドレスまで出力されるのでassert.Containsにとどめました。

   assert.Contains(t, fmt.Sprintf("%#v", file), "&os.File{file:(*os.file)")
    assert.Contains(t, fmt.Sprintf("%#v", list), "&list.List{root:list.Element")

reflectパッケージを使う

次は、reflectパッケージのTypeOfを使って型情報を取得する方法です。

こんな感じです。

func TestUsingReflectTypeOf(t *testing.T) {
    intVar := 10
    intArrayVar := [3]int{1, 2, 3}
    stringSliceVar := []string{"one", "two", "three"}
    stringIntMapVar := map[string]int{"key1": 1, "key2": 2, "key3": 3}

    assert.Equal(t, "int", reflect.TypeOf(intVar).String())
    assert.Equal(t, "[3]int", reflect.TypeOf(intArrayVar).String())
    assert.Equal(t, "[]string", reflect.TypeOf(stringSliceVar).String())
    assert.Equal(t, "map[string]int", reflect.TypeOf(stringIntMapVar).String())

    file, _ := os.Open("show_type_test.go")
    defer file.Close()
    list := list.New()

    assert.Equal(t, "*os.File", reflect.TypeOf(file).String())
    assert.Equal(t, "*list.List", reflect.TypeOf(list).String())
}

reflect.TypeOfを使うことでTypeを取得でき、ここに型情報が含まれています。

Package reflect / func TypeOf

Package reflect / type Type

ちなみに、fmtパッケージの%T書式も、結局のところはreflectパッケージのTypeOfを使ってたりします。

https://github.com/golang/go/blob/go1.15.6/src/fmt/print.go#L654-L661

じゃあTypeOfはどうしてるの?というと、unsafeというパッケージを使っています。

https://github.com/golang/go/blob/go1.15.6/src/reflect/type.go#L1366-L1369

unsafeパッケージは、Goの型安全性を迂回する操作ができ、また移植性、互換性の面で注意なので、基本、使わない方がいいやつですね。

Package unsafe contains operations that step around the type safety of Go programs.

Packages that import unsafe may be non-portable and are not protected by the Go 1 compatibility guidelines.

unsafe - The Go Programming Language

あとは、好奇心的にreflectパッケージのValueOfからも取れないかな?と思いましたが、

Package reflect / func ValueOf

こちらから取得できるKindはちょっと違いますね(ホントに、"種類"ですね)。

Package reflect / type Kind

func TestUsingReflectValueOfKind(t *testing.T) {
    intVar := 10
    intArrayVar := [3]int{1, 2, 3}
    stringSliceVar := []string{"one", "two", "three"}
    stringIntMapVar := map[string]int{"key1": 1, "key2": 2, "key3": 3}

    assert.Equal(t, "int", reflect.ValueOf(intVar).Kind().String())
    assert.Equal(t, "array", reflect.ValueOf(intArrayVar).Kind().String())
    assert.Equal(t, "slice", reflect.ValueOf(stringSliceVar).Kind().String())
    assert.Equal(t, "map", reflect.ValueOf(stringIntMapVar).Kind().String())

    file, _ := os.Open("show_type_test.go")
    defer file.Close()
    list := list.New()

    assert.Equal(t, "ptr", reflect.ValueOf(file).Kind().String())
    assert.Equal(t, "ptr", reflect.ValueOf(list).Kind().String())
}

すぐ忘れそうなので、メモとして。