これは、なにをしたくて書いたもの?
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
こんな感じですね。
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))
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.
パッケージは簡潔な1語で表現され、パッケージ内の要素にアクセスするにはパッケージ名を常に付与する必要があり、さらにパッケージ名は
import
する時に別名を付けられるので仮に衝突しても問題ない、ということみたいですね。
%#v書式を使う
%#v
書式を使うことで、型と値を得られるものもあります。
%#v a Go-syntax representation of the value
こんな感じです。
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
を取得でき、ここに型情報が含まれています。
ちなみに、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
はちょっと違いますね(ホントに、"種類"ですね)。
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()) }
すぐ忘れそうなので、メモとして。