今日(2019/12/20)までに書き終わらなかったのですが、一応書いたところはあるしもったいなかったので一旦出します。
明日(2019/12/21)中に完成させるので気になった人はまた見に来てくれると嬉しいです。
これは Go3 Advent Calendar 2019 の 20 日目の記事です。
はじめに
みなさんはコーディングするときエディタや IDE のコード補完や Symbol の Rename 等のコード解析系の機能を使っていますか?
最近はほとんどのエディタ・IDE が Language Server Protocol(LSP) に対応しているので Go であれば(あくまで私個人の場合は) gopls を設定して(若干語弊はあると思いますが)エディタ・IDE にとらわれない快適な開発環境を満喫していると思います。
最近こうも快適な環境を使っていると gopls は一体裏でどんなことをやってくれているんだろうかと個人的に気になっていました。ちょうどアドベントカレンダーに参加したのでこれをきっかけに読んでみようと思った次第です。
この記事ではあまり意識して読むことのない gopls を(できる限り)読んでみてその仕組みの一端に触れてみたいと思います。
以上のようなふわっとした動機で書いたことによってかなり技術メモ寄りな内容になってしまったので、Go に関する何らかの知見を得たいと期待する人は読む必要は無いと思います。
書かないこと
- LSP の概要・仕様
- gopls が実装している Language Features の具体的な実装
読む前に知っておくと良いこと
gopls の README には More Information というセクションがあり、ここに gopls を読む前に知っておくと良いドキュメントへのリンクが貼られています。個人的に特に読んでおいた方が良い 2 つのドキュメントを簡単に紹介します。
1 つ目は gopls design documentation です。名前の通りこれは gopls にとっての Design Document であり、gopls は何をして何をしないのか・作られた背景・設計方針が網羅されていて単純に Design Document としての完成度が高いのでこれを読むだけでも勉強になります。また下部には現在実装している LSP の Feature 一覧があり、各 Feature が既存のどの Go ツールに該当するのかが記載されているので理解の助けになると思います。
2 つ目は gopls implementation documentation です。これは gopls の実装上の概念とリポジトリ内の各 package が何を担うものなのかが記されています。View/Session/Cache
という概念についてはコード中に頻出するため把握しておくと良いと思います。また、各 package の説明は具体的な目的を持ってコードを読むときの指針として役に立つと思います。
ざっくり main を追ってみる
ここからは実際の gopls のコードを好奇心の赴くままに読んでいきたいと思います。
以下の内容は 2019/12/19 時点の master である 979b82bfef62cbd5954bf1ec69c0fec5dbf2a1e0 に従います。
まず、gopls は CLI コマンドとして提供されているので main から読むのが定石だと思います。
main は golang/tools/gopls/main.go
にあるので見てみます。
// https://github.com/golang/tools/blob/979b82bfef62cbd5954bf1ec69c0fec5dbf2a1e0/gopls/main.go
package main
import (
"context"
"os"
"golang.org/x/tools/gopls/internal/hooks"
"golang.org/x/tools/internal/lsp/cmd"
"golang.org/x/tools/internal/tool"
)
func main() {
ctx := context.Background()
tool.Main(ctx, cmd.New("gopls", "", nil, hooks.Options), os.Args[1:])
}
中身はこれだけです。internal/tool.Main
が本体のようなのでこの関数が定義されている internal/tool/tool.go
を見てみます。
// https://github.com/golang/tools/blob/979b82bfef62cbd5954bf1ec69c0fec5dbf2a1e0/internal/tool/tool.go#L80-L98
func Main(ctx context.Context, app Application, args []string) {
s := flag.NewFlagSet(app.Name(), flag.ExitOnError)
s.Usage = func() {
fmt.Fprint(s.Output(), app.ShortHelp())
fmt.Fprintf(s.Output(), "\n\nUsage: %v [flags] %v\n", app.Name(), app.Usage())
app.DetailedHelp(s)
}
if err := Run(ctx, app, args); err != nil {
fmt.Fprintf(s.Output(), "%s: %v\n", app.Name(), err)
if _, printHelp := err.(commandLineError); printHelp {
s.Usage()
}
os.Exit(2)
}
}
Testable にするために error
を返すメイン処理を行う関数(ここでは Run(ctx, app, args)
) を main とは別に作って実行し失敗したら os.Exit()
で落とすという Go の CLI ツールの実装でよく見るパターンですね。
main 処理を行っていると思われる Run(ctx, app, args)
を見てみます。
// https://github.com/golang/tools/blob/979b82bfef62cbd5954bf1ec69c0fec5dbf2a1e0/internal/tool/tool.go#L100-L153
func Run(ctx context.Context, app Application, args []string) error {
s := flag.NewFlagSet(app.Name(), flag.ExitOnError)
s.Usage = func() {
fmt.Fprint(s.Output(), app.ShortHelp())
fmt.Fprintf(s.Output(), "\n\nUsage: %v [flags] %v\n", app.Name(), app.Usage())
app.DetailedHelp(s)
}
p := addFlags(s, reflect.StructField{}, reflect.ValueOf(app))
s.Parse(args)
...中略
return app.Run(ctx, s.Args()...)
}
ここでは gopls のコマンドオプションのパースとオプションで指定されたプロファイラ、最後に app.Run(ctx, s.Args()...)
が実行されています。おそらくこの app.Run(ctx, s.Args()...)
が gopls のメイン処理だと推察します。
レシーバとなっている app
は同じファイル内で定義されている Application
という名前の interface です。
// https://github.com/golang/tools/blob/979b82bfef62cbd5954bf1ec69c0fec5dbf2a1e0/internal/tool/tool.go#L47-L65
type Application interface {
// Name returns the application's name. It is used in help and error messages.
Name() string
// Most of the help usage is automatically generated, this string should only
// describe the contents of non flag arguments.
Usage() string
// ShortHelp returns the one line overview of the command.
ShortHelp() string
// DetailedHelp should print a detailed help message. It will only ever be shown
// when the ShortHelp is also printed, so there is no need to duplicate
// anything from there.
// It is passed the flag set so it can print the default values of the flags.
// It should use the flag sets configured Output to write the help to.
DetailedHelp(*flag.FlagSet)
// Run is invoked after all flag processing, and inside the profiling and
// error handling harness.
Run(ctx context.Context, args ...string) error
}
この Application
interface を満たす型の実体は遡ると golang/tools/gopls/main.go
において internal/lsp/cmd.New("gopls", "", nil, hooks.Options)
として渡されていました。
次は main 関数に渡された internal/lsp/cmd
から追ってみようと思います。
internal/lsp/cmd
の中身を追う
internal/lsp/cmd.New
を見てみます。
// https://github.com/golang/tools/blob/979b82bfef62cbd5954bf1ec69c0fec5dbf2a1e0/internal/lsp/cmd/cmd.go#L74-L87
func New(name, wd string, env []string, options func(*source.Options)) *Application {
if wd == "" {
wd, _ = os.Getwd()
}
app := &Application{
options: options,
name: name,
wd: wd,
env: env,
OCAgent: "off", //TODO: Remove this line to default the exporter to on
}
return app
}
Application
という型のポインタを返しています。同じファイル内に Application
は存在しているので見てみます。
// https://github.com/golang/tools/blob/979b82bfef62cbd5954bf1ec69c0fec5dbf2a1e0/internal/lsp/cmd/cmd.go#L35-L72
// Application is the main application as passed to tool.Main
// It handles the main command line parsing and dispatch to the sub commands.
type Application struct {
// Core application flags
// Embed the basic profiling flags supported by the tool package
tool.Profile
// We include the server configuration directly for now, so the flags work
// even without the verb.
// TODO: Remove this when we stop allowing the serve verb by default.
Serve Serve
// the options configuring function to invoke when building a server
options func(*source.Options)
// The name of the binary, used in help and telemetry.
name string
// The working directory to run commands in.
wd string
// The environment variables to use.
env []string
// Support for remote lsp server
Remote string `flag:"remote" help:"*EXPERIMENTAL* - forward all commands to a remote lsp"`
// Enable verbose logging
Verbose bool `flag:"v" help:"verbose output"`
// Control ocagent export of telemetry
OCAgent string `flag:"ocagent" help:"the address of the ocagent, or off"`
// PrepareOptions is called to update the options when a new view is built.
// It is primarily to allow the behavior of gopls to be modified by hooks.
PrepareOptions func(*source.Options)
}
コメントを読むとどうやらこれが gopls コマンドを管理するためのメインの構造体みたいです。
ちょっと面白いのは分散トレーシング用のフレームワークである OpenCensus 用のメトリクスを export するための Agent である ocagent
を制御するためのフラグが含まれていることです。もしかしたら OpenCensus(今後は OpenTelemetry) と組み合わせて面白いことができるのかもしれませんが、今回は趣旨が異なるのでまた別の機会にしたいと思います。
次に internal/tool.Run
の最後に実行されていた Run()
メソッドを見てみます。
// https://github.com/golang/tools/blob/979b82bfef62cbd5954bf1ec69c0fec5dbf2a1e0/internal/lsp/cmd/cmd.go#L128-L148
// Run takes the args after top level flag processing, and invokes the correct
// sub command as specified by the first argument.
// If no arguments are passed it will invoke the server sub command, as a
// temporary measure for compatibility.
func (app *Application) Run(ctx context.Context, args ...string) error {
...中略
app.Serve.app = app
if len(args) == 0 {
return tool.Run(ctx, &app.Serve, args)
}
command, args := args[0], args[1:]
for _, c := range app.commands() {
if c.Name() == command {
return tool.Run(ctx, c, args)
}
}
return tool.CommandLineErrorf("Unknown command %v", command)
}
まず気になるのは app.Serve.app = app
です。app.Serve
は Application
構造体の Serve
型のフィールドです。
TBD
まとめ
TBD