Goのスコープについて考えてみよう #golang
January 2, 2017
この記事はQiitaの記事をエクスポートしたものです。内容が古くなっている可能性があります。
はじめに
Twitterで以下のような投稿をしてみました。
https://twitter.com/tenntenn/status/815807925222412292
この問題は、以下のコード中に存在するスコープの数を聞いている問題です。
package main
import "fmt"
func main() {
const message = "hello, world"
fmt.Println(message)
}
まだ回答してない方は、ぜひここでいくつになるか考えてみてください。
さて、ここでヒントを出しましょう。 以下のコードをみてみましょう。
package main
import (
myfmt "fmt"
)
func main() {
var string string = "HELLO"
myfmt.Println(string)
}
このコードに存在する識別子は、どのスコープに存在するでしょう?
myfmt
はどの範囲で使えるでしょうか?
組込み型のstring
は?一方で変数のstring
は?
それぞれの識別子の有効範囲を考えてみると、自ずと分かってくるかと思います。
それでは答えを考えていきましょう。
なお、この記事を書いた時点でのGoの最新バージョンはGo1.7.4です。
go/types
パッケージを使って識別子が存在するスコープを探す
さて、答えあわせといきたいところですが、せっかくなのでgo/types
パッケージを使って、それぞれの識別子が定義されているスコープを取得してみたいと思います。
まずは、対象となるコードをパースしてASTを取得しましょう。
ここではparser.ParseFile
関数を使っています。
詳しいASTの取得の方法は、「ASTを取得する方法を調べる」という記事に書いていますので、そちらを参考にしてください。
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "sample.go", src, 0)
if err != nil {
log.Fatalln("Error:", err)
}
さて、ファイルを表すASTのノードであるast.File
が手に入ったので(ここではf
という変数)、go/types
パッケージが提供する型チェッカーにかけ、パッケージ情報を取得してみます。
conf := types.Config{Importer: importer.Default()}
pkg, err := conf.Check("main", fset, []*ast.File{f}, nil)
if err != nil {
log.Fatalln("Error:", err)
}
types.Config
構造体は型チェッカーの設定を保持する構造体で、ここではImporter
フィールドを指定しています。types.Importer
インタフェースは、インポートパスからパッケージ情報を取得する機能を提供するインタフェースです。
なお、パッケージ情報はtypes.Pacakge
構造体で表されます。
*types.Config
型のCheck
メソッドは、型チェックを行うメソッドで以下の3つの機能を提供します。
- 識別子の解決(identifier resolution)
- 型推論(type deduction)
- 定数の評価(constant evaluation)
第1引数は、対象とするパッケージのインポートパスで、今回はmain
としています。
そして第2引数には、パッケージを構成するファイルの情報をASTのノード(*ast.File
型)のスライスとして渡します。
最後に、第3引数にはtypes.Info
構造体のポインタを渡すのですが、ここでは不要なのでnil
にしてあります。
もし、識別子の使用場所や定義場所などの情報を欲しい場合は、Uses
フィールドやDefs
フィールドのマップを初期化して渡すと、そこに解析結果が入れられます。
Check
メソッドの戻り値は、パッケージ情報であるtype.Pacakge
構造体のポインタが取得できます。
今回はここから取得できるスコープの情報を使用して、各識別子のスコープを取得します。
さて、今回対象とするファイル中に登場する識別子をすべて取得するには、ast.Inspect
関数を使うのが手っ取り早いでしょう。
なお、ast.Inspect
関数についての詳細は、「抽象構文木(AST)をトラバースする」という記事に書いてありますのでぜひ参考にしてください。
ast.Inspect(f, func(n ast.Node) bool {
if ident, ok := n.(*ast.Ident); ok {
// TODO: 識別子のスコープを取得する
}
return true
})
上記のコードには、一番大切な各識別子が定義されたスコープを取得するコードがありません。 どうすれば、取得できるのでしょうか?
go/types
パッケージでは、スコープは、types.Scope
構造体として定義されています。
そして、スコープは木構造体を形成しているので、Child
メソッドやParent
で子や親のスコープを取得できます。
*types.Config
型のCheck
メソッドの戻り値である、*types.Package
型からはScope
メソッドを呼ぶことで、パッケージスコープが取得できます。
今回対象とするコードで定義された識別子は、パッケージスコープより下にあることは予想されます。
また、*types.Scope
型のInnerMost
メソッドを使うことで、指定した位置を内包するスコープのうち最も内側にあるスコープを、メソッドのレシーバであるスコープ以下のスコープ(内側のスコープ)から探します。
そのため、一度識別子が登場した場所を含む最も内側のスコープを探し、そこからその識別子が定義されているスコープを親を辿っていきながら探します。
これは、同じ識別子でも、内側のスコープで定義された識別子が優先されることと一致しています。
*types.Scope
型のLookupParent
メソッドを使うと、親を辿って対応する識別子のスコープを探すことができます。
上記の処理をコードにすると以下のようになります。
innerMost := pkg.Scope().Innermost(ident.Pos())
s, _ := innerMost.LookupParent(ident.Name, ident.Pos())
innerMost
はident
が登場した場所のもっとも内側にあるスコープを表します。
そして、LookupParent
メソッドで、innerMost
から親を辿っていき、ident
の定義されたスコープを探します。
なお、LookupParent
メソッドの第2戻り値には、types.Object
型の値が返されますが、ここでは使用しないため_
に代入しています。
さて、上記の処理で取得したs
がいくつあるかがこの問題を解く鍵です。
ここまで出てきたコードを実行可能なようにすべて載せておきます。
package main
import (
"fmt"
"go/ast"
"go/importer"
"go/parser"
"go/token"
"go/types"
"log"
)
// sample.go
const src = `package main
import "fmt"
func main() {
const message = "hello, world"
fmt.Println(message)
}`
func main() {
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "sample.go", src, 0)
if err != nil {
log.Fatalln("Error:", err)
}
conf := types.Config{Importer: importer.Default()}
pkg, err := conf.Check("main", fset, []*ast.File{f}, nil)
if err != nil {
log.Fatalln("Error:", err)
}
scopes := map[*types.Scope]struct{}{}
ast.Inspect(f, func(n ast.Node) bool {
if ident, ok := n.(*ast.Ident); ok {
innerMost := pkg.Scope().Innermost(ident.Pos())
s, _ := innerMost.LookupParent(ident.Name, ident.Pos())
if s != nil {
scopes[s] = struct{}{}
}
}
return true
})
fmt.Println("====", len(scopes), "scopes ====")
for s := range scopes {
fmt.Println(s)
}
}
scopes
をマップにし、キーを*types.Scope
型の値にすることで、重複なくスコープを取得するようにしています。
上記のコードを実行すると以下のように表示されるでしょう。
なおこのコードは、The Go Playground上では、importer.Default
関数で取得できるtypes.Importer
がうまく動かないので実行できません。
==== 3 scopes ====
package "main" scope 0x4203ff6d0 {
. func main.main()
}
sample.go scope 0x4203ff7c0 {
. package fmt
}
function scope 0x42053d630 {
. const message untyped string
}
このコードで出てくる識別子が定義されている以下のスコープは3
つのようです!
main
パッケージのスコープ- ファイル(
sample.go
)スコープ main
関数のスコープ
出力結果を見れば分かる通り、ファイルスコープでは、import
した際に割り当てられるパッケージの識別子が定義されます。ここでは、fmt
がそれにあたります。
よく考えてみると、ファイルごとにimport
文は書くため、この結果には納得できると思います。
さて、冒頭で挙げた問題の答えば3
つが正解でしょうか!?
でもよく考えてみると、何かおかしいです。
解析したコードには、組込み関数や組込み型が一切使われていません。
これらを表すpanic
やint
などの識別子はどのスコープで定義されているのでしょうか?
ユニバーススコープ
実は組込み関数や組込み型、そしてtrue
やfalse
などの定数はユニバーススコープというスコープで定義されています。
このスコープは、どんなGoのコードでも存在し、すべてのスコープのルートとなるスコープです。
ユニバーススコープはtypes.Universe
という変数で定義されています。
なお変数として定義する便宜上、すべてのルートとなるスコープですが、Child
メソッドからは子となるスコープは取得できません。
では、組込み型を使用する以下のコードを先程の解析プログラムにかけて結果をみてみましょう。
package main
import (
myfmt "fmt"
)
func main() {
var string string = "HELLO"
myfmt.Println(string)
}
結果は以下のようになります。
==== 4 scopes ====
package "main" scope 0x4203ff6d0 {
. func main.main()
}
sample.go scope 0x4203ff7c0 {
. package myfmt ("fmt")
}
universe scope 0x4203fe1e0 {
. builtin append
. type bool bool
. type byte byte
. builtin cap
. builtin close
. builtin complex
. type complex128 complex128
. type complex64 complex64
. builtin copy
. builtin delete
. type error interface{Error() string}
. const false untyped bool
. type float32 float32
. type float64 float64
. builtin imag
. type int int
. type int16 int16
. type int32 int32
. type int64 int64
. type int8 int8
. const iota untyped int
. builtin len
. builtin make
. builtin new
. nil
. builtin panic
. builtin print
. builtin println
. builtin real
. builtin recover
. type rune rune
. type string string
. const true untyped bool
. type uint uint
. type uint16 uint16
. type uint32 uint32
. type uint64 uint64
. type uint8 uint8
. type uintptr uintptr
}
function scope 0x42053d630 {
. var string string
}
ユニバーススコープを含めて4つのスコープが存在することが分かるかと思います。
fmt
パッケージに別名をつけたmyfmt
がちゃんとファイルスコープで定義されているのも分かりますね。
実は、スコープには以下の4種類があります。
- ユニバース
- ファイル
- パッケージ
- ローカル
そして、それぞれの種類のスコープでは以下のオブジェクト(types.Object
に対応する)が定義できます。
ユニバース | ファイル | パッケージ | ローカル | |
---|---|---|---|---|
組込み関数(types.Bultin ) |
✔ | |||
nil (types.Nil ) |
✔ | |||
定数(types.Const ) |
✔ | ✔ | ✔ | |
型名(types.TypeName ) |
✔ | ✔ | ✔ | |
関数(types.Func ) |
✔ | |||
変数(types.Var ) |
✔ | ✔ | ||
パッケージ名(types.PkgName ) |
✔ | |||
ラベル(Label ) |
✔ |
おわりに
さて、長々と説明してきましたが、答えは4
つです!
今回の問題は「go/types: The Go Type Checker」というドキュメント中に出てくるものを参考にしました。
このドキュメントはgo/types
パッケージについて詳しく書いてあり、Goに関する知識も身につくため、ぜひ読んでみてください。
なお、読むためにはある程度、他のgo
パッケージについても知っておく必要があるため、go/parser
パッケージやgo/ast
パッケージくらいは触っておいた方が良さそうです。
これらのパッケージについて知りたい方は公式ドキュメントを読むか、私の他のQiitaの記事でgo
パッケージについて書いているので、ぜひそちらも読んでみてください。
ちなみに私は最初2
つだと思ってました!まだまだですね!