
为了解决m个编辑器n种编程语言造成的编译器代码需要mxn套的问题,将笛卡尔积改为和即m+n套,微软推出了pls(Programming Language Server)编程语言服务器,实现了语言服务器协议(LSP)的。
gopls 与vscode 通信的协议是基于 JSON-RPC 2.0 的 语言服务器协议(LSP),支持标准输入/输出或 TCP 套接字传输数据两种方式。调试会话中通过 Debug Adapter Protocol (DAP) 交互,独立于 LSP 但共用 JSON-RPC 通道。完整代码位于golang.org/x/tools/gopls。下面我们开始分析它的源码:
首先看下main.go文件,首先设置版本,然后配置telemetry进行信息上报监控,然后是使用文件缓存做重启后断点恢复,最后调用了tool的Main方法提供服务
versionpkg.VersionOverride = version
telemetry.Start(telemetry.Config{
_, err := filecache.Get("nonesuch", [32]byte{});
tool.Main(ctx, cmd.New(), os.Args[1:])设置版本是使用了runtime/debug包的
debug.ReadBuildInfo()方法获取返回值的
info.Main.Version 文件缓存的定义位于internal/filecache/filecache.go,先通过文件缓存获取索引文件名字,然后加载索引数据,最后通过索引加载数据并存储到lru缓存里,期间需要校验hash256摘要。
func Get(kind string, key [32]byte) ([]byte, error) {
value, ok := memCache.Get(memKey{kind, key})
// Read the index file, which provides the name of the CAS file.
indexData, err := os.ReadFile(indexName)
//Read the CAS file and check its contents match.
//Update file times used by LRU eviction.
casName, err := filename(casKind, valueHash)
memCache.Set(memKey{kind, key}, value, len(value))lru缓存的实现位于internal/util/lru/lru.go
func (c *Cache[K, V]) Set(key K, value V, size int) {
c.impl.set(key, value, size)
}cache的定义如下,具体的lru源码这里就不详细分析了
type cache struct {
capacity int
mu sync.Mutex
used int // used capacity, in user-specified units
m map[any]*entry // k/v lookup
lru queue // min-atime priority queue of *entry
clock int64 // clock time, incremented whenever the cache is updated
}处理完这些准备工作,我们就来到了Main函数的入口internal/tool/tool.go,内部调用了Run方法,Run方法里添加了很多命令行参数解析和监控上报的逻辑后,最后调用了传入的app的Run方法
func Main(ctx context.Context, app Application, args []string) {
Run(ctx, s, app, args)func Run(ctx context.Context, s *flag.FlagSet, app Application, args []string) (resultErr error) {
return app.Run(ctx, s.Args()...)app的定义位于internal/cmd/cmd.go
func New() *Application {
app := &Application{ 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(*settings.Options)
// Support for remote LSP server.
Remote string `flag:"remote" help:"forward all commands to a remote lsp specified by this flag. With no special prefix, this is assumed to be a TCP address. If prefixed by 'unix;', the subsequent address is assumed to be a unix domain socket. If 'auto', or prefixed by 'auto;', the remote address is automatically resolved based on the executing environment."`
// Verbose enables verbose logging.
Verbose bool `flag:"v,verbose" help:"verbose output"`
// VeryVerbose enables a higher level of verbosity in logging output.
VeryVerbose bool `flag:"vv,veryverbose" help:"very verbose output"`
// Control ocagent export of telemetry
OCAgent string `flag:"ocagent" help:"the address of the ocagent (e.g. http://localhost:55678), 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(*settings.Options)
// editFlags holds flags that control how file edit operations
// are applied, in particular when the server makes an ApplyEdits
// downcall to the client. Present only for commands that apply edits.
editFlags *EditFlags
}我们重点看下它的入口函数Run方法,针对有参数和没有参数场景最终都是调用了tool的Run方法
func (app *Application) Run(ctx context.Context, args ...string) error {
if len(args) == 0 {
s := flag.NewFlagSet(app.Name(), flag.ExitOnError)
return tool.Run(ctx, s, &app.Serve, args)
}
for _, c := range app.Commands() {
if c.Name() == command {
s := flag.NewFlagSet(app.Name(), flag.ExitOnError)
return tool.Run(ctx, s, c, args)其中Commands方法定义如下
func (app *Application) Commands() []tool.Application {
var commands []tool.Application
commands = append(commands, app.mainCommands()...)
commands = append(commands, app.featureCommands()...)
commands = append(commands, app.internalCommands()...)
return commands
}对应的三类Command
func (app *Application) mainCommands() []tool.Application {
return []tool.Application{
&app.Serve,
&version{app: app},
&bug{app: app},
&help{app: app},
&apiJSON{app: app},
&licenses{app: app},
}
}func (app *Application) internalCommands() []tool.Application {
return []tool.Application{
&vulncheck{app: app},
}
}func (app *Application) featureCommands() []tool.Application {
return []tool.Application{
&callHierarchy{app: app},
&check{app: app},
&codeaction{app: app},
&codelens{app: app},
&definition{app: app},
&execute{app: app},
&fix{app: app}, // (non-functional)
&foldingRanges{app: app},
&format{app: app},
&highlight{app: app},
&implementation{app: app},
&imports{app: app},
newRemote(app, ""),
newRemote(app, "inspect"),
&links{app: app},
&prepareRename{app: app},
&references{app: app},
&rename{app: app},
&semanticToken{app: app},
&signature{app: app},
&stats{app: app},
&symbols{app: app},
&workspaceSymbol{app: app},
}
}对应的featureCommands里面就是我们pls针对lsp的具体实现的列表,如果我们要扩展pls的功能,直接在列表中添加实现即可。其返回值是一个接口定义位于internal/tool/tool.go,也就是说我们的扩展只要实现了这个接口就行:
// Application is the interface that must be satisfied by an object passed to Main.
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
}除此看这段代码的时候我发现在调用tool.Main的时候传入的是cmd.New,也就是说在tool.Run方法内部会调用cmd.Run方法,可是在cmd.Run方法里又调用了tool.Run方法,这不死循环了么?
仔细研究后发现,两者是不一样的,tool.Run方法调用cmd.Run方法是执行cmd.Run把具体协议实现的列表加载出来。而cmd.Run调用tool.Run方法的时候传入的参数已经变了,不是自己而是加载出来的具体扩展实现的实例,这样就完成了所有扩展的加载。
本文分享自 golang算法架构leetcode技术php 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!