ajalab.github.io

go/packagesについて

2018-11-03 | Go

概要

Go 1.11のリリースから、パッケージ解析等の目的でGoのパッケージをロードするためのライブラリx/tools/go/packagesがGo Toolsに追加された(リリースノート)。このライブラリはGo 1.11から導入されたmodulesシステムをサポートしている。まだ標準ライブラリには入っていないが、これはmodules非対応のgo/buildを代替するものである。

ところで、Go Toolsにはパッケージ読み込みのためのライブラリ(x/tools/go/loader)が既に存在する。公式のアナウンス等は無くこのパッケージの立ち位置は不明瞭だが、x/tools/go/loaderに関するissueコメントや、loader.Programを使用するssautil.CreateProgramdeprecatedになったことから、今後はx/tools/go/loaderではなくx/tools/go/packagesを使うべきだろう。

使い方

パッケージのロードには、まずpackages.Config構造体に諸設定を記述する。もっとも重要なのはModeメンバで、ローダーによる解析のレベルをLoadMode型の値を代入して指定する。以下に挙げるが、下に行くほどレベルが深く、ローダーによって返却されるpackages.Package構造体に含まれる情報も増える。

  • LoadFiles : パッケージに所属する*.goファイルの一覧を得る
  • LoadImports: インポートの依存解析を行う
  • LoadTypes: 宣言レベルでの型解析を行う
  • LoadSyntax: 指定したパッケージについてASTの構築・型解析を行う
  • LoadAllSyntax: 指定したパッケージ及びそれらの依存パッケージにおいてASTの構築・型解析を行う

設定をpackages.Loadに与えて呼び出す。第2引数以降で読み込むパッケージを指定するが、これにはGoのビルドシステムの引数で用いられる[packages]パターンを基本的に用いる。[packages]パターンの詳細はgo help packagesに記述がある。実際、ライブラリ内部ではgo listコマンドを実行している。

[packages]パターン以外にも、"patternName=string"の書式で詳細なクエリを指定することができる。現在公式に実装されているクエリはfilepatternで、前者は"file=path/to/file.go"の形式でpath/to/file.goを含むパッケージを指定する。後者は"pattern=string"の形式で、string[packages]パターンとしてgo listに渡される。これはエスケープの用途で用いる。第3のクエリとして"name=identifier"クエリがあり、パッケージ名での指定ができる(例:"name=rand"math/randcrypto/randを読み込む)。これを書いた時点では、nameクエリは実装途中であるとドキュメントに記載されていたが、既に動くものが実装されている

packages.Loadは読み込んだパッケージのリストを*packages.Packageのスライスとして返却する。パッケージの順番及び数はpackages.Loadの第2引数以降で指定したパターンと必ずしも一致しないので注意する。

サンプル

以下のプログラムはtestdata/t.gox/tools/go/packagesによってruntimeパッケージとともに読み込み、SSA形式に変換してからx/tools/go/interpのAPIによってインタプリタ実行する。

main.go

package main

import (
	"go/types"
	"log"

	"golang.org/x/tools/go/packages"
	"golang.org/x/tools/go/ssa"
	"golang.org/x/tools/go/ssa/interp"
	"golang.org/x/tools/go/ssa/ssautil"
)

func main() {
	target := "testdata/t.go"

	// interp.Interpretで実行する対象は、依存パッケージを含めすべて
	// SSA形式に変換されている必要があるため、LoadAllSyntaxモードを指定する
	cfg := &packages.Config{
		Mode:  packages.LoadAllSyntax,
		Tests: false,
	}

	// runtimeモジュールはinterp.Interpretによる実行の際に必要。
	// 大抵依存パッケージの推移的閉包に含まれているため、明示的に指定しなくても動くことがある
	pkgs, err := packages.Load(cfg, "name=runtime", target)
	if err != nil {
		log.Fatalf("failed to load package: %v", err)
		return
	}

	// SSA形式に変換
	ssaProg, ssaPkgs := ssautil.AllPackages(pkgs, 0)
	ssaProg.Build()

	// mainパッケージを探す
	var mainPkg *ssa.Package
	for _, ssaPkg := range ssaPkgs {
		if ssaPkg.Pkg.Name() == "main" {
			mainPkg = ssaPkg
		}
	}

	// 実行
	interp.Interpret(mainPkg, 0, &types.StdSizes{WordSize: 8, MaxAlign: 8}, "hoge", []string{})
}

testdata/t.go

package main

import "fmt"

func main() {
	fmt.Println("hello golang")
}

実行結果

$ go run main.go
hello golang

参考