gorpc

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 25, 2026 License: MIT Imports: 12 Imported by: 0

README

GoRPC

Go Reference Go Report Card CI

Small Go-to-Go RPC for internal service calls.

This is meant to keep the useful shape of net/rpc without inheriting gob as the default wire choice or dragging in protobuf, schema files, generated stubs, duplicate DTO models, service discovery, load balancing, or cross-language ceremony.

Current Scope

  • TCP listener/client
  • Single full-duplex connection
  • Length-prefixed MessagePack frames
  • Shared Go request/response structs
  • Unary request/response calls
  • Request IDs in every request/response frame
  • Context deadline propagation and best-effort cancel frames
  • Structured remote errors
  • Max frame size enforcement
  • Basic protocol/version/codec/service handshake
  • Optional slog debug hooks
  • Graceful server shutdown

Server.Serve accepts any net.Listener, and Dial accepts the Go network name, so Unix sockets already work through the same path. They are not yet given extra helper behavior.

Streaming, auth/shared secret handshake fields, service discovery, pub/sub, load balancing, and generated code are intentionally out of v1.

Install

go get github.com/dan-sherwin/gorpc

Example

package channeltracker

import (
	"context"
	"net"

	"github.com/dan-sherwin/gorpc"
)

type GetChannelRequest struct {
	ID string
}

type GetChannelResponse struct {
	ID   string
	Name string
}

type ChannelTracker interface {
	GetChannel(ctx context.Context, req GetChannelRequest) (GetChannelResponse, error)
}

func Serve(ctx context.Context, ln net.Listener, svc ChannelTracker) error {
	server := gorpc.NewServer(gorpc.ServerOptions{
		ServiceName: "channel-tracker",
	})

	gorpc.MustRegister(server, "ChannelTracker", "GetChannel", svc.GetChannel)

	return server.Serve(ctx, ln)
}

type ChannelTrackerClient struct {
	getChannel func(context.Context, GetChannelRequest) (GetChannelResponse, error)
}

func NewChannelTrackerClient(client *gorpc.Client) ChannelTrackerClient {
	return ChannelTrackerClient{
		getChannel: gorpc.Method[GetChannelRequest, GetChannelResponse](client, "ChannelTracker", "GetChannel"),
	}
}

func (c ChannelTrackerClient) GetChannel(ctx context.Context, req GetChannelRequest) (GetChannelResponse, error) {
	return c.getChannel(ctx, req)
}
client, err := gorpc.Dial(ctx, "tcp", "127.0.0.1:9000", gorpc.ClientOptions{
	ClientName:          "manager",
	ExpectedServiceName: "channel-tracker",
})
if err != nil {
	return err
}
defer client.Close()

tracker := NewChannelTrackerClient(client)
channel, err := tracker.GetChannel(ctx, GetChannelRequest{ID: "abc123"})

The handwritten client adapter is optional. Direct calls work too:

resp, err := gorpc.Call[GetChannelRequest, GetChannelResponse](
	ctx,
	client,
	"ChannelTracker",
	"GetChannel",
	GetChannelRequest{ID: "abc123"},
)

License

MIT. See LICENSE.

Versioning

Semantic Versioning. First public tag: v0.1.0.

CI expectations

  • go mod tidy check
  • go build ./...
  • go vet ./...
  • go test ./... -race
  • golangci-lint run
  • govulncheck ./...

Supported Go version: 1.26.3+.

Documentation

Overview

Package gorpc provides a small Go-to-Go RPC transport for internal services.

It is intentionally not a protobuf, gRPC, Connect, or IDL replacement. Both sides share normal Go request and response types, and the wire protocol uses length-prefixed MessagePack frames over a single full-duplex connection.

Index

Constants

View Source
const (
	ErrorCodeCanceled         = "canceled"
	ErrorCodeDeadlineExceeded = "deadline_exceeded"
	ErrorCodeInternal         = "internal"
	ErrorCodeInvalidRequest   = "invalid_request"
	ErrorCodeNotFound         = "not_found"
	ErrorCodeUnavailable      = "unavailable"
)

Remote error codes used by the built-in server and helpers.

View Source
const CodecMessagePack = "msgpack"

CodecMessagePack is the v1 MessagePack codec name used during handshake.

View Source
const DefaultHandshakeTimeout = 5 * time.Second

DefaultHandshakeTimeout is the default timeout for the initial protocol handshake.

View Source
const DefaultMaxFrameSize int64 = 16 * 1024 * 1024

DefaultMaxFrameSize is the default maximum encoded frame size.

View Source
const ProtocolVersion uint16 = 1

ProtocolVersion is the current GoRPC wire protocol version.

Variables

View Source
var (
	ErrClosed         = errors.New("gorpc: closed")
	ErrDuplicateRoute = errors.New("gorpc: duplicate route")
	ErrInvalidRoute   = errors.New("gorpc: invalid route")
)

Common GoRPC errors.

View Source
var (
	ErrFrameTooLarge = errors.New("gorpc: frame too large")
	ErrProtocol      = errors.New("gorpc: protocol error")
)

Frame read/write errors.

Functions

func Call

func Call[Req, Resp any](ctx context.Context, client *Client, service, method string, req Req) (Resp, error)

Call performs a typed unary request/response call.

func MustRegister

func MustRegister[Req, Resp any](s *Server, service, method string, fn HandlerFunc[Req, Resp])

MustRegister is Register that panics on error.

func Register

func Register[Req, Resp any](s *Server, service, method string, fn HandlerFunc[Req, Resp]) error

Register binds a typed unary handler to a service and method name.

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is a single full-duplex connection to a GoRPC server.

func Dial

func Dial(ctx context.Context, network, address string, opts ClientOptions) (*Client, error)

Dial connects to a GoRPC server and completes the protocol handshake.

func (*Client) Close

func (c *Client) Close() error

Close closes the client connection and fails any pending calls.

func (*Client) RemoteService

func (c *Client) RemoteService() string

RemoteService returns the service name reported by the server handshake.

type ClientOptions

type ClientOptions struct {
	ClientName          string
	ExpectedServiceName string
	Codec               Codec
	MaxFrameSize        int64
	HandshakeTimeout    time.Duration
	Logger              *slog.Logger
	Dialer              *net.Dialer
}

ClientOptions configures Dial.

type Codec

type Codec interface {
	Name() string
	Marshal(v any) ([]byte, error)
	Unmarshal(data []byte, v any) error
}

Codec marshals frame envelopes and method payloads.

type Frame

type Frame struct {
	Version          uint16    `msgpack:"version"`
	Type             FrameType `msgpack:"type"`
	RequestID        uint64    `msgpack:"request_id,omitempty"`
	Service          string    `msgpack:"service,omitempty"`
	Method           string    `msgpack:"method,omitempty"`
	DeadlineUnixNano int64     `msgpack:"deadline_unix_nano,omitempty"`
	Payload          []byte    `msgpack:"payload,omitempty"`
}

Frame is the v1 wire envelope. It is MessagePack-encoded and written with a 4-byte big-endian length prefix.

type FrameType

type FrameType uint8

FrameType identifies the kind of message carried by a frame.

const (
	FrameHello FrameType = iota + 1
	FrameHelloAck
	FrameRequest
	FrameResponse
	FrameError
	FrameCancel
	FramePing
	FramePong
	FrameStreamItem
	FrameStreamEnd
)

Frame types used by the v1 protocol.

func (FrameType) String

func (t FrameType) String() string

type HandlerFunc

type HandlerFunc[Req, Resp any] func(context.Context, Req) (Resp, error)

HandlerFunc is the typed function shape used by registered unary methods.

func Method

func Method[Req, Resp any](client *Client, service, method string) HandlerFunc[Req, Resp]

Method returns a typed function bound to a service and method.

type MessagePackCodec

type MessagePackCodec struct{}

MessagePackCodec is the default v1 codec.

func (MessagePackCodec) Marshal

func (MessagePackCodec) Marshal(v any) ([]byte, error)

Marshal encodes v as MessagePack.

func (MessagePackCodec) Name

func (MessagePackCodec) Name() string

Name returns the handshake name for MessagePackCodec.

func (MessagePackCodec) Unmarshal

func (MessagePackCodec) Unmarshal(data []byte, v any) error

Unmarshal decodes MessagePack data into v.

type RemoteError

type RemoteError struct {
	Code    string         `msgpack:"code" json:"code"`
	Message string         `msgpack:"message" json:"message"`
	Details map[string]any `msgpack:"details,omitempty" json:"details,omitempty"`
}

RemoteError is sent in FrameError payloads and returned by callers when the server handled the request but rejected or failed it.

func NewRemoteError

func NewRemoteError(code, message string, details map[string]any) *RemoteError

NewRemoteError creates a structured error suitable for returning from a handler.

func (*RemoteError) Error

func (e *RemoteError) Error() string

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server accepts GoRPC connections and dispatches registered methods.

func NewServer

func NewServer(opts ServerOptions) *Server

NewServer creates a Server with default codec and limits where options are unset.

func (*Server) ListenAndServe

func (s *Server) ListenAndServe(ctx context.Context, network, address string) error

ListenAndServe listens on network/address and serves GoRPC connections.

func (*Server) Serve

func (s *Server) Serve(ctx context.Context, ln net.Listener) error

Serve accepts GoRPC connections from ln until ctx is canceled or Shutdown is called.

func (*Server) Shutdown

func (s *Server) Shutdown(ctx context.Context) error

Shutdown closes the listener, closes active connections, and waits for handlers to exit.

type ServerOptions

type ServerOptions struct {
	ServiceName      string
	Codec            Codec
	MaxFrameSize     int64
	HandshakeTimeout time.Duration
	Logger           *slog.Logger
}

ServerOptions configures a GoRPC server.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL