首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >golang源码分析 :gopls(1)

golang源码分析 :gopls(1)

作者头像
golangLeetcode
发布2026-03-18 18:30:03
发布2026-03-18 18:30:03
1070
举报

为了解决m个编辑器n种编程语言造成的编译器代码需要mxn套的问题,将笛卡尔积改为和即m+n套,微软推出了pls(P‌rogramming ‌L‌anguage ‌S‌erver)编程语言服务器,实现了语言服务器协议(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方法提供服务

代码语言:javascript
复制
versionpkg.VersionOverride = version
telemetry.Start(telemetry.Config{
 _, err := filecache.Get("nonesuch", [32]byte{});
tool.Main(ctx, cmd.New(), os.Args[1:])

设置版本是使用了runtime/debug包的

代码语言:javascript
复制
debug.ReadBuildInfo()

方法获取返回值的

代码语言:javascript
复制
info.Main.Version 

文件缓存的定义位于internal/filecache/filecache.go,先通过文件缓存获取索引文件名字,然后加载索引数据,最后通过索引加载数据并存储到lru缓存里,期间需要校验hash256摘要。

代码语言:javascript
复制
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

代码语言:javascript
复制
func (c *Cache[K, V]) Set(key K, value V, size int) {
    c.impl.set(key, value, size)
}

cache的定义如下,具体的lru源码这里就不详细分析了

代码语言:javascript
复制
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方法

代码语言:javascript
复制
func Main(ctx context.Context, app Application, args []string) {
    Run(ctx, s, app, args)
代码语言:javascript
复制
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

代码语言:javascript
复制
func New() *Application {
   app := &Application{	
代码语言:javascript
复制
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方法

代码语言:javascript
复制
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方法定义如下

代码语言:javascript
复制
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

代码语言:javascript
复制
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},
    }
}
代码语言:javascript
复制
func (app *Application) internalCommands() []tool.Application {
    return []tool.Application{
        &vulncheck{app: app},
    }
}
代码语言:javascript
复制
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,也就是说我们的扩展只要实现了这个接口就行:

代码语言:javascript
复制
// 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方法的时候传入的参数已经变了,不是自己而是加载出来的具体扩展实现的实例,这样就完成了所有扩展的加载。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-07-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 golang算法架构leetcode技术php 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档