はじめに

この本について

この本では、Go言語の標準パッケージのうち go/ で始まるものについて、実例を交えながら紹介します。ドキュメントとソースコードの中間となることを目指し、実例を交えつつ、ドキュメントだけからはただちに知ることのできないAPI同士の関連や全体としての使い方に焦点を当てます。

なぜ「GoのためのGo」なのか

Go言語はシンプルさを念頭にデザインされた言語です。仕様は単純明瞭さのために小さく収められていますが、そのため表現力に欠けているとか、コードが冗長になるという印象を持つ人も多いでしょう。有名なところでは、ジェネリクスや例外といった機能が(今のところ)存在しないことが問題にされることが多いようです。

一般に、ソフトウェアエンジニアリングというものは書かれる言語だけに依るものではありません。視点を拡げてGoを取りまくツール群を含めて見てみると、go fmtgoimports といったツールが広く使われていること、また go generate コマンドの存在などを見ても、Goという言語には、人間のプログラミングを機械によってさまざまな面から補助しようという態度があります。

go/* 標準パッケージは、Goのプログラムを対象とし、解析や操作を行うためのAPIを提供します。これらのAPIを利用し、一種のメタプログラミングを行うことで、Goプログラミングの力をより引き出せるようになるはずです。

対象バージョン

この本の内容は、Goバージョン1.7.4に基いています。

1. 構文解析

Goに限らず、プログラムのソースコードは、与えられた状態ではただの文字列でしかありません。

ソースコードをプログラムにとって意味のある操作可能な対象とするには、まずソースを構文解析して抽象構文木(Abstract Syntax Tree; AST)に変換し、Goのデータ構造として表現する必要があります。

いったん抽象構文木を手元に得てしまえば、任意のソースコードをプログラムから扱うのはとても簡単です。

以下では、

  • Goのソースコードの抽象構文木がどのようにして得られるのか、

  • 抽象構文木において、それぞれの構文要素がどのように表現されているのか

といったことを見ていきます。

1.1. 式の構文解析

Goのソースコードの構文解析を行うには、標準パッケージの go/parser を使用します。 まずはGoの式(expression)を解析するところからはじめましょう。

リスト 1. parseexpr.go
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)(0xc420012dc0), OpPos:3, Op:14, Y:(*ast.UnaryExpr)(0xc420012e00)}

二項演算子 * の左の項である aX*ast.Ident)として、右の項である -1Y*ast.UnaryExpr)として表現されていそうだ、ということが見て取れると思います。

1.1.1. ast.Print

%#v による表示でも大まかには構文木のノードの様子を知ることができますが、定数値の意味やさらに深いノードの情報には欠けています。構文木をさらに詳細に見ていくには、ast.Print 関数が便利です:

リスト 2. parseexpr-print.go
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
}

二項演算の左右の式である XYast.Expr として定義されていることがわかります。先ほどの例では *ast.Ident*ast.UnaryExpr がその具体的な値となっていました。

これらの構造体を含め、すべてのGoの式に対応する構文ノードは ast.Expr インタフェースを実装しています。

type Expr interface {
    Node
    exprNode()
}

ast.Expr は(埋め込まれている ast.Node を除けば)外部に公開されないメソッドで構成されています。そのため、ast パッケージ外の型が ast.Expr を実装することはありません。

exprNode() は実際にはどこからも呼ばれないメソッドです。そのため、ast.Expr はその振る舞いに関する情報を提供しない、分類用のインタフェースであるといえます。同様に、文や宣言に対応するインタフェース(ast.Stmtast.Decl)も定義されています。埋め込まれている ast.Node インタフェースも含め、これらについて詳しくは構文ノードの実装で見ます。

1.2. ファイルの構文解析

ここまで式の構文解析を例にとって見てきましたが、実践においては、Goのソースコードはファイルやパッケージの単位で扱うことが普通です。ここからはファイル全体を構文解析する方法を見ていきます。

1.2.1. ファイルの構造

まず、Goのソースコードファイルの構造を確認しておきましょう。

The Go Programming Language Specification - Source file organization によれば、ひとつのファイルの中には

  1. パッケージ名

  2. import

  3. 値や関数などトップレベルの宣言

が、この順番で現れることになっています。

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 はソースコードの位置情報にだけ使われます。srcinterface{} ですが、指定できるのは string[]byteio.Reader のいずれかのみです。

第一引数の fset は構文解析によって得られた構文木のノードの詳細な位置情報を保持する token.FileSet 構造体へのポインタです。詳しくはソースコード中の位置で説明しますが、基本的に token.NewFileSet() で得られるものを渡せば十分です。

最後の引数 mode では構文解析する範囲の指定などが行えます。後でコメントとドキュメントを扱うときに少し触れます。

リスト 3. parsefile.go
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
}

他にもいろいろなフィールドがありますが、

で解説します。

1.2.4. 構文木の探索

構文ノードのインタフェースで述べたように、構文木のノードは ast パッケージのインタフェースとして得られます。そのため、具体的な内容を知るにはtype assertionやtype switchを用いなければなりません。これを手で丁寧に書いていくのは大変で間違いも起きがちですが、ast.Inspect 関数で構文ノードに対する(深さ優先)探索を行えます。

func Inspect(node Node, f func(Node) bool)

node から始まり、子ノードを再帰的に探索しつつにコールバック関数 f が呼ばれます。子ノードの探索を終えるごとに、引数 nil でコールバックが呼ばれます。コールバック関数では false を返すことで、そのノードの子供以下への探索を打ち切ることができます。

以下は先ほどのソースコードファイル中の識別子を一覧する例です。訪問したノードの具体的な型を知るために、type assertionをおこなっています。

リスト 4. listidents.go
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)。importtype など

ast.Stmt

文(statement)。ifswitch など

ast.Expr

式(expression)。識別子や演算、型など

ファイルやコメントなど、これらに分類されない構文ノードも存在します。

以下でこれらサブインタフェースと、その実装のうち主要なものを見ていきます。

ast.Nodeの階層ast.Node を実装する型の完全な一覧を確認できます。

1.3.2. ast.Decl

ast.Decl インタフェースはGoソースコードにおける宣言(declaration)に対応する構文木のノードを表します。Goの宣言は

  • パッケージのインポート(import

  • 変数および定数(varconst

  • 型(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 が再利用されている形です。

上記の事情にも関わらず、go/parser は複数の組からなるレシーバをエラーなく解析します! コメントによると、シンプルさとロバスト性のためだということです。

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

関数以外の宣言、importconstvartypeast.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 の要素の型 表す構文

token.IMPORT

*ast.ImportSpec

import 宣言

token.CONST

*ast.ValueSpec

const 宣言

token.TYPE

*ast.TypeSpec

type 宣言

token.VAR

*ast.ValueSpec

var 宣言

これらの宣言には、以下のようにグループ化できるという共通点があります。グループ化された宣言のひとつが 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.Declの分類
  • 宣言(ast.DeclStmt

  • 空の文(ast.EmptyStmt

  • ラベル付き文(ast.LabeledStmt

  • 式だけの文(ast.ExprStmt

  • チャンネルへの送信(ast.SendStmt

  • インクリメント・デクリメント(ast.IncDecStmt

  • 代入または定義(ast.AssignStmt

  • goast.GoStmt

  • deferast.DeferStmt

  • returnast.ReturnStmt

  • breakcontinuegotofallthroughast.BranchStmt

  • ブロック(ast.BlockStmt

  • ifast.IfStmt

  • 式による switchast.SwitchStmt

  • 型による switchast.TypeSwitchStmt

  • switch 中のひとつの節(ast.CaseClause

  • selectast.SelectStmt

  • select 中のひとつの節(ast.CommClause

  • range を含まない forast.ForStmt

  • range を含む forast.RangeStmt

ast.SwitchStmt

TODO

  • Initswitch x := 1; t {

  • Tagswitch x := 1; t {

ast.TypeSwitchStmt

1.3.4. ast.Expr

ast.Expr インタフェースはおもにGoソースコードにおける および に対応する構文木のノードを表します。go/ast パッケージの実装では以下のように分類されています:

ast.Ellipsisast.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.” はこの値を参照しています。

リスト 6. structtypeincomplete.go
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)(0xc42001a540), (*ast.Field)(0xc42001a5c0)} incomplete=false
fields=[]*ast.Field{(*ast.Field)(0xc42001a540)} 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.FieldTag は構造体のフィールドである場合のみ存在しえます。

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 の出力
% 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.Nodetoken.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.EllipsisGenDecl.Lparen においてなど、token.NoPos はその位置情報を持つ要素がソースコード中に存在しないことを意味する場合もあります。

token.Pos は単なる整数値でしかないので、ファイル名や行番号などの詳細な情報をこれだけから得ることはできません。実はノードの持つこれらの位置情報は token.FileSet を基準にした相対的なものとしてエンコードされていて、完全な情報を復元するには FileSetPos を組み合わせる必要があります。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() メソッドによってわかりやすい位置情報が得られます。

リスト 7. positionstring.go
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 より)。

リスト 8. indeterminableident.go
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 に述べられていますが、抄訳します:

  • あらかじめ定義されている名前(nilint など)はユニバースブロック(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/astgo/types では、名前をつけられた言語上の要素(named language entity)をオブジェクト(object)と呼んでおり、構文上のオブジェクトはこの ast.Object によって表されています。

ast.ObjectDecl フィールドは、そのオブジェクトが宣言されたノードを表します。

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 を共有します。

リスト 9. astobject.go
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=0xc42001c3c0  kind=var decl=*ast.AssignStmt
example.go:6:13  obj=0x0           kind=bad decl=<nil>
example.go:6:20  obj=0xc42001c3c0  kind=var decl=*ast.AssignStmt
example.go:7:9   obj=0xc42001c3c0  kind=var decl=*ast.AssignStmt
example.go:7:16  obj=0xc42001c410  kind=var decl=*ast.Field
example.go:7:36  obj=0xc42001c410  kind=var decl=*ast.Field

import したパッケージ名としての x、定義された変数としての x、関数の仮引数名としての x がそれぞれ違った Obj をもち、文法的に同じものであれば Obj が同じものを指しています。

Kind フィールドは ObjKind 型に定義されている値のいずれかを取り、オブジェクトの種類を表します。

godoc: go/ast.Bad
const (
    Bad ObjKind = iota // for error handling
    Pkg                // package
    Con                // constant
    Typ                // type
    Var                // variable
    Fun                // function or method
    Lbl                // label
)

パッケージ名、定数名、型名、変数名、関数名またはメソッド名に加え、ラベル名もオブジェクトとして扱われることがわかります。

Object には、DataType など、さらに詳しい情報を保持するために用意されているフィールドも存在します。しかし、先に述べたように構文解析だけでは完全な情報が得られないので、これらの詳しい情報が必要な場合には型解析のAPIを使用することになるでしょう。

ちなみに、現在 Type フィールドはどこからも利用されていないようです。

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 はパッケージスコープを表し、各々のファイルのトップレベルに宣言された名前を格納します。Importsimport 宣言によって導入された名前を保持します。

ast.Packageast.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引数 modeparser.ParseComments 定数を指定することで、構文解析の結果にコメントを含めることができます。

func ParseFile(fset *token.FileSet, filename string, src interface{}, mode Mode) (f *ast.File, err error)
リスト 10. parsecomment.go
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
% 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.
...
リスト 11. src/go/parser/interface.go
// 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.Packagedoc.New 関数によって ast.Packageパッケージ)から生成されます。

godoc: go/doc.New
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
)

2.3.1. doc.Packageのレイアウト

WIP

doc.Package 構造体はドキュメントの表示に都合のよいように、整理された状態で

2.4. 例示のためのテスト

WIP
  • doc.Examples

3. ソースコードの文字列化

WIP
  • go/printer

  • go/format

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 でも構いません。

リスト 12. typechecksimple.go
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

以下はパッケージを読み込み、そのパッケージが公開している名前および依存しているパッケージの情報を印字する例です。

リスト 13. importer.go
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 { のような形。構文解析の時点では受けつけてしまいます

以下で、ソフトなエラーとそうでない重篤なエラーの例を確認できます。

リスト 14. typecheckerrors.go
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 ディレクトリのソースコードの型チェックを行っています。

リスト 15. typechecknameandpath.go
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におけるスコープ

PathName などの基本的な情報の他に有用なのは、解析されたパッケージのスコープ情報でしょう。

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 パッケージにはこれが定義されています。

以下の例では、ユニバーススコープに定義されている名前を列挙しています。

リスト 16. typesuniverse.go
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 な型を返します。

リスト 17. typesbuiltin.go
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: 定数畳み込み)。

リスト 18. typesconst.go
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)の出現を確認しています。

リスト 19. typesfunc.go
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 構造体を使ってファイルスコープを取得しています。

リスト 20. typespkgname.go
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.7.5. types.TypeName

TBD

4.7.6. types.Var

TBD

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

4.9. 型の情報

4.9.1. types.Type

Appendix B: types.Objectの階層

go/types.Object
  *go/types.Builtin
  *go/types.Const
  *go/types.Func
  *go/types.Label
  *go/types.Nil
  *go/types.PkgName
  *go/types.TypeName
  *go/types.Var

Appendix C: types.Typeの階層

go/types.Type
  *go/types.Array
  *go/types.Basic
  *go/types.Chan
  *go/types.Interface
  *go/types.Map
  *go/types.Named
  *go/types.Pointer
  *go/types.Signature
  *go/types.Slice
  *go/types.Struct
  *go/types.Tuple

5. ビルド情報

WIP
  • go/build

6. 高レベルのAPI

WIP
  • tools/go/loader

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 サブコマンドに与えられるコマンドライン引数は、ユーザの意図を表した以下のような形の文字列のリストになっています。

リスト 21. go doc -h
% 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パターンがあります:

  1. カレントディレクトリのソースコードを対象にする(例:go doc)。

  2. 完全なパスで指定されたパッケージを対象にする(例:go doc encoding/json)。

  3. 指定されたパスの一部からパッケージを探しだす(例:go doc json)。

これに加えて、パッケージ内のシンボルおよびそのメソッドも指定されることがあります。

カレントディレクトリを対象にする場合およびパッケージが指定されている場合(aおよびb)は、go/build のAPI build.Import を使って簡単にソースコードの所在を示す build.Package が得られます。

そうでない場合(c)、パスの一部が一致するパッケージを発見する必要があります。ここでも go/build のAPIを利用し、build.Default.GOROOTbuild.Default.GOPATH 以下のディレクトリを探索します。go doc コマンドが実行された時点でこの探索のためのgoroutineが起動していて、すばやく結果を返せるようになっています。

7.1.2. ソースコードの解析(parsePackage

ドキュメント情報を得るためには、ソースコードを解析する必要があります。パッケージの情報が手元にあるので parser.ParseDir でディレクトリ内のファイルを一度に解析できますが、その際第3引数の filter を指定して GoFilesCgoFiles に含まれないものを除去します。こうすることで、実行環境(GOOSGOARCH)に合わせたソースコードのみを解析対象としています。

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のメインの処理はソースコードの整形と印字です。これを担当するのが processFileinternal.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 _ = rangefor 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種類のモードがあり、

  • 引数 mnil の場合は、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)。

実際の処理は rewriteFilerewrite.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 と印字されるだけでどんな意味を持った値なのかの情報に欠けてしまっています。

リスト 22. sushi.go
package sushi

type Sushi uint

const (
    Maguro Sushi = iota
    Ikura
    Uni
    Tamago
)

fmt パッケージのメソッドは、値が fmt.Stringer インタフェースを満たしていればその String() メソッドを利用するので、ここで Sushi.String() が定数の名前を返すようにすればいいはずです。

type Stringer interface {
    String() string
}

見るからに単調な作業になので、プログラム的に生成することを考えますよね。そこで stringer の出番です。以上のような内容のソースコード sushi.go に対して stringer -type Sushi sushi.go を実行すると、次のソースコードが pill_string.go として生成されます。

リスト 23. sushi_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 は以下の流れでその仕事を行います。

  1. ディレクトリ名やファイル名の形で引数に指定されたソースコードを読み込む。

  2. コード中から指定された型を発見し、文字列化のために必要な情報を収集する。

  3. 文字列化のためのソースコードを生成する。

主だった処理は Generator という型のメソッドになっています。Generator は中に Package 型の構造体を保持していて、コードを解析して得られた情報

7.3.2. ソースのロード

ソースコードとして引数には1つのディレクトリか複数のファイルが指定できますが、どちらの場合も Generator.parsePackagestringer.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 宣言を挿入します。

具体的に行っていることは:

  1. 構文木を探索し、

    • x.y の形の参照を収集する

    • import 宣言によってファイルスコープに導入された名前を収集する

      • importPathToName

  2. その後、

    • 一度も参照されていない import 宣言を削除する

    • 未解決の参照を修正できるパッケージを探し出す

      • findImport

    • 上記のパッケージに対応する import 宣言を挿入する

という流れです。

構文木の探索

構文木の探索は ast.Walk を使って実装されています。goimports 中では、 ast.SelectorExprast.ImportSpec の2種類の構文要素が興味ある対象です。

ast.SelectorExprexpr.sel の形の式で、:セレクタ(selector)と呼ばれています。expr には任意の式が入り得ますが、ここではインポートされたパッケージの呼び出しを発見したいだけなので expr が識別子(ast.Ident)であるかのチェックを行っています。こうして発見されたパッケージ名へのセレクタのうち、オブジェクトが未解決のものを収集します。

ast.ImportSpecimport 宣言ひとつ分に対応します。例えば以下のような 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 コマンドが行うように、GOOSGOARCH 環境変数、ビルドタグに基づいてパッケージやソースコードを探しだすためのAPIを提供します。

ロードに失敗した場合はインポートパスの末尾部分が代替として使用されます。

import 宣言の挿入

続いて、上記の過程で収集された未解決の識別子からパッケージを探し出し、import 宣言を挿入します。このメイン部分、パッケージを探索するのが findImportGoPath です。パッケージ名と、そのパッケージによって提供されているべき名前から、パッケージのインポートパスを探し出します。

最初に標準パッケージのAPIとの一致がチェックされます。これはあらかじめテーブルが生成されているので高速にマッチします。

その後、ユーザによってインストールされたパッケージが探索されます。パッケージは最初に pkgIndexOnce.Do(loadPkgIndex) でインデックスします。go/build.Default.SrcDirs() 以下の、Goのソースコードを格納しているディレクトリに対して先ほどの importPathToName でパッケージ名の解決を行ってテーブルを作ります。

こうやって生成されたテーブルに対し、期待する識別子を公開しているパッケージを探し出します。build.ImportDir で得られたディレクトリ中のファイルを解析して(loadExportsGoPath)、エクスポートされてるものを発見して突き合わせます。

vendoring の対応

vendor ディレクトリまたは internal ディレクトリはその親ディレクトリからしかインポートできません。

  • TODO: canUse

7.5. guru

7.6. gddo

GoDoc.orgはサードパーティ製のものを含むGoライブラリのドキュメントを閲覧できるウェブサイトです。ここでは以下のようなURLでGitHubなどにホストされているGoライブラリにアクセスできます。

標準ライブラリのドキュメントも同じように閲覧できます。

ソースコードはhttps://github.com/golang/gddoにホストされています(“gddo” はGoDocDotOrgの頭文字を取ったものです)。

gddoはユーザから見るとドキュメントを表示するだけのサイトですが、裏側でソースコードのクロールを行うなど複雑な機能を持ち合わせています。ここでは指定されたドキュメントの表示機能のみに絞ってソースコードを読んでみます。

この機能を受け持つのが servePackage です。HTTPリクエストにしたがってパッケージのドキュメントを返すのが getDocmain.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.Getgosrc.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やBitBuckectなど有名どころで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)
}

getgetPresentationgetProject はそれぞれ DirectoryPresentationProject 型の値を返します。

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 はプレゼンテーション用の機能で、http://go-talks.appspot.com/[go-talks.appspot.org]でのみ利用されているものです。

ProjectDescription だけを持つ構造体で、Goのドキュメントレベルでパッケージの説明が得られなかった場合に、サービスで設定されている説明を利用するためのものです。

以下のサービスがあらかじめ実装されています。

  • BitBucket(bitbucket.go

  • Launchpad(launchpad.go

  • Google(google.go

  • GitHubおよびGist(github.go

例:GitHub
リスト 24. 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,
})

getGitHubDirgithub.go:51)でわりとストレートにファイルを一覧してる。

7.6.2. 仮想的なソースコードディレクトリからドキュメントを生成する

メソッド gddo/doc.newPackage が、仮想的なソースコードディレクトリである gosrc.Directory からドキュメントである gddo/doc.Package を生成します。gosrc.Directory はインポートパスや、ファイル名とそのデータを全て保持しています。

ディレクトリに含まれるファイルは、すべてが必要なファイルであるとは限りません。例えば spec_linux.gospec_windows.go は共存し得ないし、_test.go で終わるファイルはテスト用なのでドキュメントには不要です。また、ソースコード中のビルドタグもコンパイルやドキュメント生成にあたって留意しなくてはなりません。そこで go/build のAPIを利用します。

go/build のAPIは、特定の GOOSGOARCH 下で、あるパッケージを構成するソースコードを一覧するものでした。

通常はローカルのファイルシステムに対してファイルの探索を行うのですが、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)
}

8. フロー解析

WIP
  • tools/go/ssa