はじめに
この本について
この本では、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:20addService(&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)
}