Package protocol implements Language Server Protocol specification in Go
See the codeA fast, spec-faithful implementation of the Language Server Protocol 3.18 for Go — types generated from the official LSP meta-model, a union-aware JSON codec that skips reflection on the hot path, and a ready-to-wire client/server RPC layer.
import "go.lsp.dev/protocol"
metaModel.json. Every request, notification, and
structure is generated from the official LSP meta-model, so the
type surface tracks the specification rather than a hand-maintained subset.
The generator lives in internal/genlsp."or" types are modeled as sealed Go
interfaces — each arm is a distinct concrete type implementing a private
marker method. Callers discriminate arms with an ordinary type switch, and the
compiler keeps the arm set closed.Marshal
and Unmarshal dispatch
through generated byte-level encoders and decoders; struct values, named slice
wrappers, and union values all (de)serialize without entering
reflect. AppendMarshal
lets callers amortize the output allocation across messages.NewServer
and NewClient wire the
union-aware codec onto a go.lsp.dev/jsonrpc2 connection and
return typed Server /
Client dispatchers.go get go.lsp.dev/protocol@latest
Requires Go 1.26+.
Implement the methods you care about by embedding UnimplementedServer, then let
NewServer serve it over any jsonrpc2.Stream (here, an in-memory pipe):
package main
import (
"context"
"go.lsp.dev/jsonrpc2"
"go.lsp.dev/protocol"
)
// langServer overrides only the methods it implements; UnimplementedServer
// supplies a "method not found" default for the rest of the LSP surface.
type langServer struct {
protocol.UnimplementedServer
}
func (langServer) Hover(ctx context.Context, params *protocol.HoverParams) (*protocol.Hover, error) {
return &protocol.Hover{
Contents: protocol.String("hello from the server"),
}, nil
}
func serve(ctx context.Context, stream jsonrpc2.Stream) (jsonrpc2.Conn, protocol.Client) {
// NewServer serves langServer and returns the connection plus a typed
// Client dispatcher for server -> client requests.
ctx, conn, client := protocol.NewServer(ctx, langServer{}, stream)
_ = ctx
return conn, client
}
The mirror-image helper NewClient
serves a Client and hands back a typed Server dispatcher, so the same model
drives both ends of the connection.
Where the LSP says a value is A | B, this package exposes a sealed interface.
Construct a value with the concrete arm and read it back with a type switch:
// Hover.Contents (HoverContents) is one of:
// String | *MarkupContent | *MarkedStringWithLanguage | MarkedStringSlice
hover := &protocol.Hover{Contents: protocol.String("plain text")}
switch c := hover.Contents.(type) {
case protocol.String:
// a bare string arm
case *protocol.MarkupContent:
// structured markup
default:
_ = c
}
Because the arm set is closed by an unexported marker method, no value outside the package can satisfy the interface — the type switch is exhaustive by construction.
Generated byte-walkers make one GC-managed copy of the input before decoding it. To avoid per-field allocations on hot paths, unescaped decoded strings and raw JSON value fields may alias that single owned copy. Two consequences:
Unmarshal (or the LSP codec) returns, you may freely reuse or mutate
the original []byte — the decoded value no longer references it.Clone.Generated URI fields use go.lsp.dev/uri.URI directly. Construct new
values with uri.Parse, uri.File, or uri.From.
protocol.URI remains as a package-local named type for compatibility and for
the sealed union arms (such as RelativePatternBaseURI) that require a local
marker-method receiver. Prefer go.lsp.dev/uri.URI in ordinary code; convert
explicitly with protocol.URI(u) only when assigning a URI string to such an
arm.
The codec is the project's hot path, and it is benchmarked and gated in CI. A
relative-regression gate (.github/workflows/bench.yaml) benchmarks the base
ref and the PR head on the same runner and fails on a >5% sec/op geomean
regression. The figures below are from the Phase-3 authoritative re-baseline
(Intel Xeon 8481C, linux/amd64), comparing the optimized codec against the
reflection-based baseline:
| Benchmark | sec/op change |
|---|---|
Encode/initialize_request | −77.93% |
Encode/publish_diagnostics | −15.98% |
Decode/semantic_tokens | −27.92% |
Decode/completion_array | −23.80% |
Decode/completion_list | −13.24% |
Run them yourself:
go test -run='^$' -bench='^(BenchmarkDecode|BenchmarkEncode)$' -benchmem -count=6 .
make test # run the test suite with race detection
make coverage # run tests and emit coverage
make lint # goimports-rereviser + gofumpt + golangci-lint
make generate # regenerate the package from metaModel.json, then format
make help # list all targets
Generated files carry a .gen.go suffix; edit the generator under
internal/genlsp and run make generate rather than
hand-editing them.
| Module | Role |
|---|---|
go.lsp.dev/jsonrpc2 | JSON-RPC 2.0 transport for the RPC layer |
go.lsp.dev/uri | RFC 3986 / file:// URI handling |
go-json-experiment/json | JSON engine (the github.com/go-json-experiment/json prototype) |
BSD 3-Clause. See LICENSE.
Go
97.9%
Makefile
2.1%
Package protocol implements Language Server Protocol specification in Go
See the codeA fast, spec-faithful implementation of the Language Server Protocol 3.18 for Go — types generated from the official LSP meta-model, a union-aware JSON codec that skips reflection on the hot path, and a ready-to-wire client/server RPC layer.
import "go.lsp.dev/protocol"
metaModel.json. Every request, notification, and
structure is generated from the official LSP meta-model, so the
type surface tracks the specification rather than a hand-maintained subset.
The generator lives in internal/genlsp."or" types are modeled as sealed Go
interfaces — each arm is a distinct concrete type implementing a private
marker method. Callers discriminate arms with an ordinary type switch, and the
compiler keeps the arm set closed.Marshal
and Unmarshal dispatch
through generated byte-level encoders and decoders; struct values, named slice
wrappers, and union values all (de)serialize without entering
reflect. AppendMarshal
lets callers amortize the output allocation across messages.NewServer
and NewClient wire the
union-aware codec onto a go.lsp.dev/jsonrpc2 connection and
return typed Server /
Client dispatchers.go get go.lsp.dev/protocol@latest
Requires Go 1.26+.
Implement the methods you care about by embedding UnimplementedServer, then let
NewServer serve it over any jsonrpc2.Stream (here, an in-memory pipe):
package main
import (
"context"
"go.lsp.dev/jsonrpc2"
"go.lsp.dev/protocol"
)
// langServer overrides only the methods it implements; UnimplementedServer
// supplies a "method not found" default for the rest of the LSP surface.
type langServer struct {
protocol.UnimplementedServer
}
func (langServer) Hover(ctx context.Context, params *protocol.HoverParams) (*protocol.Hover, error) {
return &protocol.Hover{
Contents: protocol.String("hello from the server"),
}, nil
}
func serve(ctx context.Context, stream jsonrpc2.Stream) (jsonrpc2.Conn, protocol.Client) {
// NewServer serves langServer and returns the connection plus a typed
// Client dispatcher for server -> client requests.
ctx, conn, client := protocol.NewServer(ctx, langServer{}, stream)
_ = ctx
return conn, client
}
The mirror-image helper NewClient
serves a Client and hands back a typed Server dispatcher, so the same model
drives both ends of the connection.
Where the LSP says a value is A | B, this package exposes a sealed interface.
Construct a value with the concrete arm and read it back with a type switch:
// Hover.Contents (HoverContents) is one of:
// String | *MarkupContent | *MarkedStringWithLanguage | MarkedStringSlice
hover := &protocol.Hover{Contents: protocol.String("plain text")}
switch c := hover.Contents.(type) {
case protocol.String:
// a bare string arm
case *protocol.MarkupContent:
// structured markup
default:
_ = c
}
Because the arm set is closed by an unexported marker method, no value outside the package can satisfy the interface — the type switch is exhaustive by construction.
Generated byte-walkers make one GC-managed copy of the input before decoding it. To avoid per-field allocations on hot paths, unescaped decoded strings and raw JSON value fields may alias that single owned copy. Two consequences:
Unmarshal (or the LSP codec) returns, you may freely reuse or mutate
the original []byte — the decoded value no longer references it.Clone.Generated URI fields use go.lsp.dev/uri.URI directly. Construct new
values with uri.Parse, uri.File, or uri.From.
protocol.URI remains as a package-local named type for compatibility and for
the sealed union arms (such as RelativePatternBaseURI) that require a local
marker-method receiver. Prefer go.lsp.dev/uri.URI in ordinary code; convert
explicitly with protocol.URI(u) only when assigning a URI string to such an
arm.
The codec is the project's hot path, and it is benchmarked and gated in CI. A
relative-regression gate (.github/workflows/bench.yaml) benchmarks the base
ref and the PR head on the same runner and fails on a >5% sec/op geomean
regression. The figures below are from the Phase-3 authoritative re-baseline
(Intel Xeon 8481C, linux/amd64), comparing the optimized codec against the
reflection-based baseline:
| Benchmark | sec/op change |
|---|---|
Encode/initialize_request | −77.93% |
Encode/publish_diagnostics | −15.98% |
Decode/semantic_tokens | −27.92% |
Decode/completion_array | −23.80% |
Decode/completion_list | −13.24% |
Run them yourself:
go test -run='^$' -bench='^(BenchmarkDecode|BenchmarkEncode)$' -benchmem -count=6 .
make test # run the test suite with race detection
make coverage # run tests and emit coverage
make lint # goimports-rereviser + gofumpt + golangci-lint
make generate # regenerate the package from metaModel.json, then format
make help # list all targets
Generated files carry a .gen.go suffix; edit the generator under
internal/genlsp and run make generate rather than
hand-editing them.
| Module | Role |
|---|---|
go.lsp.dev/jsonrpc2 | JSON-RPC 2.0 transport for the RPC layer |
go.lsp.dev/uri | RFC 3986 / file:// URI handling |
go-json-experiment/json | JSON engine (the github.com/go-json-experiment/json prototype) |
BSD 3-Clause. See LICENSE.
Go
97.9%
Makefile
2.1%