go/typesでgo moduleを有効にするまでの旅路
はじめに
この記事はGo2 Advent Calendar 2021 23日目の記事です。
TL; DR;
go/types で go moduleを有効にするためには golang.org/x/tools/go/packages を使う。以上。
旅のはじまり
日頃仕事でDX(Developer Experienceの方)おじさんをやっている関係上、Code Generate ツールを書くことが結構ありまして、その際に go/types が便利なのでよく使っています。
AST は生で触ると結構大変ですが、このgo/typesパッケージを利用することで、あたかも reflection のような感覚で扱うことが出来ます。 例えば embedded は AST の鬼門ですが、go/types を使うと非常に楽ちんです。go/typesサイコー!
例えばこんなファイルがあったとして
sample/foo.go
package sample type Foo struct { Bar } type Bar struct { }
こんな感じで Foo
の型情報にアクセスすることが出来ます。
func main() { set := token.NewFileSet() path := "path/to/sample/foo.go" pkgs, _ := parser.ParseDir(set, path, nil, parser.ParseComments) pkg, _ := pkgs["sample"] cfg := types.Config{ Importer: importer.Default(), } afs := []*ast.File{} for _, f := range pkg.Files { afs = append(afs, f) } pkz, _ := cfg.Check("", set, afs, nil) obj := pkz.Scope().Lookup("Foo") tp := obj.Type().Underlying() fmt.Println(tp) // *types.Struct strct := tp.(*types.Struct) f := strct.Field(0) fmt.Println(f.Name(), f.Embedded()) // Bar true }
そんなこんなで、go/types を使って Happy Hacking な毎日を送っていましたが、 とある問題に直面しました。
GOPATH モード非対応パッケージで躓く
日頃使っていて、どうやら go/types が 依存パッケージの参照を GOPATH
を参照、
いわゆるGOPATHモードで動作していることには気づいていました。
2021年にもなってどうなのか、とは思っていましたが、基本利用シーンはツールですので、 深追いはせずに、適宜依存パッケージが見つからない旨のエラーが出たら
$ GO111MODULE=off go get -u xxxx
をして事なきを得ていました。しかしある日、事件は起きました。
$ GO111MODULE=off go get -u github.com/labstack/echo/v4 cannot find package "github.com/labstack/echo/v4" in any of: $GOROOT/src/github.com/labstack/echo/v4 (from $GOROOT) $GOPATH/src/github.com/labstack/echo/v4 (from $GOPATH)
ついに来てしまいました。 モジュールモードがデフォルトでONになって久しく、 昨今こういったGOPATHモード非対応のライブラリも増えてきています。
という訳で、仕方がないので重い腰を上げて go/types のコードを読むことにしました。
go/types Deep Dive
先程掲載したサンプルコードのこの部分。
cfg := types.Config{ Importer: importer.Default(), }
怪しいですね。ということで定義を見てみました。
// A Config specifies the configuration for type checking. // The zero value for Config is a ready-to-use default configuration. type Config struct { ~ // 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 ~ } ~ // An Importer resolves import paths to Packages. // // CAUTION: This interface does not support the import of locally // vendored packages. See https://golang.org/s/go15vendor. // If possible, external implementations should implement ImporterFrom. type Importer interface { // Import returns the imported package for the given import path. // The semantics is like for ImporterFrom.ImportFrom except that // dir and mode are ignored (since they are not present). Import(path string) (*Package, error) } ~ // An ImporterFrom resolves import paths to packages; it // supports vendoring per https://golang.org/s/go15vendor. // Use go/importer to obtain an ImporterFrom implementation. type ImporterFrom interface { // Importer is present for backward-compatibility. Calling // Import(path) is the same as calling ImportFrom(path, "", 0); // i.e., locally vendored packages may not be found. // The types package does not call Import if an ImporterFrom // is present. Importer // ImportFrom returns the imported package for the given import // path when imported by a package file located in dir. // If the import failed, besides returning an error, ImportFrom // is encouraged to cache and return a package anyway, if one // was created. This will reduce package inconsistencies and // follow-on type checker errors due to the missing package. // The mode value must be 0; it is reserved for future use. // Two calls to ImportFrom with the same path and dir must // return the same package. ImportFrom(path, dir string, mode ImportMode) (*Package, error) }
types.Config
に設定された Importer
インターフェースを使って依存パッケージを探しに行くことが読み取れました。
そして、更に ImporterFrom
が実装されていればそっちを使うと。なるほど。
で、importer.Default()
を見てみると、
// Default returns an Importer for the compiler that built the running binary. // If available, the result implements types.ImporterFrom. func Default() types.Importer { return For(runtime.Compiler, nil) }
コメントが何やら含みをもたせていますが ImporterFrom を実装してる可能性がありそうです。
runtime.Compiler
は長くなるので引用しませんがGoデフォルトのコンパイラを示す gc
を返します。
func ForCompiler(fset *token.FileSet, compiler string, lookup Lookup) types.Importer { switch compiler { case "gc": return &gcimports{ fset: fset, packages: make(map[string]*types.Package), lookup: lookup, } ~ }
どうやら、Importer を実装しているのはこの gcimports
のようでした
type gcimports struct { fset *token.FileSet packages map[string]*types.Package lookup Lookup } func (m *gcimports) Import(path string) (*types.Package, error) { return m.ImportFrom(path, "" /* no vendoring */, 0) } func (m *gcimports) ImportFrom(path, srcDir string, mode types.ImportMode) (*types.Package, error) { if mode != 0 { panic("mode must be 0") } return gcimporter.Import(m.fset, m.packages, path, srcDir, m.lookup) }
gcimporter は ImporterFrom を実装していました。 ということは、どうにかして srcDir を指定することが出来れば良さそうです。
// If the installed importer implements ImporterFrom, the type
// checker calls ImportFrom instead of Import.
こちらのコメントの実装部分を探します。
func (check *Checker) importPackage(at positioner, path, dir string) *Package { ~ // no package yet => import it if path == "C" && (check.conf.FakeImportC || check.conf.go115UsesCgo) { imp = NewPackage("C", "C") imp.fake = true // package scope is not populated imp.cgo = check.conf.go115UsesCgo } else { // ordinary import var err error if importer := check.conf.Importer; importer == nil { err = fmt.Errorf("Config.Importer not installed") } else if importerFrom, ok := importer.(ImporterFrom); ok { imp, err = importerFrom.ImportFrom(path, dir, 0) if imp == nil && err == nil { err = fmt.Errorf("Config.Importer.ImportFrom(%s, %s, 0) returned nil but no error", path, dir) } ~ }
ありました。このpathがどこから来ているかというと・・ この辺は長くなるので割愛します。結論、
pkz, _ := cfg.Check("", set, afs, nil)
冒頭のサンプルコードのこの部分。定義を見てみます
// Check type-checks a package and returns the resulting package object and // the first error if any. Additionally, if info != nil, Check populates each // of the non-nil maps in the Info struct. // // The package is marked as complete if no errors occurred, otherwise it is // incomplete. See Config.Error for controlling behavior in the presence of // errors. // // The package is specified by a list of *ast.Files and corresponding // file set, and the package path the package is identified with. // The clean path must not be empty or dot ("."). func (conf *Config) Check(path string, fset *token.FileSet, files []*ast.File, info *Info) (*Package, error) { pkg := NewPackage(path, "") return pkg, NewChecker(conf, fset, pkg, info).Files(files) }
第一引数で空文字を渡していた path
、こちらに任意のパスを指定することで
そちらを見てくれるようです。尚、その指定がない場合gcimportsの実装的には $GOROOT
や $GOPATH
を参照、つまり GOPATHモードと同じ動きになるようでした。
一旦動作。しかし・・
pathの指定の仕方がわかった所で、次のような手順でやってみました
1 ツール上で必要なパッケージをimport
後述しますが、ツール自体がパッケージを必要としているのではなく、 Importerに知らせる場所(vendor/) に配置するためimportします。
import ( ~ _ "github.com/labstack/echo/v4" // ツールでは使用しないため blank identifier を指定 )
2 ツール上で go mod vendor
を実行
$ go mod vendor go: finding module for package github.com/labstack/echo/v4 go: found github.com/labstack/echo/v4 in github.com/labstack/echo/v4 v4.6.1
これで vendor/ 以下に依存パッケージがダウンロードされます
$ tree vendor/ -L 2 vendor/ ├── github.com │ ├── labstack │ ├── mattn │ └── valyala ├── golang.org │ └── x └── modules.txt
3 path にvendorのパスを知らる
pkz, _ := dcg.Check("/path/to/tool/vendor", set, afs, nil)
こうして、めでたく動作しました。やった!! ・・とはなりませんでした。
解析するプログラムが依存しているだけで、本来ツールに不要な依存を生み出してしまいます。 それはあまりにもイケてません、こういったパッケージが増えるたびにvendor以下に追加していく? そもそも2021年にもなって (略)
というわけで、この方法はボツにしました。
Google先生に聞いて一瞬で解決
仕方がないのでGoogle先生の力を借りることにしました。 はい。すぐ見つかりました(半ギレ)
https://github.com/golang/go/issues/28328 https://github.com/golang/go/issues/27556#issuecomment-419468978 (上記issueから引用)
旅のおわり
というわけで、回り道しましたが golang.org/x/tools/go/packages を使うことで、 めでたく go module を参照することができるようになりました。こんな感じです。
import ( ~ "golang.org/x/tools/go/packages" "golang.org/x/tools/imports" ) func main() { set := token.NewFileSet() path := "path/to/sample/foo.go" pkgs, _ := parser.ParseDir(set, path, nil, parser.ParseComments) pkg, _ := pkgs["sample"] cfg := &packages.Config{ Mode: packages.LoadAllSyntax, } ptns := []string{} for k := range pkg.Files { ptns = append(ptns, k) } pkzs, _ := packages.Load(cfg, ptns...) var pkz *types.Package for _, p := range pkzs { if p.Types.Name() == name { pkz = p.Types break } } obj := pkg.Scope().Lookup("Foo") tp := obj.Type().Underlying() fmt.Println(tp) // *types.Struct strct := tp.(*types.Struct) f := strct.Field(0) fmt.Println(f.Name(), f.Embedded()) // Bar true }
最初からGoogle先生に聞けば5分で終わった話ではありますが、 久しぶりに標準パッケージをじっくり読んで、遠回りした分、学びもそれなりにあったの良しとします。
今回は以上です。それでは、良いお年を。