はじめに
この本について
この本では、Go言語の標準パッケージのうち go/
で始まるものについて、実例を交えながら紹介します。ドキュメントとソースコードの中間となることを目指し、実例を交えつつ、ドキュメントだけからはただちに知ることのできないAPI同士の関連や全体としての使い方に焦点を当てます。
なぜ「GoのためのGo」なのか
Go言語はシンプルさを念頭にデザインされた言語です。仕様は単純明瞭さのために小さく収められていますが、そのため表現力に欠けているとか、コードが冗長になるという印象を持つ人も多いでしょう。有名なところでは、ジェネリクスや例外といった機能が(今のところ)存在しないことが問題にされることが多いようです。
一般に、ソフトウェアエンジニアリングというものは書かれる言語だけに依るものではありません。視点を拡げてGoを取りまくツール群を含めて見てみると、go fmt
や goimports
といったツールが広く使われていること、また go generate
コマンドの存在などを見ても、Goという言語には、人間のプログラミングを機械によってさまざまな面から補助しようという態度があります。
go/*
標準パッケージは、Goのプログラムを対象とし、解析や操作を行うためのAPIを提供します。これらのAPIを利用し、一種のメタプログラミングを行うことで、Goプログラミングの力をより引き出せるようになるはずです。
1. 構文解析
Goに限らず、プログラムのソースコードは、与えられた状態ではただの文字列でしかありません。
ソースコードをプログラムにとって意味のある操作可能な対象とするには、まずソースを構文解析して抽象構文木(Abstract Syntax Tree; AST)に変換し、Goのデータ構造として表現する必要があります。
いったん抽象構文木を手元に得てしまえば、任意のソースコードをプログラムから扱うのはとても簡単です。
以下では、
-
Goのソースコードの抽象構文木がどのようにして得られるのか、
-
抽象構文木において、それぞれの構文要素がどのように表現されているのか
といったことを見ていきます。
1.1. 式の構文解析
Goのソースコードの構文解析を行うには、標準パッケージの go/parser
を使用します。
まずはGoの式(expression)を解析するところからはじめましょう。
package main
import (
"fmt"
"go/parser"
)
func main() {
expr, _ := parser.ParseExpr("a * -1")
fmt.Printf("%#v", expr)
}
簡単のため、サンプルコードではエラーを無視することがあります。 |
go/parser.ParserExpr
はGoの式である文字列を構文解析し、式を表現する抽象構文木である ast.Expr
を返します。
func ParseExpr(x string) (ast.Expr, error)
実行すると以下のように、式 a * -1
に対応する抽象構文木が *ast.BinaryExpr
として得られたことが分かります。
&ast.BinaryExpr{X:(*ast.Ident)(0xc42000ede0), OpPos:3, Op:14, Y:(*ast.UnaryExpr)(0xc42000ee20)}
二項演算子 *
の左の項である a
が X
(*ast.Ident
)として、右の項である -1
が Y
(*ast.UnaryExpr
)として表現されていそうだ、ということが見て取れると思います。
1.1.1. ast.Print
%#v
による表示でも大まかには構文木のノードの様子を知ることができますが、定数値の意味やさらに深いノードの情報には欠けています。構文木をさらに詳細に見ていくには、ast.Print
関数が便利です:
package main
import (
"go/ast"
"go/parser"
)
func main() {
expr, _ := parser.ParseExpr("a * -1")
ast.Print(nil, expr)
}
0 *ast.BinaryExpr { 1 . X: *ast.Ident { 2 . . NamePos: 1 3 . . Name: "a" 4 . . Obj: *ast.Object { 5 . . . Kind: bad 6 . . . Name: "" 7 . . } 8 . } 9 . OpPos: 3 10 . Op: * 11 . Y: *ast.UnaryExpr { 12 . . OpPos: 5 13 . . Op: - 14 . . X: *ast.BasicLit { 15 . . . ValuePos: 6 16 . . . Kind: INT 17 . . . Value: "1" 18 . . } 19 . } 20 }
X.Name
が "a"
であることや Op
が *
であることなど、先ほどの式 a * -1
を表す抽象構文木の構造がより詳細に掴めます。
ast.Print
は抽象構文木を人間に読みやすい形で標準出力に印字します。便利な関数ですがあくまで開発中やデバッグ用途であって、実際にコードを書いて何かを達成するために直接これを使うことはないでしょう。
func Print(fset *token.FileSet, x interface{}) error
第一引数 fset
に関しては、ソースコード中の位置 で触れます。ここでは nil
を渡せば十分です。
1.1.2. 構文ノードのインタフェース
ast.ParseExpr
の返り値となっている ast.Expr
はインタフェース型であり、先ほどの例で得られたのは具体的には *ast.BinaryExpr
構造体でした。これは二項演算に対応する構文ノードです。
type BinaryExpr struct {
X Expr // left operand
OpPos token.Pos // position of Op
Op token.Token // operator
Y Expr // right operand
}
二項演算の左右の式である X
や Y
も ast.Expr
として定義されていることがわかります。先ほどの例では *ast.Ident
や *ast.UnaryExpr
がその具体的な値となっていました。
これらの構造体を含め、すべてのGoの式に対応する構文ノードは ast.Expr
インタフェースを実装しています。
type Expr interface {
Node
exprNode()
}
ast.Expr
は(埋め込まれている ast.Node
を除けば)外部に公開されないメソッドで構成されています。そのため、ast
パッケージ外の型が ast.Expr
を実装することはありません。
exprNode()
は実際にはどこからも呼ばれないメソッドです。そのため、ast.Expr
はその振る舞いに関する情報を提供しない、分類用のインタフェースであるといえます。同様に、文や宣言に対応するインタフェース(ast.Stmt
と ast.Decl
)も定義されています。埋め込まれている ast.Node
インタフェースも含め、これらについて詳しくは構文ノードの実装で見ます。
1.2. ファイルの構文解析
ここまで式の構文解析を例にとって見てきましたが、実践においては、Goのソースコードはファイルやパッケージの単位で扱うことが普通です。ここからはファイル全体を構文解析する方法を見ていきます。
1.2.1. ファイルの構造
まず、Goのソースコードファイルの構造を確認しておきましょう。
The Go Programming Language Specification - Source file organization によれば、ひとつのファイルの中には
-
パッケージ名
-
import
節 -
値や関数などトップレベルの宣言
が、この順番で現れることになっています。
1.2.2. parser.ParseFile
Goのソースコードファイルの構文解析を行うには parser.ParseFile
を使用します。
func ParseFile(fset *token.FileSet, filename string, src interface{}, mode Mode) (f *ast.File, err error)
第二引数の filename
と第三引数の src
はふたつで一組になっていて、構文解析するソースコードを指定します。src == nil
であるときは filename
に指定されたファイルの内容をソースコードとして読み込みます。それ以外の場合は src
をソースコードとして読み込み、filename
はソースコードの位置情報にだけ使われます。src
は interface{}
ですが、指定できるのは string
、[]byte
、io.Reader
のいずれかのみです。
第一引数の fset
は構文解析によって得られた構文木のノードの詳細な位置情報を保持する token.FileSet
構造体へのポインタです。詳しくはソースコード中の位置で説明しますが、基本的に token.NewFileSet()
で得られるものを渡せば十分です。
最後の引数 mode
では構文解析する範囲の指定などが行えます。後でコメントとドキュメントを扱うときに少し触れます。
package main
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
)
func main() {
fset := token.NewFileSet()
f, _ := parser.ParseFile(fset, "example.go", src, parser.Mode(0))
for _, d := range f.Decls {
ast.Print(fset, d)
fmt.Println()
}
}
var src = `package p
import _ "log"
func add(n, m int) {}
`
0 *ast.GenDecl { 1 . TokPos: example.go:2:1 2 . Tok: import 3 . Lparen: - 4 . Specs: []ast.Spec (len = 1) { 5 . . 0: *ast.ImportSpec { 6 . . . Name: *ast.Ident { 7 . . . . NamePos: example.go:2:8 8 . . . . Name: "_" 9 . . . } 10 . . . Path: *ast.BasicLit { 11 . . . . ValuePos: example.go:2:10 12 . . . . Kind: STRING 13 . . . . Value: "\"log\"" 14 . . . } 15 . . . EndPos: - 16 . . } 17 . } 18 . Rparen: - 19 } 0 *ast.FuncDecl { 1 . Name: *ast.Ident { 2 . . NamePos: example.go:3:6 3 . . Name: "add" 4 . . Obj: *ast.Object { 5 . . . Kind: func 6 . . . Name: "add" 7 . . . Decl: *(obj @ 0) 8 . . } 9 . } 10 . Type: *ast.FuncType { 11 . . Func: example.go:3:1 12 . . Params: *ast.FieldList { 13 . . . Opening: example.go:3:9 14 . . . List: []*ast.Field (len = 1) { 15 . . . . 0: *ast.Field { 16 . . . . . Names: []*ast.Ident (len = 2) { 17 . . . . . . 0: *ast.Ident { 18 . . . . . . . NamePos: example.go:3:10 19 . . . . . . . Name: "n" 20 . . . . . . . Obj: *ast.Object { 21 . . . . . . . . Kind: var 22 . . . . . . . . Name: "n" 23 . . . . . . . . Decl: *(obj @ 15) 24 . . . . . . . } 25 . . . . . . } 26 . . . . . . 1: *ast.Ident { 27 . . . . . . . NamePos: example.go:3:13 28 . . . . . . . Name: "m" 29 . . . . . . . Obj: *ast.Object { 30 . . . . . . . . Kind: var 31 . . . . . . . . Name: "m" 32 . . . . . . . . Decl: *(obj @ 15) 33 . . . . . . . } 34 . . . . . . } 35 . . . . . } 36 . . . . . Type: *ast.Ident { 37 . . . . . . NamePos: example.go:3:15 38 . . . . . . Name: "int" 39 . . . . . } 40 . . . . } 41 . . . } 42 . . . Closing: example.go:3:18 43 . . } 44 . } 45 . Body: *ast.BlockStmt { 46 . . Lbrace: example.go:3:20 47 . . Rbrace: example.go:3:21 48 . } 49 }
例では src
変数のもつソースコードを構文解析し、トップレベルの宣言を印字します。今回は import
宣言が *ast.GenDecl
として、関数 func f
が *ast.FuncDecl
として得られました。
1.2.3. ast.File
ソースファイルは ast.File
構造体で表現され、パッケージ名やトップレベルの宣言の情報を含んでいます。
type File struct {
Doc *CommentGroup // associated documentation; or nil
Package token.Pos // position of "package" keyword
Name *Ident // package name
Decls []Decl // top-level declarations; or nil
Scope *Scope // package scope (this file only)
Imports []*ImportSpec // imports in this file
Unresolved []*Ident // unresolved identifiers in this file
Comments []*CommentGroup // list of all comments in the source file
}
他にもいろいろなフィールドがありますが、
-
Package
はソースコード中の位置、 -
Scope
とUnresolved
はスコープ、 -
Doc
とComments
はコメントとドキュメント
で解説します。
1.2.4. 構文木の探索
構文ノードのインタフェースで述べたように、構文木のノードは ast
パッケージのインタフェースとして得られます。そのため、具体的な内容を知るにはtype assertionやtype switchを用いなければなりません。これを手で丁寧に書いていくのは大変で間違いも起きがちですが、ast.Inspect
関数で構文ノードに対する(深さ優先)探索を行えます。
func Inspect(node Node, f func(Node) bool)
node
から始まり、子ノードを再帰的に探索しつつにコールバック関数 f
が呼ばれます。子ノードの探索を終えるごとに、引数 nil
でコールバックが呼ばれます。コールバック関数では false
を返すことで、そのノードの子供以下への探索を打ち切ることができます。
以下は先ほどのソースコードファイル中の識別子を一覧する例です。訪問したノードの具体的な型を知るために、type assertionをおこなっています。
package main
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
)
func main() {
fset := token.NewFileSet()
f, _ := parser.ParseFile(fset, "example.go", src, parser.Mode(0))
ast.Inspect(f, func(n ast.Node) bool {
if ident, ok := n.(*ast.Ident); ok {
fmt.Println(ident.Name)
}
return true
})
}
var src = `package p
import _ "log"
func add(n, m int) {}
`
p _ add n m int
パッケージ名(p
)や変数名(n
)などの識別子が構文木に含まれていることが確認できます。
もうひとつの方法として、ast.Visitor インタフェースを実装して ast.Walk(v ast.Visitor, node ast.Node) を使うこともできます。実際 ast.Inspect の内部では ast.Walk が使われています。
|
1.3. 構文ノードの実装
1.3.1. ast.Node
抽象構文木のノードに対応する構造体は、すべて ast.Node
インタフェースを実装しています。
type Node interface {
Pos() token.Pos // position of first character belonging to the node
End() token.Pos // position of first character immediately after the node
}
定義を見れば分かるとおり、ast.Node
インタフェース自身はそのソースコード中の位置を提供するだけであり、このままでは構文木に関する情報を得ることはできません。構文木を探索・操作するにはtype assertionやtype swtichによる具体的な型への変換が必要になります。
構文木のノードを大別するため、ast.Node
を実装するサブインタフェースが定義されています:
ast.Decl
-
宣言(declaration)。
import
やtype
など ast.Stmt
-
文(statement)。
if
やswitch
など ast.Expr
-
式(expression)。識別子や演算、型など
ファイルやコメントなど、これらに分類されない構文ノードも存在します。
以下でこれらサブインタフェースと、その実装のうち主要なものを見ていきます。
ast.Nodeの階層で ast.Node を実装する型の完全な一覧を確認できます。
|
1.3.2. ast.Decl
ast.Decl
インタフェースはGoソースコードにおける宣言(declaration)に対応する構文木のノードを表します。Goの宣言は
-
パッケージのインポート(
import
) -
変数および定数(
var
、const
) -
型(
type
) -
関数およびメソッド(
func
)
と4種類に分けられますが、ast.Decl
インタフェースを実装している構造体は *ast.FuncDecl
と *ast.GenDecl
の2つのみです。前者は名前どおり関数及びメソッドの宣言に相当し、後者が残りすべてをカバーします。
ast.FuncDecl
type FuncDecl struct {
Doc *CommentGroup // associated documentation; or nil
Recv *FieldList // receiver (methods); or nil (functions)
Name *Ident // function/method name
Type *FuncType // function signature: parameters, results, and position of "func" keyword
Body *BlockStmt // function body; or nil (forward declaration)
}
ast.FuncDecl
構造体は関数の宣言に対応します。Recv
フィールドはそのレシーバを表しており、これが nil
である場合は関数を、そうでない場合はメソッドの宣言を表します。
Recv
の型である *ast.FieldList
は識別子と型の組のリストで、関数のパラメータや構造体のフィールドを表すのに使われます。
FieldList はその名の通り複数の組を表しますが、Goの文法上、レシーバとしてはただ1つの組のみが有効です。が構造体のフィールド宣言の表現などにも使われる FieldList が再利用されている形です。
|
上記の事情にも関わらず、 TODO: なんかいい感じに引用スタイル リスト 5. src/go/parser/parser.go
The parser accepts a larger language than is syntactically permitted by the Go spec, for simplicity, and for improved robustness in the presence of syntax errors. For instance, in method declarations, the receiver is treated like an ordinary parameter list and thus may contain multiple entries where the spec permits exactly one. Consequently, the corresponding field in the AST (ast.FuncDecl.Recv) field is not restricted to one entry. |
ast.GenDecl
関数以外の宣言、import
、const
、var
、type
は ast.GenDecl
がまとめて引き受けます。
type GenDecl struct {
Doc *CommentGroup // associated documentation; or nil
TokPos token.Pos // position of Tok
Tok token.Token // IMPORT, CONST, TYPE, VAR
Lparen token.Pos // position of '(', if any
Specs []Spec
Rparen token.Pos // position of ')', if any
}
Specs
フィールドはスライスであり、その要素がそれぞれ ast.Spec
インタフェースであると定義されています。実際には、要素の具体的な型は Tok
フィールドの値によってひとつに決まります。
Tok の値 |
Specs の要素の型 |
表す構文 |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
これらの宣言には、以下のようにグループ化できるという共通点があります。グループ化された宣言のひとつが Specs
スライスのひとつの要素に対応します。
import (
"foo"
"bar"
)
const (
a = 1
b = 2
)
var (
x int
y bool
)
type (
s struct{}
t interface{}
)
1.3.3. ast.Stmt
ast.Stmt
インタフェースはGoソースコードにおける 文 に対応する構文木のノードを表します。文はプログラムの実行を制御するもので、go/ast
パッケージの実装では以下のように分類されています:
-
宣言(
ast.DeclStmt
) -
空の文(
ast.EmptyStmt
) -
ラベル付き文(
ast.LabeledStmt
) -
式だけの文(
ast.ExprStmt
) -
チャンネルへの送信(
ast.SendStmt
) -
インクリメント・デクリメント(
ast.IncDecStmt
) -
代入または定義(
ast.AssignStmt
) -
go
(ast.GoStmt
) -
defer
(ast.DeferStmt
) -
return
(ast.ReturnStmt
) -
break
、continue
、goto
、fallthrough
(ast.BranchStmt
) -
ブロック(
ast.BlockStmt
) -
if
(ast.IfStmt
) -
式による
switch
(ast.SwitchStmt
) -
型による
switch
(ast.TypeSwitchStmt
) -
switch
中のひとつの節(ast.CaseClause
) -
select
(ast.SelectStmt
) -
select
中のひとつの節(ast.CommClause
) -
range
を含まないfor
(ast.ForStmt
) -
range
を含むfor
(ast.RangeStmt
)
1.3.4. ast.Expr
ast.Ellipsis や ast.KeyValueExpr のように、それ単体では式となり得ないノードも ast.Expr を実装していますが、このおかげでこれらを含むノードの実装が簡単になっているようです。
|
-
識別子(
ast.Ident
) -
…
(ast.Ellipsis
) -
基本的な型のリテラル(
ast.BasicLit
) -
関数リテラル(
ast.FuncLit
) -
複合リテラル(
ast.CompositeLit
) -
括弧(
ast.ParenExpr
) -
セレクタまたは修飾された識別子(
x.y
)(ast.SelectorExpr
) -
添字アクセス(
ast.IndexExpr
) -
スライス式(
ast.SliceExpr
) -
型アサーション(
ast.TypeAssertExpr
) -
関数またはメソッド呼び出し(
ast.CallExpr
) -
ポインタの間接参照またはポインタ型(
*p
)(ast.StarExpr
) -
単項演算(
ast.UnaryExpr
) -
二項演算(
ast.BinaryExpr
) -
複合リテラル中のキーと値のペア(
key: value
)(ast.KeyValueExpr
) -
配列またはスライス型(
ast.ArrayType
) -
構造体型(
ast.StructType
) -
関数型(
ast.FuncType
) -
インタフェース型(
ast.InterfaceType
) -
マップ型(
ast.MapType
) -
チャンネル型(
ast.ChanType
)
ast.Ident
type Ident struct {
NamePos token.Pos // identifier position
Name string // identifier name
Obj *Object // denoted object; or nil
}
ast.Ident
はコード中の識別子を表し、変数名をはじめパッケージ名、ラベルなどさまざまな場所に登場します。
Obj
フィールドはその実体を表す ast.Object
への参照になっています。詳しくは スコープとオブジェクトで触れます。
ast.StructTypeとast.InterfaceType
type StructType struct {
Struct token.Pos // position of "struct" keyword
Fields *FieldList // list of field declarations
Incomplete bool // true if (source) fields are missing in the Fields list
}
type InterfaceType struct {
Interface token.Pos // position of "interface" keyword
Methods *FieldList // list of methods
Incomplete bool // true if (source) methods are missing in the Methods list
}
これら2つの構造体はそれぞれ構造体、インタフェースを表現します。また、Incomplete
フィールドを持っています。これらは通常 false
ですが、フィルタによってノード中のフィールドやメソッドの宣言が取り除かれる際に true
となり、ソースコードとノードに乖離があることを示します。go doc
が出力する “// Has unexported fields.” はこの値を参照しています。
package main
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
)
func main() {
fset := token.NewFileSet()
f, _ := parser.ParseFile(fset, "example.go", src, parser.Mode(0))
structType := f.Decls[0].(*ast.GenDecl).Specs[0].(*ast.TypeSpec).Type.(*ast.StructType)
fmt.Printf("fields=%#v incomplete=%#v\n", structType.Fields.List, structType.Incomplete)
ast.FileExports(f)
fmt.Printf("fields=%#v incomplete=%#v\n", structType.Fields.List, structType.Incomplete)
}
var src = `package p
type S struct {
Public string
private string
}
`
fields=[]*ast.Field{(*ast.Field)(0xc420016580), (*ast.Field)(0xc420016600)} incomplete=false fields=[]*ast.Field{(*ast.Field)(0xc420016580)} incomplete=true
1.3.5. その他のノード
以上の3種類に分類されないノードもいくつか存在します。
ast.Commentとast.CommentGroup
type Comment struct {
Slash token.Pos // position of "/" starting the comment
Text string // comment text (excluding '\n' for //-style comments)
}
type CommentGroup struct {
List []*Comment // len(List) > 0
}
ast.Comment
はひとつのコメント(// …
または /* … */
)に、ast.CommentGroup
は連続するコメントに対応します。コメントとドキュメントで詳しく見ます。
ast.Fieldとast.FieldList
type Field struct {
Doc *CommentGroup // associated documentation; or nil
Names []*Ident // field/method/parameter names; or nil if anonymous field
Type Expr // field/method/parameter type
Tag *BasicLit // field tag; or nil
Comment *CommentGroup // line comments; or nil
}
type FieldList struct {
Opening token.Pos // position of opening parenthesis/brace, if any
List []*Field // field list; or nil
Closing token.Pos // position of closing parenthesis/brace, if any
}
それぞれ、識別子と型の組ひとつ、そのリストに対応します。
ast.FieldList
は以下の構造体に含まれています:
-
ast.StructType
…… 構造体のフィールドのリストとして -
ast.InterfaceType
…… インタフェースのメソッドのリストとして -
ast.FuncType
…… 関数のパラメータおよび返り値として -
ast.FuncDecl
…… メソッドのレシーバとして
ast.Field
の Tag
は構造体のフィールドである場合のみ存在しえます。
TODO Names のふるまい方; nil と複数 |
Appendix A: ast.Nodeの階層
Node Decl *BadDecl *FuncDecl *GenDecl Expr *ArrayType *BadExpr *BasicLit *BinaryExpr *CallExpr *ChanType *CompositeLit *Ellipsis *FuncLit *FuncType *Ident *IndexExpr *InterfaceType *KeyValueExpr *MapType *ParenExpr *SelectorExpr *SliceExpr *StarExpr *StructType *TypeAssertExpr *UnaryExpr Spec *ImportSpec *TypeSpec *ValueSpec Stmt *AssignStmt *BadStmt *BlockStmt *BranchStmt *CaseClause *CommClause *DeclStmt *DeferStmt *EmptyStmt *ExprStmt *ForStmt *GoStmt *IfStmt *IncDecStmt *LabeledStmt *RangeStmt *ReturnStmt *SelectStmt *SendStmt *SwitchStmt *TypeSwitchStmt *Comment *CommentGroup *Field *FieldList *File *Package
1.4. ソースコード中の位置
ソースコードを対象とするプログラムがユーザにフィードバックを行う際は、以下の go vet
の出力のように、ファイル名や行番号などソースコードにおける位置情報を含めるのが普通です。
% go vet github.com/motemen/gore quickfix.go:76: go/ast.ExprStmt composite literal uses unkeyed fields
以下では、このようなソースコード中の位置情報を扱うためのAPIを見ていきます。
1.4.1. token.Pos
すべての抽象構文木のノードはast.Nodeインタフェースを実装しているのでした。ast.Node
は token.Pos
を返す Pos()
と End()
の2つのメソッドで構成されます。
type Node interface {
Pos() token.Pos // position of first character belonging to the node
End() token.Pos // position of first character immediately after the node
}
これらはその名とコメントの示すとおり、当該のノードがソースコード上に占める開始位置と終了位置を表しています。token.Pos
の実体は基準位置からオフセットを示す int
型です。
type Pos int
オフセット値は 1
から始まるバイト単位の値です。特に、token.Pos
のzero value(= 0
)には token.NoPos
という特別な名前が与えられています。
const NoPos Pos = 0
CallExpr.Ellipsis や GenDecl.Lparen においてなど、token.NoPos はその位置情報を持つ要素がソースコード中に存在しないことを意味する場合もあります。
|
token.Pos
は単なる整数値でしかないので、ファイル名や行番号などの詳細な情報をこれだけから得ることはできません。実はノードの持つこれらの位置情報は token.FileSet
を基準にした相対的なものとしてエンコードされていて、完全な情報を復元するには FileSet
と Pos
を組み合わせる必要があります。token.FileSet
はこれまでの例にも登場してきた(そして無視されてきた)fset
と名づけられるデータです。
ここから分かるように、構文解析の際に与える token.FileSet によってノードの構造体の値は変化します。抽象構文木を扱うプログラムでは、構文解析によって得られたノードは常にその基準となる token.FileSet とともに保持しておく必要があります。
|
1.4.2. token.FileSet
type FileSet struct {
// Has unexported fields.
}
token.FileSet
は、go/parser
が生成する抽象構文木のノードの位置情報を一手に引きうけ、保持する構造体です。ノードの構造体が保持する位置情報は前項で述べたように token.FileSet
を基準にした相対的なもので、整数値としてエンコードされています。
名前の通り、token.FileSet
が表すのは複数のソースファイルの集合です。ここでのファイルとは概念上のもので、ファイルシステム上に存在する必要はなく、またファイル名が重複していても問題ありません。
興味あるソースファイル集合に対して1つあれば十分なので、いちど token.NewFileSet()
で生成した参照を保持しておくのが普通です。
func NewFileSet() *FileSet
token.FileSet
は、構文要素の具体的な位置を参照するAPIで要求されます。
-
構文木のノードを生成する際に必要です。
-
ソースコードの文字列化に必要です。
-
ast.Printに渡すと、
token.Pos
がダンプされる際にファイル名と行番号、カラム位置が表示されます。
token.FileSet
はファイルそれぞれについて、
-
ファイルの開始位置のオフセット
-
各行の長さ
をバイト単位で保持しており、整数値にエンコードされた位置情報から、次に見る完全な位置情報を復元できます。
1.4.3. token.Position
type Position struct {
Filename string // filename, if any
Offset int // offset, starting at 0
Line int // line number, starting at 1
Column int // column number, starting at 1 (byte count)
}
token.Position
構造体はファイル名、行番号、カラム位置を持ち、ソースコード中の位置としては最も詳細な情報を含みます。String()
メソッドによってわかりやすい位置情報が得られます。
package main
import (
"fmt"
"go/token"
)
func main() {
fmt.Println("Invalid position without file name:", token.Position{}.String())
fmt.Println("Invalid position with file name: ", token.Position{Filename: "example.go"}.String())
fmt.Println("Valid position without file name: ", token.Position{Line: 2, Column: 3}.String())
fmt.Println("Valid position with file name: ", token.Position{Filename: "example.go", Line: 2, Column: 3}.String())
}
Invalid position without file name: - Invalid position with file name: example.go Valid position without file name: 2:3 Valid position with file name: example.go:2:3
1.5. スコープとオブジェクト
Goのソースコードにおいて名前はレキシカルスコープを持ち、その有効範囲は静的に決まります。構文解析のAPIにもスコープに関係するものがいくつか存在します。以下ではこれらについて簡単に見ていきます。
1.5.1. 構文解析だけでは不十分な例
ただし、構文解析だけでは全ての名前を正しく解決できるわけではありません。
以下のプログラムには T{k: 0}
という同じ形をしたコードが出現します。ここで k
が指すものは一方ではトップレベルの定数、もう一方では構造体のフィールドと、それぞれ違ったものになります(go/types
: The Go Type Checker より)。
package p
const k = 0
func f1() {
type T [1]int
_ = T{k: 0}
}
func f2() {
type T struct{ k int }
_ = T{k: 0}
}
また、名前なしの import
文によって導入されたパッケージ名は構文解析だけでは判定できません。
import "github.com/motemen/go-gitconfig" // gitconfig という名前が導入される
これらも含めて正しく(Go言語の仕様通りに)名前のスコープを決定するには、意味解析の手続きを経なくてはなりません。
このように、go/ast のAPIで得られるスコープの情報は不完全なもので、あくまでソースコードが構文的に誤りのないことを保証するものです。より正確で詳しい情報が知りたい場合には型解析を行います。
|
1.5.2. スコープ
Goのスコープはブロックにもとづいて作られます。ブレース({ … }
)による明示的なブロックのほかにも、構文から作られるスコープがあります。Declarations and scope - The Go Programming Language Specification に述べられていますが、抄訳します:
-
あらかじめ定義されている名前(
nil
やint
など)はユニバースブロック(universe block)に属します。 -
トップレベルの定数、変数、関数(メソッドを除く)はパッケージブロック(package block)に属します。
-
インポートされたパッケージの名前は、それを含むファイルブロック(file block)に属します。
go/ast
のAPIを使用してアクセスできるのは、ファイルブロックのスコープとパッケージブロックのスコープ(パッケージ)のみです。
構文解析によって得られたスコープは ast.Scope
として表現されます:
type Scope struct {
Outer *Scope
Objects map[string]*Object
}
スコープはその外側のスコープへの参照と、名前からオブジェクトへのマッピングで構成されています。あるスコープはその内側のスコープの情報を保持していませんが、これはスコープが基本的に識別子の解決のために使われるものだからです。あるスコープに出現した識別子がどんなオブジェクトであるかを判定するには、子スコープの情報は不要であり、親スコープを辿ることによって解決されます。
1.5.3. オブジェクト
ソースコード中の識別子を表す ast.Ident
には、*ast.Object
型の Obj
というフィールドが定義されていました。
type Ident struct {
NamePos token.Pos // identifier position
Name string // identifier name
Obj *Object // denoted object; or nil
}
go/ast
や go/types
では、名前をつけられた言語上の要素(named language entity)をオブジェクト(object)と呼んでおり、構文上のオブジェクトはこの ast.Object
によって表されています。
ast.Object
の Decl
フィールドは、そのオブジェクトが宣言されたノードを表します。
type Object struct {
Kind ObjKind
Name string // declared name
Decl interface{} // corresponding Field, XxxSpec, FuncDecl, LabeledStmt, AssignStmt, Scope; or nil
Data interface{} // object-specific data; or nil
Type interface{} // placeholder for type information; may be nil
}
構文上同じオブジェクトを指すと思わしき識別子に対応する *ast.Ident
は、同じ ast.Object
を共有します。
package main
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
)
func main() {
fset := token.NewFileSet()
f, _ := parser.ParseFile(fset, "example.go", src, parser.Mode(0))
ast.Inspect(f, func(n ast.Node) bool {
if ident, ok := n.(*ast.Ident); ok && ident.Name == "x" {
var decl interface{}
if ident != nil && ident.Obj != nil {
decl = ident.Obj.Decl
}
var kind ast.ObjKind
if ident.Obj != nil {
kind = ident.Obj.Kind
}
fmt.Printf("%-17sobj=%-12p kind=%s decl=%T\n", fset.Position(ident.Pos()), ident.Obj, kind, decl)
}
return true
})
}
var src = `package p
import x "pkg"
func f() {
if x := x.f(); x != nil {
x(func(x int) int { return x + 1 })
}
}
`
example.go:3:8 obj=0x0 kind=bad decl=<nil> example.go:6:8 obj=0xc4200620f0 kind=var decl=*ast.AssignStmt example.go:6:13 obj=0x0 kind=bad decl=<nil> example.go:6:20 obj=0xc4200620f0 kind=var decl=*ast.AssignStmt example.go:7:9 obj=0xc4200620f0 kind=var decl=*ast.AssignStmt example.go:7:16 obj=0xc420062140 kind=var decl=*ast.Field example.go:7:36 obj=0xc420062140 kind=var decl=*ast.Field
import
したパッケージ名としての x
、定義された変数としての x
、関数の仮引数名としての x
がそれぞれ違った Obj
をもち、文法的に同じものであれば Obj
が同じものを指しています。
Kind
フィールドは ObjKind
型に定義されている値のいずれかを取り、オブジェクトの種類を表します。
const (
Bad ObjKind = iota // for error handling
Pkg // package
Con // constant
Typ // type
Var // variable
Fun // function or method
Lbl // label
)
パッケージ名、定数名、型名、変数名、関数名またはメソッド名に加え、ラベル名もオブジェクトとして扱われることがわかります。
ちなみに、現在 |
1.5.4. パッケージ
Goでは、ひとつのディレクトリに配置された複数のソースファイルが集まって、ひとつのパッケージを構成します。パッケージを構成するソースコードに登場する名前は、すべてどこかで定義されている必要があるという意味において、解決できなければいけません。
ast.File.Scopeとast.File.Unresolved
type File struct {
Doc *CommentGroup // associated documentation; or nil
Package token.Pos // position of "package" keyword
Name *Ident // package name
Decls []Decl // top-level declarations; or nil
Scope *Scope // package scope (this file only)
Imports []*ImportSpec // imports in this file
Unresolved []*Ident // unresolved identifiers in this file
Comments []*CommentGroup // list of all comments in the source file
}
ast.File
構造体の Scope
フィールドは、当該のソースファイルのファイルスコープ(ファイルブロック)を表します。ここで解決できなかったものは Unresolved
フィールドに記録されます。正しくコンパイルできるソースコードであれば、ここに入るのは
-
import
によってファイルスコープに導入される名前 -
同パッケージの他ファイルのトップレベルに定義されている名前
-
ユニバースブロックに定義されている名前
への参照になるはずです。
ast.Package
type Package struct {
Name string // package name
Scope *Scope // package scope across all files
Imports map[string]*Object // map of package id -> package object
Files map[string]*File // Go source files by filename
}
複数のソースファイルをまとめ、パッケージとして扱うものが ast.Package
です。Scope
はパッケージスコープを表し、各々のファイルのトップレベルに宣言された名前を格納します。Imports
は import
宣言によって導入された名前を保持します。
ast.Package
は ast.NewPackage
で生成されます。
func NewPackage(fset *token.FileSet, files map[string]*File, importer Importer, universe *Scope) (*Package, error)
第3引数の importer ast.Importer
は、import
されるパッケージパスから、それが導入するオブジェクトを(記録しつつ)返す関数を渡します。
type Importer func(imports map[string]*Object, path string) (pkg *Object, err error)
go
本体と同じ挙動をするという意味での ast.Importer
のカノニカルな実装は提供されていません。パッケージファイルのインポートを解決するために、ビルド済みのオブジェクトファイルを読み込むAPIは提供されています(TODO: 後述)。これは型も含めたパッケージ情報の読み込みとなるため、文法レベルの情報を扱う ast
パッケージの範疇を外れます。
第4引数の universe *ast.Scope
には、パッケージの外側のスコープであるユニバーススコープを渡します。こちらについても、型におけるユニバーススコープの情報を得るAPIは存在しますが、抽象構文木のみのレベルのものはありません。
これらを正しく渡すことで完全な ast.Package
を生成することができますが、正しい情報が必要な場合には型解析を行うことを考えたほうがよいでしょう。
TODO: golang/gddo の例 |
1.5.5. parser.ParseDir
Goでは、ひとつのパッケージに属するソースコードファイルは同じディレクトリ直下に配置されます。これらを一度に構文解析し、ast.Package
を生成するAPIもあります。
func ParseDir(fset *token.FileSet, path string, filter func(os.FileInfo) bool, mode Mode) (pkgs map[string]*ast.Package, first error)
この関数では ast.NewPackage
で行われるような名前の解決は行われません。
ParseDir はひとつのディレクトリからひとつでなく複数のパッケージを返しうるAPIになっていますが、異常なことではありません。普通にコンパイルできるような構成においても、複数のパッケージがひとつのディレクトリに共在することはありえます(Test packages)。
|
2. コメントとドキュメント
これまではプログラムの実行に関わるコード本体をプログラムから扱う方法について見てきました。この章ではGoソースコード中のコメントを扱っていきます。
コメントはドキュメントの記述にも使用されており、そのためのAPIも go/doc
パッケージとして用意されています。
2.1. コメントの解析
parser.ParseFile
の第4引数 mode
に parser.ParseComments
定数を指定することで、構文解析の結果にコメントを含めることができます。
func ParseFile(fset *token.FileSet, filename string, src interface{}, mode Mode) (f *ast.File, err error)
package main
import (
"fmt"
"go/parser"
"go/token"
)
func main() {
fset := token.NewFileSet()
f, _ := parser.ParseFile(fset, "example.go", src, parser.ParseComments)
for _, c := range f.Comments {
fmt.Printf("%s: %q\n", fset.Position(c.Pos()), c.Text())
}
}
var src = `// Package p provides Add function
// ...
package p
// Add adds two ints.
func add(n, m int) int {
return n + m
}
`
example.go:1:1: "Package p provides Add function\n...\n" example.go:5:1: "Add adds two ints.\n"
こうやって解析されたコメントは通常の構文木とは別に、ast.File
構造体の Comments
フィールドに格納されます。Comments
フィールドは []*ast.CommentGroup
として宣言されています。ast.CommentGroup
は連続して続くコメントをひとまとめにしたもので、
-
/* … */
形式のコメントなら/*
から*/
まで -
// …
形式なら//
から行末まで
が、ひとつの ast.Comment
に対応します。
type CommentGroup struct {
List []*Comment // len(List) > 0
}
type Comment struct {
Slash token.Pos // position of "/" starting the comment
Text string // comment text (excluding '\n' for //-style comments)
}
例えばコメントが以下のように書かれていた場合、それぞれ CommentGroup
は2つ生成され、それぞれ2個の Comment
を持ちます。
// foo
/* bar */
// baz
// quux
コメントも ast.Node
インタフェースを実装し、位置情報を保持しています。ソースコードの文字列化の際は、この位置情報にもとづいてコメントが正しく挿入されるようになっています。
2.2. Goにおけるドキュメント
Goではトップレベルの型や関数のすぐ直前のコメントがそのAPIのドキュメントである、と標準的に定められています(Godoc: documenting Go code)。標準の go doc
コマンドもこのルールに則ってドキュメントを表示します。
% go doc go/parser.ParseFile func ParseFile(fset *token.FileSet, filename string, src interface{}, mode Mode) (f *ast.File, err error) ParseFile parses the source code of a single Go source file and returns the corresponding ast.File node. The source code may be provided via the filename of the source file, or via the src parameter. ...
// ParseFile parses the source code of a single Go source file and returns
// the corresponding ast.File node. The source code may be provided via
// the filename of the source file, or via the src parameter.
// ...
//
func ParseFile(fset *token.FileSet, filename string, src interface{}, mode Mode) (f *ast.File, err error) {
2.3. doc.Package
Goパッケージの(ソースコードから生成される)ドキュメントは、doc.Package
として表現されます。
type Package struct {
Doc string
Name string
ImportPath string
Imports []string
Filenames []string
Notes map[string][]*Note
// Deprecated: For backward compatibility Bugs is still populated,
// but all new code should use Notes instead.
Bugs []string
// declarations
Consts []*Value
Types []*Type
Vars []*Value
Funcs []*Func
}
doc.Package
は doc.New
関数によって ast.Package
(パッケージ)から生成されます。
func New(pkg *ast.Package, importPath string, mode Mode) *Package
ドキュメントに “New takes ownership of the AST pkg and may edit or overwrite it.” とある通り、doc.New は与えられた pkg を書き換えることがあります。
|
mode
パラメータの指定によって、非公開のAPIに関してもドキュメントを収集することができます。
const (
// extract documentation for all package-level declarations,
// not just exported ones
AllDecls Mode = 1 << iota
// show all embedded methods, not just the ones of
// invisible (unexported) anonymous fields
AllMethods
)
4. 型解析
ここまで見てきたようなGoの抽象構文木を扱うAPIを知っていれば、Goプログラムを対象にしてできることの7割ほどは実現できたも同然です。しかし、
-
ソースコード中に登場する名前が定義された位置や、
-
ある型がインタフェースを実装しているか
など、プログラムの構造を越えたより高度な情報が必要になった場合は、型解析に手を出す必要があります。
この章では、Goパッケージの型チェックと型にまつわるデータ構造を提供する go/types
パッケージのAPIを見ていきます。
TODO
|
4.1. 型チェックを行う
types
パッケージによる型チェックは、types.Config
構造体の Check
メソッドを呼ぶところから始まります。
func (conf *Config) Check(path string, fset *token.FileSet, files []*ast.File, info *Info) (*Package, error)
files []ast.File
には、ひとつのパッケージを構成する構文解析されたファイル群を指定します。ファイルの構文解析 で files
の要素を生成した際に使用した fset
も引数として渡します。
最後の引数である info *types.Info
は、パッケージ内の型にまつわる詳細な情報を格納する先として指定します。単純に型チェックを行いたいだけの場合は nil
でも構いません。
package main
import (
"fmt"
"go/ast"
"go/importer"
"go/parser"
"go/token"
"go/types"
)
func main() {
fset := token.NewFileSet()
f, _ := parser.ParseFile(fset, "example.go", src, parser.Mode(0))
conf := types.Config{Importer: importer.Default()}
pkg, _ := conf.Check("path/to/pkg", fset, []*ast.File{f}, nil)
fmt.Println(pkg)
fmt.Println(pkg.Scope().Lookup("s").Type())
}
var src = `package p
var s = "Hello, world"
`
package p ("path/to/pkg") string
パッケージのトップレベルに定義された s
という変数の型が string
であるという情報が得られました。
このように、型チェックおよび型情報の取得は Config
構造体をエントリポイントとしてパッケージごとに行います。結果は types.Package 構造体と、ここでは登場しませんでしたが types.Info 構造体に格納されます。
4.2. パッケージのインポート
先ほどの例では Config.Importer
を設定していました。これは types.Importer
という型を持つフィールドです。
type Importer interface {
// Import returns the imported package for the given import
// path, or an error if the package couldn't be imported.
// Two calls to Import with the same path return the same
// package.
Import(path string) (*Package, error)
}
types.Importer
は、パッケージのパスを解決し、そのパッケージに関する型レベルの情報を返すインタフェースです。具体的には、コンパイルされたパッケージオブジェクトを GOPATH
から探し出し、解析するのが仕事です。
この具体的な実装を提供するのが go/importer
パッケージです。importer.Default()
は実行中のバイナリのコンパイラ(runtime.Compiler
)に対応するインポートの実装を返します。
func Default() types.Importer
以下はパッケージを読み込み、そのパッケージが公開している名前および依存しているパッケージの情報を印字する例です。
package main
import (
"fmt"
"go/importer"
)
func main() {
pkg, _ := importer.Default().Import("log")
fmt.Println(pkg.Scope().Names())
fmt.Println(pkg.Imports())
}
[Fatal Fatalf Fatalln Flags LUTC Ldate Llongfile Lmicroseconds Logger Lshortfile LstdFlags Ltime New Output Panic Panicf Panicln Prefix Print Printf Println SetFlags SetOutput SetPrefix init] [package io ("io") package sync ("sync") package time ("time")]
プログラムの型チェックにはインポートしているパッケージがどんな名前と型を提供するのか知る必要があるため、Config
構造体の Importer
フィールドという形でその実装を指定します。
4.3. types.Config
types.Config
構造体が go/types
パッケージのエントリポイントとなります。
type Config struct {
// If IgnoreFuncBodies is set, function bodies are not
// type-checked.
IgnoreFuncBodies bool
// If FakeImportC is set, `import "C"` (for packages requiring Cgo)
// declares an empty "C" package and errors are omitted for qualified
// identifiers referring to package C (which won't find an object).
// This feature is intended for the standard library cmd/api tool.
//
// Caution: Effects may be unpredictable due to follow-up errors.
// Do not use casually!
FakeImportC bool
// If Error != nil, it is called with each error found
// during type checking; err has dynamic type Error.
// Secondary errors (for instance, to enumerate all types
// involved in an invalid recursive type declaration) have
// error strings that start with a '\t' character.
// If Error == nil, type-checking stops with the first
// error found.
Error func(err error)
// An importer is used to import packages referred to from
// import declarations.
// If the installed importer implements ImporterFrom, the type
// checker calls ImportFrom instead of Import.
// The type checker reports an error if an importer is needed
// but none was installed.
Importer Importer
// If Sizes != nil, it provides the sizing functions for package unsafe.
// Otherwise &StdSizes{WordSize: 8, MaxAlign: 8} is used instead.
Sizes Sizes
// If DisableUnusedImportCheck is set, packages are not checked
// for unused imports.
DisableUnusedImportCheck bool
}
各フィールドを調整することで、Check()
による型チェックを行う際の挙動をカスタマイズできます。
特に、Error func(err error)
は型チェックの際に生じたエラーを全て受け取るコールバックとして便利です。これが nil である場合、最初のエラーが起きた時点で型チェックが停止します。
4.4. 型チェックのエラー
型チェック時のエラーは types.Error
構造体によって表現されます。
type Error struct {
Fset *token.FileSet // file set for interpretation of Pos
Pos token.Pos // error position
Msg string // error message
Soft bool // if set, error is "soft"
}
エラーの起きた位置情報に加え、Soft
フィールドを持っています。このフィールドは、当該のエラーが「ソフト」であるかどうかを示します。ソフトなエラーは、型チェックそのものには影響を与えません。具体的には、以下のエラーです。
-
import
されたパッケージが使用されていない -
定義された変数が使用されていない
-
ラベルが利用されていない・重複している
-
:=
の左辺に新しい変数が登場していない -
init
関数の本体が存在しない -
C形式の
for
文の後処理文で変数を定義しようとしている
……for i := 0; i < 10; i, j := i+1, i {
のような形。構文解析の時点では受けつけてしまいます
以下で、ソフトなエラーとそうでない重篤なエラーの例を確認できます。
package main
import (
"fmt"
"go/ast"
"go/importer"
"go/parser"
"go/token"
"go/types"
)
func main() {
fset := token.NewFileSet()
f, _ := parser.ParseFile(fset, "example.go", src, parser.Mode(0))
conf := types.Config{
Importer: importer.Default(),
Error: func(err error) {
fmt.Printf("soft=%-5v %s\n", err.(types.Error).Soft, err)
},
}
_, err := conf.Check("path/to/pkg", fset, []*ast.File{f}, nil)
fmt.Println(err)
}
var src = `package p
import "log"
func main() {
var s, t string
s + 1
foo = 42
}
`
soft=false example.go:7:6: cannot convert 1 (untyped int constant) to string soft=false example.go:8:2: undeclared name: foo soft=true example.go:6:9: t declared but not used soft=true example.go:3:8: "log" imported but not used example.go:7:6: cannot convert 1 (untyped int constant) to string
4.5. パッケージの型情報
types.Config.Check()
によって得られる型情報は、パッケージに対応する types.Package
と、補助的で詳細な情報である types.Info
で表現されます。
4.5.1. types.Package
type Package struct {
// Has unexported fields.
}
types.Package
構造体は公開されたフィールドを持たないため、メソッドからアクセスします。
あまり意味のない例ですが、以下では $GOROOT/src/cmd/cover
ディレクトリのソースコードの型チェックを行っています。
package main
import (
"fmt"
"go/ast"
"go/importer"
"go/parser"
"go/token"
"go/types"
"path/filepath"
"runtime"
)
func main() {
path := "cmd/cover"
fset := token.NewFileSet()
aPkgs, _ := parser.ParseDir(fset, filepath.Join(runtime.GOROOT(), "src", path), nil, parser.Mode(0))
conf := types.Config{Importer: importer.Default()}
for _, aPkg := range aPkgs {
files := []*ast.File{}
for _, f := range aPkg.Files {
files = append(files, f)
}
pkg, _ := conf.Check(path, fset, files, nil)
fmt.Printf("path=%v name=%v\n", pkg.Path(), pkg.Name())
}
}
path=cmd/cover name=main path=cmd/cover name=main_test
4.6. typesにおけるスコープ
Path
や Name
などの基本的な情報の他に有用なのは、解析されたパッケージのスコープ情報でしょう。
func (pkg *Package) Scope() *Scope
type Scope struct {
// Has unexported fields.
}
types.Scope
構造体は型におけるスコープを表します。スコープは基本的に、そこに所属する名前から、それが表すオブジェクト(→ オブジェクト)へのマッピングであると考えられます。
スコープは階層構造になっていて、Parent()
および Child()
メソッドによりその親(ひとつ外側のスコープ)や子にアクセスできます。
func (s *Scope) Parent() *Scope
func (s *Scope) Child(i int) *Scope
4.6.1. ユニバーススコープ
最も外側のスコープをユニバーススコープと呼ぶことは前に述べたとおりですが、ast
パッケージと違い types
パッケージにはこれが定義されています。
以下の例では、ユニバーススコープに定義されている名前を列挙しています。
package main
import (
"fmt"
"go/types"
)
func main() {
fmt.Println(types.Universe.Names())
}
[append bool byte cap close complex complex128 complex64 copy delete error false float32 float64 imag int int16 int32 int64 int8 iota len make new nil panic print println real recover rune string true uint uint16 uint32 uint64 uint8 uintptr]
組み込みの関数や型がユニバーススコープに属していることが分かります。
4.7. typesにおけるオブジェクト
types
パッケージにおけるオブジェクトは、ast
パッケージにおけるそれと異なり、インタフェースとして表現されています。また、より詳しい情報を持ちます。
type Object interface {
Parent() *Scope // scope in which this object is declared
Pos() token.Pos // position of object identifier in declaration
Pkg() *Package // nil for objects in the Universe scope and labels
Name() string // package local object name
Type() Type // object type
Exported() bool // reports whether the name starts with a capital letter
Id() string // object id (see Id below)
// String returns a human-readable string of the object.
String() string
// Has unexported methods.
}
ast.Object
における Kind
フィールドに対応するものを持たないため、type switchでその種類を判別することになります。typesパッケージで表現されるオブジェクトの種類と、対応するデータ型は以下のようになっています。
-
組み込み関数(
*types.Builtin
) -
定数(
*types.Const
) -
関数(
*types.Func
) -
ラベル(
*types.Label
) -
nil
(*types.Nil
) -
インポートされたパッケージ(
*types.PkgName
) -
宣言された型(
*types.TypeName
) -
宣言された変数など(
*types.Var
)
これらについて、以下で見ていきます。
4.7.1. types.Builtin
組み込み関数を表します。組み込み関数は決まった型を持たないため、Type()
は invalid な型を返します。
package main
import (
"fmt"
"go/types"
)
func main() {
obj := types.Universe.Lookup("append")
fmt.Printf("%v (%T)\n", obj, obj)
fmt.Printf("%v (%T)\n", obj.Type(), obj.Type())
}
builtin append (*types.Builtin) invalid type (*types.Basic)
4.7.2. types.Const
定数を表します。Val()
メソッドは、その定数値を表す go/constant
パッケージの値を返します。
func (obj *Const) Val() constant.Value
定数式は型チェック時に評価され、値としてオブジェクトに保持されます(TODO: 定数畳み込み)。
package main
import (
"fmt"
"go/ast"
"go/importer"
"go/parser"
"go/token"
"go/types"
)
func main() {
fset := token.NewFileSet()
f, _ := parser.ParseFile(fset, "example.go", src, parser.Mode(0))
conf := types.Config{Importer: importer.Default()}
pkg, _ := conf.Check("path/to/pkg", fset, []*ast.File{f}, nil)
fmt.Println(pkg.Scope().Lookup("c2").(*types.Const).Val())
}
var src = `package p
const (
s = "Hello, " + "world"
c1 = complex(iota, float64(len(s)))
c2
)
`
(2 + 12i)
4.7.3. types.Func
関数を表します。より詳細には、
-
宣言された関数
-
具象メソッド
-
(インタフェースの)抽象メソッド
です。
以下で、それぞれの場合(トップレベルの関数 F
、型 *T
のメソッド F
、インタフェース I
のメソッド F
)の出現を確認しています。
package main
import (
"fmt"
"go/ast"
"go/importer"
"go/parser"
"go/token"
"go/types"
)
func main() {
fset := token.NewFileSet()
f, _ := parser.ParseFile(fset, "example.go", src, parser.Mode(0))
conf := types.Config{Importer: importer.Default()}
pkg, _ := conf.Check("path/to/pkg", fset, []*ast.File{f}, nil)
// type assersions for clarity
var (
objF types.Object = pkg.Scope().Lookup("F").(*types.Func)
objT types.Object = pkg.Scope().Lookup("T").(*types.TypeName)
objI types.Object = pkg.Scope().Lookup("I").(*types.TypeName)
) (1)
fmt.Println(objF)
fmt.Println(objT.Type().(*types.Named).Method(0)) (2)
fmt.Println(objI.Type().Underlying().(*types.Interface).Method(0)) (3)
}
var src = `package p
func F() {}
type T struct{}
func (*T) F() {}
type I interface {
F()
}
`
func path/to/pkg.F() func (*path/to/pkg.T).F() func (path/to/pkg.I).F()
1 | パッケージスコープ内の名前にアクセスします。F はトップレベルの関数に、T および I は型名に対応するオブジェクトとして取得します。 |
2 | 型 T に属する最初のメソッドとして、T.F() に対応するオブジェクトを取得します。 |
3 | 型 I が指すインタフェース interface { F() } の最初の(抽象)メソッドとして、I.F() に対応するオブジェクトを取得します。 |
インタフェースの抽象メソッドと、それ以外の具象メソッドに対応するオブジェクトへアクセスする方法は微妙に異なりますが、これはあとの節で詳しく触れます。
4.7.4. types.PkgName
import
宣言によってインポートされたパッケージの名前を表します。
Imported()
メソッドで、インポートされたパッケージに関する情報を保持する types.Package
構造体を得られます。これは Config.Check
で得られるのと同様のものです。
以下の例では、fmtPkg
という名前でインポートした fmt
パッケージと、同パッケージのエクスポートする Errorf
関数に対応するオブジェクトを取得しています。
インポートしたパッケージの名前はパッケージスコープではなくファイルスコープに導入されるため、後の節で説明するInfo
構造体を使ってファイルスコープを取得しています。
package main
import (
"fmt"
"go/ast"
"go/importer"
"go/parser"
"go/token"
"go/types"
)
func main() {
fset := token.NewFileSet()
f, _ := parser.ParseFile(fset, "example.go", src, parser.Mode(0))
conf := types.Config{Importer: importer.Default()}
info := types.Info{
Scopes: map[ast.Node]*types.Scope{},
}
_, _ = conf.Check("path/to/pkg", fset, []*ast.File{f}, &info)
objPkgName := info.Scopes[f].Lookup("fmtPkg").(*types.PkgName)
fmt.Println(objPkgName)
fmt.Println(objPkgName.Imported().Scope().Lookup("Errorf"))
}
var src = `package p
import fmtPkg "fmt"
func main() {
fmtPkg.Println("Hello, world")
}
`
package fmtPkg ("fmt") func fmt.Errorf(format string, a ...interface{}) error
ファイル先頭の package 節で指定された名前はスコープに導入されないので、自パッケージの名前が PkgName の形で登場することはありません。
|
4.8. types.Info
さて、types.Scope
のメソッドを使うことで、パッケージ中に登場した名前に関する情報をオブジェクトとして得ることは一応可能です。しかしこれだけでは、
-
あるオブジェクトがどこで定義されたのか
-
ある式にどんな型が与えられたのか
などについて(ただちには)知ることができません。そこで Check
関数の最後の引数に渡す構造体、types.Info
の出番となります。
type Info struct {
// Types maps expressions to their types, and for constant
// expressions, their values. Invalid expressions are omitted.
//
// For (possibly parenthesized) identifiers denoting built-in
// functions, the recorded signatures are call-site specific:
// if the call result is not a constant, the recorded type is
// an argument-specific signature. Otherwise, the recorded type
// is invalid.
//
// Identifiers on the lhs of declarations (i.e., the identifiers
// which are being declared) are collected in the Defs map.
// Identifiers denoting packages are collected in the Uses maps.
Types map[ast.Expr]TypeAndValue
// Defs maps identifiers to the objects they define (including
// package names, dots "." of dot-imports, and blank "_" identifiers).
// For identifiers that do not denote objects (e.g., the package name
// in package clauses, or symbolic variables t in t := x.(type) of
// type switch headers), the corresponding objects are nil.
//
// For an anonymous field, Defs returns the field *Var it defines.
//
// Invariant: Defs[id] == nil || Defs[id].Pos() == id.Pos()
Defs map[*ast.Ident]Object
// Uses maps identifiers to the objects they denote.
//
// For an anonymous field, Uses returns the *TypeName it denotes.
//
// Invariant: Uses[id].Pos() != id.Pos()
Uses map[*ast.Ident]Object
// Implicits maps nodes to their implicitly declared objects, if any.
// The following node and object types may appear:
//
// node declared object
//
// *ast.ImportSpec *PkgName for dot-imports and imports without renames
// *ast.CaseClause type-specific *Var for each type switch case clause (incl. default)
// *ast.Field anonymous parameter *Var
//
Implicits map[ast.Node]Object
// Selections maps selector expressions (excluding qualified identifiers)
// to their corresponding selections.
Selections map[*ast.SelectorExpr]*Selection
// Scopes maps ast.Nodes to the scopes they define. Package scopes are not
// associated with a specific node but with all files belonging to a package.
// Thus, the package scope can be found in the type-checked Package object.
// Scopes nest, with the Universe scope being the outermost scope, enclosing
// the package scope, which contains (one or more) files scopes, which enclose
// function scopes which in turn enclose statement and function literal scopes.
// Note that even though package-level functions are declared in the package
// scope, the function scopes are embedded in the file scope of the file
// containing the function declaration.
//
// The following node types may appear in Scopes:
//
// *ast.File
// *ast.FuncType
// *ast.BlockStmt
// *ast.IfStmt
// *ast.SwitchStmt
// *ast.TypeSwitchStmt
// *ast.CaseClause
// *ast.CommClause
// *ast.ForStmt
// *ast.RangeStmt
//
Scopes map[ast.Node]*Scope
// InitOrder is the list of package-level initializers in the order in which
// they must be executed. Initializers referring to variables related by an
// initialization dependency appear in topological order, the others appear
// in source order. Variables without an initialization expression do not
// appear in this list.
InitOrder []*Initializer
}
types.Info
は型チェック中に得られた、詳細な情報を保持する構造体です。
WIP |
7. ソースコードを読む
ここではGoプログラマに広く使われているツール類のソースコードを読むことで、実践においてどのようにAPIが利用されているかを見ていきます。ここで見るのはAPIの利用の仕方のベストプラクティスであるとともに、エンドユーザにどのようなインタフェースで機能を提供するべきかの実例でもあります。
FIXME: 全体的に雑 |
7.1. go doc
"go doc" はパッケージのAPIのドキュメントを閲覧する機能を提供するサブコマンドです。
go doc go/ast Node
のようにパッケージやシンボルを指定すると、そのドキュメントを表示します。
% go doc go/ast Node type Node interface { Pos() token.Pos // position of first character belonging to the node End() token.Pos // position of first character immediately after the node } All node types implement the Node interface.
パッケージ名は完全なパスでなくてもよく、その場合はGOPATH以下からマッチするものを探してきます。
% go doc template package template // import "html/template" Package template (html/template) implements data-driven templates for ... % go doc ast.Node package ast // import "go/ast" type Node interface { Pos() token.Pos // position of first character belonging to the node End() token.Pos // position of first character immediately after the node } All node types implement the Node interface.
ソースは src/cmd/doc/
以下にあります。
go doc
が行う処理は以下のように分解できます。
-
コマンドライン引数の解決
-
ソースコードの解析
-
ドキュメントの表示
これから、それぞれの処理について詳しく見ていきます。
7.1.1. 引数の解決(parseArgs
)
go doc
サブコマンドに与えられるコマンドライン引数は、ユーザの意図を表した以下のような形の文字列のリストになっています。
% go doc -h Usage of [go] doc: go doc go doc <pkg> go doc <sym>[.<method>] go doc [<pkg>].<sym>[.<method>] go doc <pkg> <sym>[.<method>]
さまざまな形式がありますが、大きく
最初に、引数で指定された要件にしたがってドキュメントの元となるソースコードを取得します。
go doc
への引数の与え方はさまざまで、パッケージを指定する方法には次の3パターンがあります:
-
カレントディレクトリのソースコードを対象にする(例:
go doc
)。 -
完全なパスで指定されたパッケージを対象にする(例:
go doc encoding/json
)。 -
指定されたパスの一部からパッケージを探しだす(例:
go doc json
)。
これに加えて、パッケージ内のシンボルおよびそのメソッドも指定されることがあります。
カレントディレクトリを対象にする場合およびパッケージが指定されている場合(aおよびb)は、go/build
のAPI build.Import
を使って簡単にソースコードの所在を示す build.Package
が得られます。
そうでない場合(c)、パスの一部が一致するパッケージを発見する必要があります。ここでも go/build
のAPIを利用し、build.Default.GOROOT
と build.Default.GOPATH
以下のディレクトリを探索します。go doc
コマンドが実行された時点でこの探索のためのgoroutineが起動していて、すばやく結果を返せるようになっています。
7.1.2. ソースコードの解析(parsePackage
)
ドキュメント情報を得るためには、ソースコードを解析する必要があります。パッケージの情報が手元にあるので parser.ParseDir
でディレクトリ内のファイルを一度に解析できますが、その際第3引数の filter
を指定して GoFiles
や CgoFiles
に含まれないものを除去します。こうすることで、実行環境(GOOS
や GOARCH
)に合わせたソースコードのみを解析対象としています。
pkgs, err := parser.ParseDir(fs, pkg.Dir, include, parser.ParseComments)
その後 doc.New
して得られた doc.Package
から Package
構造体を生成します。
7.1.3. ドキュメントの表示(Pacakge.packageDoc
など)
Package
構造体の以下のメソッドがモードに応じて選ばれ、ユーザに表示される内容を生成します。
-
packageDoc
… パッケージのドキュメントを表示 -
symbolDoc
… シンボル(型、関数、メソッドなど)のドキュメントを表示 -
methodDoc
… ある型のメソッドのドキュメントを表示
ドキュメントを表示するなかで対象のソースコードにおける定義が必要になった場合(go doc go/ast File
など)、format.Node
で生成されます。ast.FuncDecl
を表示する際は Body
フィールドに nil
を代入することで、宣言のみが表示されるようにしています。
7.2. gofmt
Goにおいて特徴的なコマンドで、ソースコードを標準的なスタイルにフォーマットします。ほとんどのGoプログラマが利用しているコマンドです(たぶん)。
7.2.1. gofmtの主なインタフェース
何もオプションを指定しない場合、gofmtは引数のファイルまたは標準入力をフォーマットして、標準出力に印字します。
よく利用されるのは -w
で、これが指定された場合結果は引数のファイルを上書きするのに使われます。また -d
では、入力の内容と結果の diff
が表示されます。
ソースコードは src/cmd/gofmt
以下にあります。
7.2.2. ソースコードの印字
gofmtのメインの処理はソースコードの整形と印字です。これを担当するのが processFile
(internal.go:75
)関数です。この関数は go/source
パッケージの format.Source
関数とよく似ていて、違いは次以降の項で触れるソースコードの書き換えや標準入力の扱いなどです。
入力をソースコードとして解析するのが parse
関数(internal.go:23
)で、内部では parser.ParseFile
を利用しています。fragmentOk
引数が true
である場合、宣言のリストや式などファイルとしては不完全なソースコードも解析できるよう、ソースコードの先頭に package p;
を追加したり、ソースを func _() { … }
で囲んだりという処理がなされます。gofmt
では標準入力からソースコードが与えられた場合にこのモードを使います。
こうやって得られた抽象構文木は format
関数(internal.go:94
)により整形されます。実際の整形処理は printer.printer
によって行われ、ノードの持つソースコード中の位置を利用して、入力を尊重しつつ標準のフォーマットにしたがってソースコードが文字列化されます。抽象構文木とは別に得られたコメントも、ここでソースコードに織り込まれます。
7.2.3. gofmt -s
: ソースをシンプルにする
通常gofmtが行うのはソースコードの整形のみで、抽象構文木の構造が変わるような変更を行いませんが、-s
や -r
を指定することでより積極的なフォーマットが可能です。
このオプションが指定された場合、ソースコードの印字の前に simplify(f *ast.File)
が呼び出され、構文木の書き換えが行われます。以下でその流れを見ていきましょう。
最初に const ()
のような空の宣言が取り除かれます。これは ast.File.Decls
を書き換えることで実現できます(simplify.go:137-146
)。
それから、構文木を辿って単純化が行われます。適用されるのは以下のルールになります。
-
複合リテラルの単純化
-
スライスの単純化(
s[a:len(s)]
→s[a:]
) -
for/range文の単純化(
for _ = range
→for range
)
スライスとfor/range文の単純化は比較的単純な作業で、type assertionを利用しながら構文木を探索し、求める構造に合致するノードを発見します。合致した場合、消し去りたい部分を表すフィールドに nil
を代入することで結果のソースコードから削除しています。
複合リテラルの単純化の内部で使用されているのが func match(m map[string]reflect.Value, pattern, val reflect.Value) bool
関数です(rewrite.go:160
)。match()
は構文ノード(ast.Node
)への reflect.Value
を2つ引数に取り、それらが一致するかをチェックします。
match()
には2種類のモードがあり、
-
引数
m
がnil
の場合は、2つのreflect.Value
の表すast.Node
が同じ値であるかを再帰的にチェックします。-
この際、ソースコード中の位置などの値が異なっていても基本的に無視します。
-
-
引数
m
が非nil
の場合には、pattern
引数の表すパターンにval
が一致するかを見ます(後述)。
ここでは前者の場合のみが起こり、複合リテラルの外側と内側の型(を表す構文ノード)が等しい場合には内側の型を消去する、という処理を行っています。
7.2.4. gofmt -r
: ソースを書き換える
さらに高度な機能として、引数に指定されたパターンに従ってソースコードを書き換えることもできます。以下のように ->
を2つのGoの式で挟んだ形式によってコードの書き換え規則を指定します。
gofmt -r 'a[b:len(a)] -> a[b:]' ...
書き換え規則の入力は、まず2つの ast.Expr
として解釈されます(rewrite.go:19-32
)。
実際の処理は rewriteFile
(rewrite.go:57-82
)です。内部では、構文木を表すデータ構造を reflect
APIによって探索しながら rewriteVal
で書き換えを行います(rewrite.go:64-77
)。探索中に出現した構文ノードがパターンに一致した場合、マッチ結果とユーザの入力にしたがってノードを置き換えます。
前述のように、書き換えは書き換え元のパターン(a[b:len(a)]
)と書き換え先(a[b:]
)の組によって指定されます。パターンはGoの式になっていて、中でも小文字1文字からなる識別子は「ワイルドカード」として扱われ、任意の式にマッチします。例えば a + b
というパターンは、以下のような式にマッチします。
f.g(x) + "y" // a=f.g(x), b="y"
(1 / 2) + (3 + 4) // a=(1 / 2), b=(3 + 4) および a=3, b=4
2番目の例のように、パターンの探索は再帰的に行われます。パターンとの一致のチェックには、前述の match
関数を用います。ワイルドカードに一致した構文ノードは引数 m
に格納され、その後のチェックと書き換え後のノードの生成に利用されます。
7.3. stringer
stringer
は定数の文字列化のためのコードを自動生成するコマンドです。以下のようにして入手できます。
$ go get golang.org/x/tools/cmd/stringer
go doc golang.org/x/tools/cmd/stringer
にある例を見るのが分かりやすいでしょう。以下のように定数を定義したソースコードを書いたとします。この定数を表示するため fmt.Print(Uni)
などとしても、2
と印字されるだけでどんな意味を持った値なのかの情報に欠けてしまっています。
fmt
パッケージのメソッドは、値が fmt.Stringer
インタフェースを満たしていればその String()
メソッドを利用するので、ここで Sushi.String()
が定数の名前を返すようにすればいいはずです。
type Stringer interface {
String() string
}
見るからに単調な作業になので、プログラム的に生成することを考えますよね。そこで stringer
の出番です。以上のような内容のソースコード sushi.go
に対して stringer -type Sushi sushi.go
を実行すると、次のソースコードが pill_string.go
として生成されます。
// Code generated by "stringer -type Sushi sushi.go"; DO NOT EDIT
package sushi
import "fmt"
const _Sushi_name = "MaguroIkuraUniTamago"
var _Sushi_index = [...]uint8{0, 6, 11, 14, 20}
func (i Sushi) String() string {
if i >= Sushi(len(_Sushi_index)-1) {
return fmt.Sprintf("Sushi(%d)", i)
}
return _Sushi_name[_Sushi_index[i]:_Sushi_index[i+1]]
}
これで Sushi
型の値を文字列化したときの情報量が増しました。fmt.Print(Uni)
は Uni
を印字します。コードは複雑なことをしているように見えますが、基本的に値に対応する定数名を返しているだけです。
7.3.1. 処理の流れ
stringer
は以下の流れでその仕事を行います。
-
ディレクトリ名やファイル名の形で引数に指定されたソースコードを読み込む。
-
コード中から指定された型を発見し、文字列化のために必要な情報を収集する。
-
文字列化のためのソースコードを生成する。
主だった処理は Generator
という型のメソッドになっています。Generator
は中に Package
型の構造体を保持していて、コードを解析して得られた情報
7.3.2. ソースのロード
ソースコードとして引数には1つのディレクトリか複数のファイルが指定できますが、どちらの場合も Generator.parsePackage
(stringer.go:231
)を通ります。
ソースコードファイルのリストは構文解析されたのち、go/types
のAPIで型チェックされます。この際、型の定義の情報も収集するようになっており、これが後の工程で必要になってきます。定義の情報は map[*ast.Ident]types.Object
の形で保持され、ソースコード中に出現する識別子の、その言語上の役割や型などの情報が得られます。
type Object interface {
Parent() *Scope // scope in which this object is declared
Pos() token.Pos // position of object identifier in declaration
Pkg() *Package // nil for objects in the Universe scope and labels
Name() string // package local object name
Type() Type // object type
Exported() bool // reports whether the name starts with a capital letter
Id() string // object id (see Id below)
// String returns a human-readable string of the object.
String() string
// Has unexported methods.
}
7.3.3. 型の発見(Generator.generate()
)
コマンドライン引数に指定された型名を、ソースコードから探し出します。
構文解析されたソースコードを走査し、Generator.genDecl
で指定された名前の型を持つ定数の宣言グループを発見します。Goにおいて定数の宣言はグループ化でき、値や型を指定しない場合には、直前の値や型と同内容の宣言をしたものとみなされます(Constant declarations)。特に iota
というキーワードを使って値を宣言することで、連続する値を持つ定数を宣言できます。
const (
Maguro Sushi = iota
Ikura
Uni
Tamago
)
宣言された定数を発見したあと、前の段階で得られた型の定義を収集します。ここではその型が整数型であることをチェックし、その場合、名前や値を Value
として登録します。
7.3.4. 文字列化処理の生成
最後に、発見された定数の値と名前をもとに、定数の文字列化を提供するコードを生成します。
戦略として、値が連続する定数の文字列表現をひとつの長い文字列に連結し、定数の文字列化の際にはそのスライスを返すようにします。この部分のコード生成は単純な文字列連結で実現されています。
生成されるのは前述の sushi_string.go のようなコードです。このソースコード文字列に対して go/format.Source
を行って得られた文字列が、出力先のファイルに書き込まれます。ファイル名は、対象の型の名前と入力であるソースコードのディレクトリを元に sushi_string.go
といった名前に決まります。
7.4. goimports
goimports
は与えられたソースコードを編集し、import
宣言の追加や削除忘れの面倒を見てくれるツールです。その際 gofmt
相当のことも行うので、gofmt
代わりに利用している人も多いのではないでしょうか。
goimports
は以下のコマンドで入手できます:
go get golang.org/x/tools/cmd/goimports
7.4.1. import
の解決
goimports
のメイン部分は fixImports
(gosource:TODO)です。この関数は与えられた解析済みのソースコードから未解決の識別子を探しだし、必要なパッケージを GOPATH
以下から探しだして import
宣言を挿入します。
具体的に行っていることは:
-
構文木を探索し、
-
x.y
の形の参照を収集する -
import
宣言によってファイルスコープに導入された名前を収集する-
importPathToName
-
-
-
その後、
-
一度も参照されていない
import
宣言を削除する -
未解決の参照を修正できるパッケージを探し出す
-
findImport
-
-
上記のパッケージに対応する
import
宣言を挿入する
-
という流れです。
構文木の探索
構文木の探索は ast.Walk
を使って実装されています。goimports
中では、 ast.SelectorExpr
と ast.ImportSpec
の2種類の構文要素が興味ある対象です。
ast.SelectorExpr
は expr.sel
の形の式で、:セレクタ(selector)と呼ばれています。expr
には任意の式が入り得ますが、ここではインポートされたパッケージの呼び出しを発見したいだけなので expr
が識別子(ast.Ident
)であるかのチェックを行っています。こうして発見されたパッケージ名へのセレクタのうち、オブジェクトが未解決のものを収集します。
ast.ImportSpec
は import
宣言ひとつ分に対応します。例えば以下のような import
宣言には4つの ImportSpec
が含まれています。
import (
"fmt"
. "math"
_ "net/http/pprof"
)
import logPkg "log"
ここで注目すべきは名前なしの import "fmt"
です。パッケージの import
によってファイルに導入される名前は、そのインポートパスではなくパッケージ中の宣言に依ります:
import "github.com/motemen/go-astutil" // "astutil" という名前が導入される
この解決を行うのが importPathToName
です。ここでは go/build.Import
を利用してパッケージに相当するソースコードを GOPATH
以下から発見します。
go/build
は、go build
コマンドが行うように、GOOS
や GOARCH
環境変数、ビルドタグに基づいてパッケージやソースコードを探しだすためのAPIを提供します。
ロードに失敗した場合はインポートパスの末尾部分が代替として使用されます。
import
宣言の挿入
続いて、上記の過程で収集された未解決の識別子からパッケージを探し出し、import
宣言を挿入します。このメイン部分、パッケージを探索するのが findImportGoPath
です。パッケージ名と、そのパッケージによって提供されているべき名前から、パッケージのインポートパスを探し出します。
最初に標準パッケージのAPIとの一致がチェックされます。これはあらかじめテーブルが生成されているので高速にマッチします。
その後、ユーザによってインストールされたパッケージが探索されます。パッケージは最初に pkgIndexOnce.Do(loadPkgIndex)
でインデックスします。go/build.Default.SrcDirs()
以下の、Goのソースコードを格納しているディレクトリに対して先ほどの importPathToName
でパッケージ名の解決を行ってテーブルを作ります。
こうやって生成されたテーブルに対し、期待する識別子を公開しているパッケージを探し出します。build.ImportDir
で得られたディレクトリ中のファイルを解析して(loadExportsGoPath
)、エクスポートされてるものを発見して突き合わせます。
7.6. gddo
GoDoc.orgはサードパーティ製のものを含むGoライブラリのドキュメントを閲覧できるウェブサイトです。ここでは以下のようなURLでGitHubなどにホストされているGoライブラリにアクセスできます。
標準ライブラリのドキュメントも同じように閲覧できます。
ソースコードはhttps://github.com/golang/gddoにホストされています(“gddo” はGoDocDotOrgの頭文字を取ったものです)。
gddoはユーザから見るとドキュメントを表示するだけのサイトですが、裏側でソースコードのクロールを行うなど複雑な機能を持ち合わせています。ここでは指定されたドキュメントの表示機能のみに絞ってソースコードを読んでみます。
この機能を受け持つのが servePackage
です。HTTPリクエストにしたがってパッケージのドキュメントを返すのが getDoc
(main.go:74
)で、gddo/doc.Package
を返します。gddo/doc.Package
はあるパッケージのドキュメントに相当し、パッケージの提供する関数や型とそのドキュメントなど、godoc.orgで閲覧できるドキュメントのHTMLを生成するのに必要な主要な情報を保持しています。インポートパスに基づいてRedisからパッケージのドキュメントを取り出しますが、データが存在しないか古い場合、外部にホストされているソースコードからドキュメントの生成に必要な情報を取得します。
リクエストされたパッケージは crawlDoc
から github.com/golang/gddo/doc.Get
を経由して呼び出される github.com/golang/gddo/gosrc.Get
によって、そのパッケージがホストされているリモートのVCSから取得されます。
doc.Get
は解析済みのパッケージのドキュメントを返す。gosrc.Get
は gosrc.Directory
という仮想的なソースコードディレクトリを返します。それを変換するのが newPackage
。
-
getStatic
-
“getStatic gets a diretory from a statically known service”
-
githubとか。gosrc/github.go
-
Directoryを得るnewPackage
-
getDynamic
-
getVCSDir(vcs.go)
7.6.1. 外部サービスへの対応
GitHubやBitBucketなど有名どころでAPIも提供されているサービスに対しては、それぞれからソースコードを取得する処理が実装されています。
各サービスは gosrc.service
構造体として表現されます:
type service struct {
pattern *regexp.Regexp
prefix string
get func(*http.Client, map[string]string, string) (*Directory, error)
getPresentation func(*http.Client, map[string]string) (*Presentation, error)
getProject func(*http.Client, map[string]string) (*Project, error)
}
get
、getPresentation
、getProject
はそれぞれ Directory
、Presentation
、Project
型の値を返します。
Directory
が主に利用される型となります。これはパッケージのインポートパスやパッケージを構成するファイル名を保持しています。
type Directory struct {
// The import path for this package.
ImportPath string
// Import path of package after resolving go-import meta tags, if any.
ResolvedPath string
// Import path prefix for all packages in the project.
ProjectRoot string
// Name of the project.
ProjectName string
// Project home page.
ProjectURL string
// Version control system: git, hg, bzr, ...
VCS string
// Version control: active or should be suppressed.
Status DirectoryStatus
// Cache validation tag. This tag is not necessarily an HTTP entity tag.
// The tag is "" if there is no meaningful cache validation for the VCS.
Etag string
// Files.
Files []*File
// Subdirectories, not guaranteed to contain Go code.
Subdirectories []string
// Location of directory on version control service website.
BrowseURL string
// Format specifier for link to source line. It must contain one %s (file URL)
// followed by one %d (source line number), or be empty string if not available.
// Example: "%s#L%d".
LineFmt string
// Whether the repository of this directory is a fork of another one.
Fork bool
// How many stars (for a GitHub project) or followers (for a BitBucket
// project) the repository of this directory has.
Stars int
}
Presentation
はプレゼンテーション用の機能で、go-talks.appspot.orgでのみ利用されているものです。
Project
は Description
だけを持つ構造体で、Goのドキュメントレベルでパッケージの説明が得られなかった場合に、サービスで設定されている説明を利用するためのものです。
以下のサービスがあらかじめ実装されています。
-
BitBucket(
bitbucket.go
) -
Launchpad(
launchpad.go
) -
Google(
google.go
) -
GitHubおよびGist(
github.go
)
例:GitHub
github.go:20
addService(&service{
pattern: regexp.MustCompile(`^github\.com/(?P<owner>[a-z0-9A-Z_.\-]+)/(?P<repo>[a-z0-9A-Z_.\-]+)(?P<dir>/.*)?$`),
prefix: "github.com/",
get: getGitHubDir,
getPresentation: getGitHubPresentation,
getProject: getGitHubProject,
})
getGitHubDir
(github.go:51
)でわりとストレートにファイルを一覧してる。
7.6.2. 仮想的なソースコードディレクトリからドキュメントを生成する
メソッド gddo/doc.newPackage
が、仮想的なソースコードディレクトリである gosrc.Directory
からドキュメントである gddo/doc.Package
を生成します。gosrc.Directory
はインポートパスや、ファイル名とそのデータを全て保持しています。
ディレクトリに含まれるファイルは、すべてが必要なファイルであるとは限りません。例えば spec_linux.go
と spec_windows.go
は共存し得ないし、_test.go
で終わるファイルはテスト用なのでドキュメントには不要です。また、ソースコード中のビルドタグもコンパイルやドキュメント生成にあたって留意しなくてはなりません。そこで go/build
のAPIを利用します。
go/build
のAPIは、特定の GOOS
や GOARCH
下で、あるパッケージを構成するソースコードを一覧するものでした。
通常はローカルのファイルシステムに対してファイルの探索を行うのですが、build.Context
のファイルシステムへのアクセスに相当するフィールドを書き換えることで
type Context struct {
GOARCH string // target architecture
GOOS string // target operating system
GOROOT string // Go root
GOPATH string // Go path
CgoEnabled bool // whether cgo can be used
UseAllFiles bool // use files regardless of +build lines, file names
Compiler string // compiler to assume when computing target paths
// The build and release tags specify build constraints
// that should be considered satisfied when processing +build lines.
// Clients creating a new context may customize BuildTags, which
// defaults to empty, but it is usually an error to customize ReleaseTags,
// which defaults to the list of Go releases the current release is compatible with.
// In addition to the BuildTags and ReleaseTags, build constraints
// consider the values of GOARCH and GOOS as satisfied tags.
BuildTags []string
ReleaseTags []string
// The install suffix specifies a suffix to use in the name of the installation
// directory. By default it is empty, but custom builds that need to keep
// their outputs separate can set InstallSuffix to do so. For example, when
// using the race detector, the go command uses InstallSuffix = "race", so
// that on a Linux/386 system, packages are written to a directory named
// "linux_386_race" instead of the usual "linux_386".
InstallSuffix string
// JoinPath joins the sequence of path fragments into a single path.
// If JoinPath is nil, Import uses filepath.Join.
JoinPath func(elem ...string) string
// SplitPathList splits the path list into a slice of individual paths.
// If SplitPathList is nil, Import uses filepath.SplitList.
SplitPathList func(list string) []string
// IsAbsPath reports whether path is an absolute path.
// If IsAbsPath is nil, Import uses filepath.IsAbs.
IsAbsPath func(path string) bool
// IsDir reports whether the path names a directory.
// If IsDir is nil, Import calls os.Stat and uses the result's IsDir method.
IsDir func(path string) bool
// HasSubdir reports whether dir is a subdirectory of
// (perhaps multiple levels below) root.
// If so, HasSubdir sets rel to a slash-separated path that
// can be joined to root to produce a path equivalent to dir.
// If HasSubdir is nil, Import uses an implementation built on
// filepath.EvalSymlinks.
HasSubdir func(root, dir string) (rel string, ok bool)
// ReadDir returns a slice of os.FileInfo, sorted by Name,
// describing the content of the named directory.
// If ReadDir is nil, Import uses ioutil.ReadDir.
ReadDir func(dir string) ([]os.FileInfo, error)
// OpenFile opens a file (not a directory) for reading.
// If OpenFile is nil, Import uses os.Open.
OpenFile func(path string) (io.ReadCloser, error)
}